Merge changes from topic 'star-labels-part-2'

* changes:
  Support ignore label that suppresses notifications on update
  Include star labels into ChangeInfo
diff --git a/.buckconfig b/.buckconfig
index c697fc8..3d5e9d8 100644
--- a/.buckconfig
+++ b/.buckconfig
@@ -8,6 +8,7 @@
   headless = //:headless
   polygerrit = //:polygerrit
   release = //:release
+  releasenotes = //ReleaseNotes:html
   safari = //:safari
   soyc = //gerrit-gwtui:ui_soyc
   soyc_r = //gerrit-gwtui:ui_soyc_r
diff --git a/Documentation/BUCK b/Documentation/BUCK
index ea95063..48ca579 100644
--- a/Documentation/BUCK
+++ b/Documentation/BUCK
@@ -14,7 +14,7 @@
 genasciidoc(
   name = 'html',
   out = 'html.zip',
-  docdir = DOC_DIR,
+  directory = DOC_DIR,
   srcs = SRCS + [':licenses.txt'],
   attributes = documentation_attributes(git_describe()),
   backend = 'html5',
@@ -24,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',
@@ -57,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 0123724..771e323 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -204,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
diff --git a/Documentation/asciidoc.defs b/Documentation/asciidoc.defs
index 1cf0790..4b17071 100644
--- a/Documentation/asciidoc.defs
+++ b/Documentation/asciidoc.defs
@@ -52,7 +52,7 @@
 
     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,
@@ -72,40 +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',
-      ]) + [
-        '//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-index-changes.txt b/Documentation/cmd-index-changes.txt
new file mode 100644
index 0000000..8566827
--- /dev/null
+++ b/Documentation/cmd-index-changes.txt
@@ -0,0 +1,40 @@
+= gerrit index changes
+
+== NAME
+gerrit index changes - Index one or more changes.
+
+== SYNOPSIS
+--
+'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.txt b/Documentation/cmd-index.txt
index 90212fb..e244228 100644
--- a/Documentation/cmd-index.txt
+++ b/Documentation/cmd-index.txt
@@ -126,6 +126,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.
 
diff --git a/Documentation/cmd-stream-events.txt b/Documentation/cmd-stream-events.txt
index c45bbdb..028bd58 100644
--- a/Documentation/cmd-stream-events.txt
+++ b/Documentation/cmd-stream-events.txt
@@ -181,23 +181,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
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index b88f1a3..34cff05 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -2063,11 +2063,6 @@
 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
diff --git a/Documentation/config-hooks.txt b/Documentation/config-hooks.txt
index 980c612..1d92b49 100644
--- a/Documentation/config-hooks.txt
+++ b/Documentation/config-hooks.txt
@@ -78,14 +78,6 @@
   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.
diff --git a/Documentation/config-plugins.txt b/Documentation/config-plugins.txt
index 86570bd..5dec21d 100644
--- a/Documentation/config-plugins.txt
+++ b/Documentation/config-plugins.txt
@@ -118,14 +118,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].
@@ -191,16 +189,6 @@
 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
 
@@ -559,6 +547,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
 
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 8315776..dbaa3c1 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -94,7 +94,6 @@
   Implementation-Title: Example plugin showing examples
   Implementation-Version: 1.0
   Implementation-Vendor: Example, Inc.
-  Implementation-URL: http://example.com/opensource/plugin-foo/
 ====
 
 === ApiType
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
index fc1ad10..5ede117 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -335,9 +335,12 @@
 * Build the release notes:
 +
 ----
-  make -C ReleaseNotes
+  buck build releasenotes
 ----
 
+* Extract the release notes files from the zip file generated from the previous
+step: `buck-out/gen/ReleaseNotes/html/html.zip`.
+
 * Extract the documentation files from the zip file generated from
 `buck build docs`: `buck-out/gen/Documentation/searchfree/searchfree.zip`.
 
diff --git a/Documentation/images/link.png b/Documentation/images/link.png
deleted file mode 100644
index 25eacb7..0000000
--- a/Documentation/images/link.png
+++ /dev/null
Binary files differ
diff --git a/Documentation/replace_macros.py b/Documentation/replace_macros.py
index 0930572..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");
@@ -171,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,
@@ -183,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-accounts.txt b/Documentation/rest-api-accounts.txt
index 8e2dae9..bb61c3d 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -1300,7 +1300,7 @@
 
 .Request
 ----
-  GET /a/accounts/self/preferences.diff HTTP/1.0
+  PUT /a/accounts/self/preferences.diff HTTP/1.0
   Content-Type: application/json; charset=UTF-8
 
   {
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 661abb0..345f759 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -936,6 +936,103 @@
   ]
 ----
 
+[[get-diff-preferences]]
+=== Get 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 Diff Preferences
+
+--
+'PUT /config/server/preferences.diff'
+--
+
+Sets the default diff preferences for the server. Default diff preferences can
+only be set by a Gerrit link:access-control.html#administrators[administrator].
+At least one field of alink:rest-api-accounts.html#diff-preferences-info[
+DiffPreferencesInfo] must be provided in the request body.
+
+.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
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index cbba2f2..36e7489 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -923,6 +923,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
 
@@ -1651,7 +1788,7 @@
   }
 ----
 
-[[get-content]]
+[[get-content-from-commit]]
 === Get Content
 --
 'GET /projects/link:#project-name[\{project-name\}]/commits/link:#commit-id[\{commit-id\}]/files/link:rest-api-changes.html#file-id[\{file-id\}]/content'
@@ -2293,6 +2430,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
@@ -2472,60 +2630,6 @@
 The path to the `GerritSiteFooter.html` file.
 |=============================
 
-[[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
-  }
 ----
 
 GERRIT
diff --git a/Documentation/user-review-ui.txt b/Documentation/user-review-ui.txt
index 8553634..b7311d6 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.
diff --git a/Documentation/user-submodules.txt b/Documentation/user-submodules.txt
index a8a0262..af2b344 100644
--- a/Documentation/user-submodules.txt
+++ b/Documentation/user-submodules.txt
@@ -46,9 +46,9 @@
 With this feature, one could attach 'sub' inside of 'super' repository
 at path 'sub' by executing the following command when being inside
 'super':
-=====
+====
 git submodule add ssh://server/sub sub
-=====
+====
 
 Still considering the above example, after its execution notice that
 inside the local repository 'super' the 'sub' folder is considered a
@@ -85,7 +85,7 @@
 ====
 and add the following lines:
 ====
-  [subscribe "<superproject>"]
+  [allowSuperproject "<superproject>"]
     refs = <refspec>
 ====
 where the 'superproject' should be the exact project name of the superproject.
@@ -100,6 +100,15 @@
 ====
 After the change is integrated a superproject subscription is possible.
 
+The configuration is inherited from parent projects, such that you can have
+a configuration in the "All-Projects" project like:
+====
+    [allowSuperproject "my-only-superproject"]
+        refs = refs/heads/*:refs/heads/*
+====
+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
@@ -153,14 +162,14 @@
 'stable':
 ====
   [allowSuperproject "<superproject>"]
-    refs/heads/*:refs/heads/*
+    refs = refs/heads/*:refs/heads/*
 ====
 
 If you want to enable a branch to be subscribed to any other branch of
 the superproject, omit the second part of the RefSpec:
 ====
   [allowSuperproject "<superproject>"]
-    refs/heads/<submodule-branch>
+    refs = refs/heads/<submodule-branch>
 ====
 
 === Subscription Limitations
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.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
new file mode 100644
index 0000000..52ee3fe
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.11.9.txt
@@ -0,0 +1,49 @@
+= Release notes for Gerrit 2.11.9
+
+Gerrit 2.11.9 is now available:
+
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.11.9.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.11.9.war]
+
+There are no schema changes from link:ReleaseNotes-2.11.8.html[2.11.8].
+
+== 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
+visible.
++
+When querying changes with the `gerrit query` ssh command, and passing the
+`--current-patch-set` option, the current patch set was included even when
+it is not visible to the caller (for example when the patch set is a draft,
+and the caller cannot see drafts).
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3970[Issue 3970]:
+Fix keyboard shortcuts for special processing of CTRL and META.
++
+The processing of CTRL and META was incorrectly removed in Gerrit version
+2.11.8, resulting in shortcuts like 'STRG+T' being interpreted as 'T'.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=4056[Issue 4056]:
+Fix download URLs for BouncyCastle libraries.
++
+The location of the libraries was moved, so the download URLs are updated
+accordingly.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=4055[Issue 4055]:
+Fix subject for 'Updated Changes' lines on push.
++
+When a change was updated it showed the subject from the previous patch set
+instead of the subject from the new current patch set.
+
+* Fix incorrect loading of access sections in `project.config` files.
+
+* Fix internal server error when `auth.userNameToLowerCase` is enabled
+and the auth backend does not provide the username.
+
+* Fix error reindexing changes when a change no longer exists.
+
+* Fix internal server error when loading submit rules.
+
+* Fix internal server error when parsing tracking footers from commit
+messages.
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.txt b/ReleaseNotes/ReleaseNotes-2.12.txt
index 797a138..65a4484 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.txt b/ReleaseNotes/ReleaseNotes-2.13.txt
index 071dc30..07ceb4d 100644
--- a/ReleaseNotes/ReleaseNotes-2.13.txt
+++ b/ReleaseNotes/ReleaseNotes-2.13.txt
@@ -1,5 +1,4 @@
-Release notes for Gerrit 2.13
-=============================
+= Release notes for Gerrit 2.13
 
 
 Gerrit 2.13 is now available:
@@ -8,23 +7,19 @@
 https://www.gerritcodereview.com/download/gerrit-2.13.war]
 
 
-Important Notes
----------------
+== Important Notes
 
 TODO
 
 
-Release Highlights
-------------------
+== Release Highlights
 
 * Metrics interface.
 
 
-New Features
-------------
+== New Features
 
-Metrics
-~~~~~~~
+=== Metrics
 
 Metrics about Gerrit's internal state can be sent to external
 monitoring systems.
@@ -66,21 +61,18 @@
 
 * TODO add more
 
-Changes
-~~~~~~~
+=== 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.
 
-Bug Fixes
----------
+== Bug Fixes
 
 TODO
 
 
-Upgrades
---------
+== Upgrades
 
 * Upgrade CodeMirror to 5.14.2
 
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 4cab151..49491ac 100644
--- a/ReleaseNotes/index.txt
+++ b/ReleaseNotes/index.txt
@@ -1,21 +1,18 @@
-Gerrit Code Review - Release Notes
-==================================
+= Gerrit Code Review - Release Notes
 
-[[2_13]]
-Version 2.13.x
---------------
+[[s2_13]]
+== Version 2.13.x
 * link:ReleaseNotes-2.13.html[2.13]
 
-[[2_12]]
-Version 2.12.x
---------------
+[[s2_12]]
+== Version 2.12.x
 * link:ReleaseNotes-2.12.2.html[2.12.2]
 * link:ReleaseNotes-2.12.1.html[2.12.1]
 * link:ReleaseNotes-2.12.html[2.12]
 
-[[2_11]]
-Version 2.11.x
---------------
+[[s2_11]]
+== Version 2.11.x
+* link:ReleaseNotes-2.11.9.html[2.11.9]
 * link:ReleaseNotes-2.11.8.html[2.11.8]
 * link:ReleaseNotes-2.11.7.html[2.11.7]
 * link:ReleaseNotes-2.11.6.html[2.11.6]
@@ -26,9 +23,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.7.html[2.10.7]
 * link:ReleaseNotes-2.10.6.html[2.10.6]
 * link:ReleaseNotes-2.10.5.html[2.10.5]
@@ -39,18 +35,16 @@
 * 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.4.html[2.9.4]
 * link:ReleaseNotes-2.9.3.html[2.9.3]
 * link:ReleaseNotes-2.9.2.html[2.9.2]
 * 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]
@@ -60,20 +54,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]
@@ -82,33 +73,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]
@@ -129,9 +116,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/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/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 1b3b9d9..f681fd5 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
@@ -81,7 +81,6 @@
 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;
@@ -532,7 +531,7 @@
 
   private Context newRequestContext(TestAccount account) {
     return atrScope.newContext(reviewDbProvider, new SshSession(server, account),
-        identifiedUserFactory.create(Providers.of(db), account.getId()));
+        identifiedUserFactory.create(account.getId()));
   }
 
   protected Context setApiUser(TestAccount account) {
@@ -717,7 +716,7 @@
   }
 
   protected IdentifiedUser user(TestAccount testAccount) {
-    return identifiedUserFactory.create(Providers.of(db), testAccount.getId());
+    return identifiedUserFactory.create(testAccount.getId());
   }
 
   protected RevisionResource parseCurrentRevisionResource(String changeId)
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 84e7557..f2c701b 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
@@ -298,7 +298,7 @@
         throws OrmException, NoSuchChangeException {
       Iterable<Account.Id> actualIds = approvalsUtil
           .getReviewers(db, notesFactory.createChecked(db, c))
-          .values();
+          .all();
       assertThat(actualIds).containsExactlyElementsIn(
           Sets.newHashSet(TestAccount.ids(expectedReviewers)));
     }
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 4c8ba42..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
@@ -54,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);
   }
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 7dc4cda..5d038c4 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
@@ -629,7 +629,7 @@
         .votes();
 
     assertThat(m).hasSize(1);
-    assertThat(m).containsEntry("Code-Review", new Short((short)2));
+    assertThat(m).containsEntry("Code-Review", Short.valueOf((short)2));
 
     setApiUser(user);
     gApi.changes()
@@ -643,7 +643,7 @@
         .votes();
 
     assertThat(m).hasSize(1);
-    assertThat(m).containsEntry("Code-Review", new Short((short)-1));
+    assertThat(m).containsEntry("Code-Review", Short.valueOf((short)-1));
   }
 
   @Test
@@ -681,7 +681,7 @@
       // 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", new Short((short)0));
+      assertThat(m).containsEntry("Code-Review", Short.valueOf((short)0));
     }
 
     ChangeInfo c = gApi.changes()
@@ -1090,7 +1090,7 @@
 
   @Test
   public void noteDbCommitsOnPatchSetCreation() throws Exception {
-    assume().that(notesMigration.enabled()).isTrue();
+    assume().that(notesMigration.readChanges()).isTrue();
 
     PushOneCommit.Result r = createChange();
     pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
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 fd7b359..e63b28ba 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
@@ -456,8 +456,8 @@
 
   @Test
   public void testPushForMasterWithHashtagsNoteDbDisabled() throws Exception {
-    // push with hashtags should fail when noteDb is disabled
-    assume().that(notesMigration.enabled()).isFalse();
+    // 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");
   }
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 bb1a656..b504d25 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
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.SubscribeSection;
 import com.google.gerrit.reviewdb.client.Project;
@@ -35,14 +36,19 @@
 import java.util.concurrent.atomic.AtomicInteger;
 
 public abstract class AbstractSubmoduleSubscription extends AbstractDaemonTest {
-  protected TestRepository<?> createProjectWithPush(String name)
-      throws Exception {
-    Project.NameKey project = createProject(name);
+  protected TestRepository<?> createProjectWithPush(String name,
+      @Nullable Project.NameKey parent) throws Exception {
+    Project.NameKey project = createProject(name, parent);
     grant(Permission.PUSH, project, "refs/heads/*");
     grant(Permission.SUBMIT, project, "refs/for/refs/heads/*");
     return cloneProject(project);
   }
 
+  protected TestRepository<?> createProjectWithPush(String name)
+      throws Exception {
+    return createProjectWithPush(name, null);
+  }
+
   private static AtomicInteger contentCounter = new AtomicInteger(0);
 
   protected ObjectId pushChangeTo(TestRepository<?> repo, String ref,
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..6a4454f
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSectionParserIT.java
@@ -0,0 +1,370 @@
+// 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);
+  }
+}
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 e69a647..4af4a41 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
@@ -17,8 +17,12 @@
 import static com.google.common.truth.Truth.assertThat;
 
 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;
@@ -26,7 +30,12 @@
 import org.eclipse.jgit.transport.RefSpec;
 import org.junit.Test;
 
+@NoHttpd
 public class SubmoduleSubscriptionsIT extends AbstractSubmoduleSubscription {
+  @ConfigSuite.Config
+  public static Config submitWholeTopicEnabled() {
+    return submitWholeTopicEnabledConfig();
+  }
 
   @Test
   @GerritConfig(name = "submodule.enableSuperProjectSubscriptions", value = "false")
@@ -34,9 +43,11 @@
     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();
+    assertThat(hasSubmodule(superRepo, "master",
+        "subscribed-to-project")).isFalse();
   }
 
   @Test
@@ -46,7 +57,8 @@
     allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
         "super-project", "refs/heads/master");
 
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master",
+        "subscribed-to-project", "master");
     pushChangeTo(subRepo, "master");
     ObjectId subHEAD = pushChangeTo(subRepo, "master");
     expectToHaveSubmoduleState(superRepo, "master",
@@ -61,7 +73,8 @@
         "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");
     expectToHaveSubmoduleState(superRepo, "master",
         "subscribed-to-project", subHEAD);
@@ -78,8 +91,10 @@
     pushChangeTo(superRepo, "branch");
 
     pushChangeTo(subRepo, "master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
-    createSubmoduleSubscription(superRepo, "branch", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master",
+        "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "branch",
+        "subscribed-to-project", "master");
 
     ObjectId subHEAD = pushChangeTo(subRepo, "master");
 
@@ -102,8 +117,10 @@
     pushChangeTo(subRepo, "branch");
 
     pushChangeTo(subRepo, "master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
-    createSubmoduleSubscription(superRepo, "branch", "subscribed-to-project", "branch");
+    createSubmoduleSubscription(superRepo, "master",
+        "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "branch",
+        "subscribed-to-project", "branch");
 
     ObjectId subHEAD1 = pushChangeTo(subRepo, "master");
     ObjectId subHEAD2 = pushChangeTo(subRepo, "branch");
@@ -114,7 +131,8 @@
         "subscribed-to-project", subHEAD2);
 
     // Now test that cross subscriptions do not work:
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "branch");
+    createSubmoduleSubscription(superRepo, "master",
+        "subscribed-to-project", "branch");
     ObjectId subHEAD3 = pushChangeTo(subRepo, "branch");
 
     expectToHaveSubmoduleState(superRepo, "master",
@@ -132,7 +150,8 @@
         "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");
@@ -157,7 +176,8 @@
         "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
@@ -187,7 +207,8 @@
         "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");
@@ -205,14 +226,16 @@
   }
 
   @Test
-  public void testSubscriptionUnsubscribeByDeletingGitModules() throws Exception {
+  public void testSubscriptionUnsubscribeByDeletingGitModules()
+      throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
     allowSubmoduleSubscription("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");
@@ -236,7 +259,8 @@
     allowSubmoduleSubscription("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");
 
@@ -254,16 +278,18 @@
         "subscribed-to-project", "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    pushChangeTo(superRepo, "master");
+    createSubmoduleSubscription(superRepo, "master",
+        "subscribed-to-project", "master");
     createSubmoduleSubscription(subRepo, "master", "super-project", "master");
 
-    ObjectId subHEAD = pushChangeTo(subRepo, "master");
+    pushChangeTo(subRepo, "master");
     pushChangeTo(superRepo, "master");
 
-    expectToHaveSubmoduleState(superRepo, "master",
-        "subscribed-to-project", subHEAD);
-
-    assertThat(hasSubmodule(subRepo, "master", "super-project")).isFalse();
+    assertThat(hasSubmodule(subRepo, "master",
+        "super-project")).isFalse();
+    assertThat(hasSubmodule(superRepo, "master",
+        "subscribed-to-project")).isFalse();
   }
 
 
@@ -273,9 +299,11 @@
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
 
     pushChangeTo(subRepo, "master");
-    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();
+    assertThat(hasSubmodule(superRepo, "master",
+        "subscribed-to-project")).isFalse();
   }
 
   @Test
@@ -286,9 +314,11 @@
         "wrong-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");
-    assertThat(hasSubmodule(superRepo, "master", "subscribed-to-project")).isFalse();
+    assertThat(hasSubmodule(superRepo, "master",
+        "subscribed-to-project")).isFalse();
   }
 
   @Test
@@ -299,9 +329,28 @@
         "super-project", "refs/heads/wrong-branch");
 
     pushChangeTo(subRepo, "master");
-    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();
+    assertThat(hasSubmodule(superRepo, "master",
+        "subscribed-to-project")).isFalse();
+  }
+
+  @Test
+  public void testSubscriptionInheritACL() throws Exception {
+    createProjectWithPush("config-repo");
+    TestRepository<?> superRepo = createProjectWithPush("super-project",
+        new Project.NameKey(name("config-repo")));
+    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+    allowSubmoduleSubscription("config-repo", "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();
   }
 
   private void deleteAllSubscriptions(TestRepository<?> repo, String branch)
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 30482dd..0a067e9 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
@@ -93,6 +93,71 @@
   }
 
   @Test
+  public void testSubscriptionUpdateIncludingChangeInSuperproject() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+    allowSubmoduleSubscription("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");
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 cfe04a2..3e2b9a6 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
@@ -38,7 +38,6 @@
 import com.google.gerrit.server.project.Util;
 import com.google.gerrit.testutil.DisabledReviewDb;
 import com.google.inject.Inject;
-import com.google.inject.util.Providers;
 
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
@@ -272,7 +271,7 @@
     AcceptanceTestRequestScope.Context ctx = disableDb();
     try (Repository repo = repoManager.openRepository(project)) {
       ProjectControl ctl = projectControlFactory.controlFor(project,
-          identifiedUserFactory.create(Providers.of(db), user.getId()));
+          identifiedUserFactory.create(user.getId()));
       VisibleRefFilter filter = new VisibleRefFilter(
           tagCache, changeCache, repo, ctl, new DisabledReviewDb(), true);
       Map<String, Ref> all = repo.getAllRefs();
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 5403e0d..7fb91f1 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
@@ -95,7 +95,7 @@
     return submitWholeTopicEnabledConfig();
   }
 
-  private Map<String, String> mergeResults;
+  private Map<String, String> changeMergedEvents;
 
   @Inject
   private ApprovalsUtil approvalsUtil;
@@ -127,7 +127,7 @@
 
   @Before
   public void setUp() throws Exception {
-    mergeResults = new HashMap<>();
+    changeMergedEvents = new HashMap<>();
     eventListenerRegistration =
         eventListeners.add(new UserScopedEventListener() {
           @Override
@@ -139,7 +139,7 @@
             ChangeAttribute c = e.change.get();
             PatchSetAttribute ps = e.patchSet.get();
             log.debug("Merged {},{} as {}", ps.number, c.number, e.newRev);
-            mergeResults.put(e.change.get().number, e.newRev);
+            changeMergedEvents.put(e.change.get().number, e.newRev);
           }
 
           @Override
@@ -305,8 +305,8 @@
     // newRev of the ChangeMergedEvent.
     BranchInfo branch = gApi.projects().name(change.project)
         .branch(change.branch).get();
-    assertThat(mergeResults).isNotEmpty();
-    String newRev = mergeResults.get(Integer.toString(change._number));
+    assertThat(changeMergedEvents).isNotEmpty();
+    String newRev = changeMergedEvents.get(Integer.toString(change._number));
     assertThat(newRev).isNotNull();
     assertThat(branch.revision).isEqualTo(newRev);
   }
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 b8f0ec9..6781ef1 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,6 +26,7 @@
 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.server.git.ProjectConfig;
 import com.google.gerrit.server.project.Util;
@@ -56,6 +57,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 +87,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("refs/meta/config").get().revision;
+    assertThat(changeRev).isEqualTo(branchRev);
+    return id;
   }
 
   @Test
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 924eb4d..f8640bd 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
@@ -116,7 +116,7 @@
 
   @Test
   public void noteDbCommit() throws Exception {
-    assume().that(notesMigration.enabled()).isTrue();
+    assume().that(notesMigration.readChanges()).isTrue();
 
     ChangeInfo c = assertCreateSucceeds(newChangeInput(ChangeStatus.NEW));
     try (Repository repo = repoManager.openRepository(project);
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 c0e0efd..300905e 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
@@ -4,11 +4,14 @@
 
 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.changes.SubmitInput;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.reviewdb.client.Project;
 
 import org.eclipse.jgit.junit.TestRepository;
@@ -414,4 +417,21 @@
         "Change " + change3.getPatchSetId().getParentKey().get() +
         ": depends on change that was not submitted");
   }
+
+  @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
+    submit(changeResult.getChangeId(), new SubmitInput(),
+        ResourceConflictException.class, "nothing to merge", false);
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/DiffPreferencesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/DiffPreferencesIT.java
new file mode 100644
index 0000000..e68b4af
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/DiffPreferencesIT.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.acceptance.rest.config;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.config.ConfigUtil.skipField;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+
+import org.junit.Test;
+
+import java.lang.reflect.Field;
+
+public class DiffPreferencesIT extends AbstractDaemonTest {
+
+  @Test
+  public void GetDiffPreferences() throws Exception {
+    DiffPreferencesInfo result = get();
+    assertPrefsEqual(result, DiffPreferencesInfo.defaults());
+  }
+
+  @Test
+  public void SetDiffPreferences() throws Exception {
+    int newLineLength = DiffPreferencesInfo.defaults().lineLength + 10;
+    DiffPreferencesInfo update = new DiffPreferencesInfo();
+    update.lineLength = newLineLength;
+    DiffPreferencesInfo result = put(update);
+    assertThat(result.lineLength).named("lineLength").isEqualTo(newLineLength);
+
+    result = get();
+    DiffPreferencesInfo expected = DiffPreferencesInfo.defaults();
+    expected.lineLength = newLineLength;
+    assertPrefsEqual(result, expected);
+  }
+
+  private DiffPreferencesInfo get() throws Exception {
+    RestResponse r = adminRestSession.get("/config/server/preferences.diff");
+    r.assertOK();
+    return newGson().fromJson(r.getReader(), DiffPreferencesInfo.class);
+  }
+
+  private DiffPreferencesInfo put(DiffPreferencesInfo input) throws Exception {
+    RestResponse r = adminRestSession.put(
+        "/config/server/preferences.diff", input);
+    r.assertOK();
+    return newGson().fromJson(r.getReader(), DiffPreferencesInfo.class);
+  }
+
+  private void assertPrefsEqual(DiffPreferencesInfo actual,
+      DiffPreferencesInfo expected) throws Exception {
+    for (Field field : actual.getClass().getDeclaredFields()) {
+      if (skipField(field)) {
+        continue;
+      }
+      Object actualField = field.get(actual);
+      Object expectedField = field.get(expected);
+      Class<?> type = field.getType();
+      if ((type == boolean.class || type == Boolean.class)
+          && actualField == null) {
+        continue;
+      }
+      assertThat(actualField).named(field.getName()).isEqualTo(expectedField);
+    }
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AccessIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AccessIT.java
index 9a5dfeb..255fe57 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AccessIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AccessIT.java
@@ -16,16 +16,407 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+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.server.config.AllProjectsNameProvider;
+import com.google.gerrit.server.group.SystemGroupBackend;
 
+import org.eclipse.jgit.lib.Constants;
+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 testGetDefaultInheritance() throws Exception {
-    String newProjectName = createProject("newProjectAccess").get();
-    String inheritedName = gApi.projects()
-      .name(newProjectName).access().inheritsFrom.name;
+  public void getDefaultInheritance() throws Exception {
+    String inheritedName = pApi.access().inheritsFrom.name;
     assertThat(inheritedName).isEqualTo(AllProjectsNameProvider.DEFAULT);
   }
+
+  @Test
+  public void addAccessSection() throws Exception {
+    ProjectAccessInput accessInput = newProjectAccessInput();
+    AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
+
+    accessInput.add.put(REFS_HEADS, accessSectionInfo);
+    pApi.access(accessInput);
+
+    assertThat(pApi.access().local).isEqualTo(accessInput.add);
+  }
+
+  @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());
+  }
+
+  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/server/notedb/ChangeRebuilderIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
index 274b7d7..ea4909f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
@@ -17,6 +17,9 @@
 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 com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -35,7 +38,6 @@
 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.RefNames;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
@@ -44,6 +46,7 @@
 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.RepoRefCache;
 import com.google.gerrit.server.notedb.ChangeBundle;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.NoteDbChangeState;
@@ -56,7 +59,9 @@
 import com.google.inject.Provider;
 
 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;
@@ -297,14 +302,14 @@
 
     putDraft(user, id, 1, "comment by user");
     ObjectId userDraftsId = getMetaRef(
-        allUsers, RefNames.refsDraftComments(id, user.getId()));
+        allUsers, refsDraftComments(id, user.getId()));
     assertThat(unwrapDb().changes().get(id).getNoteDbState()).isEqualTo(
         changeMetaId.name()
         + "," + user.getId() + "=" + userDraftsId.name());
 
     putDraft(admin, id, 2, "comment by admin");
     ObjectId adminDraftsId = getMetaRef(
-        allUsers, RefNames.refsDraftComments(id, admin.getId()));
+        allUsers, refsDraftComments(id, admin.getId()));
     assertThat(admin.getId().get()).isLessThan(user.getId().get());
     assertThat(unwrapDb().changes().get(id).getNoteDbState()).isEqualTo(
         changeMetaId.name()
@@ -313,7 +318,7 @@
 
     putDraft(admin, id, 2, "revised comment by admin");
     adminDraftsId = getMetaRef(
-        allUsers, RefNames.refsDraftComments(id, admin.getId()));
+        allUsers, refsDraftComments(id, admin.getId()));
     assertThat(unwrapDb().changes().get(id).getNoteDbState()).isEqualTo(
         changeMetaId.name()
         + "," + admin.getId() + "=" + adminDraftsId.name()
@@ -578,6 +583,56 @@
     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();
+    }
+  }
+
   private void setInvalidNoteDbState(Change.Id id) throws Exception {
     ReviewDb db = unwrapDb();
     Change c = db.changes().get(id);
@@ -595,7 +650,8 @@
       Change c = unwrapDb().changes().get(id);
       assertThat(c).isNotNull();
       assertThat(c.getNoteDbState()).isNotNull();
-      assertThat(NoteDbChangeState.parse(c).isChangeUpToDate(repo))
+      assertThat(NoteDbChangeState.parse(c).isChangeUpToDate(
+              new RepoRefCache(repo)))
           .isEqualTo(expected);
     }
   }
@@ -607,7 +663,8 @@
       assertThat(c).isNotNull();
       assertThat(c.getNoteDbState()).isNotNull();
       NoteDbChangeState state = NoteDbChangeState.parse(c);
-      assertThat(state.areDraftsUpToDate(repo, account.getId()))
+      assertThat(state.areDraftsUpToDate(
+              new RepoRefCache(repo), account.getId()))
           .isEqualTo(expected);
     }
   }
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..993820b 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
@@ -75,6 +75,10 @@
     }
   }
 
+  public void addPermission(Permission p) {
+    getPermissions().add(p);
+  }
+
   public void remove(Permission permission) {
     if (permission != null) {
       removePermission(permission.getName());
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SubscribeSection.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubscribeSection.java
index b05f335..6cee630 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SubscribeSection.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubscribeSection.java
@@ -68,4 +68,18 @@
   public Collection<RefSpec> getRefSpecs() {
     return Collections.unmodifiableCollection(refSpecs);
   }
+
+  @Override
+  public String toString() {
+    StringBuilder ret = new StringBuilder();
+    ret.append("[SubscribeSection, project=");
+    ret.append(project);
+    ret.append(", refs=[");
+    for (RefSpec r : refSpecs) {
+      ret.append(r.toString());
+      ret.append(", ");
+    }
+    ret.append("]");
+    return ret.toString();
+  }
 }
diff --git a/gerrit-extension-api/BUCK b/gerrit-extension-api/BUCK
index 67bfdc1..9b83c5a 100644
--- a/gerrit-extension-api/BUCK
+++ b/gerrit-extension-api/BUCK
@@ -38,6 +38,9 @@
 java_library(
   name = 'api',
   srcs = glob([SRC + '**/*.java']),
+  deps = [
+    '//gerrit-common:annotations',
+  ],
   provided_deps = [
     '//lib/guice:guice',
     '//lib/guice:guice-assistedinject',
@@ -72,6 +75,7 @@
     '//lib/guice:javax-inject',
     '//lib/guice:guice_library',
     '//lib/guice:guice-assistedinject',
+    '//gerrit-common:annotations',
   ],
   visibility = ['PUBLIC'],
 )
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
index b97fd3b..7c47ec5 100644
--- 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
@@ -14,7 +14,22 @@
 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
index c6d25b3..c4808a5 100644
--- 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
@@ -14,6 +14,7 @@
 package com.google.gerrit.extensions.api.access;
 
 import java.util.Map;
+import java.util.Objects;
 
 public class PermissionInfo {
   public String label;
@@ -24,4 +25,20 @@
     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
index 93990dc..f979039 100644
--- 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
@@ -13,6 +13,8 @@
 // limitations under the License.
 package com.google.gerrit.extensions.api.access;
 
+import java.util.Objects;
+
 public class PermissionRuleInfo {
   public enum Action {
     ALLOW,
@@ -31,4 +33,21 @@
     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/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/projects/ProjectApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
index 75cebaa..260d7ce 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
@@ -15,6 +15,7 @@
 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;
@@ -30,6 +31,7 @@
   void description(PutDescriptionInput in) throws RestApiException;
 
   ProjectAccessInfo access() throws RestApiException;
+  ProjectAccessInfo access(ProjectAccessInput p) throws RestApiException;
 
   ListRefsRequest<BranchInfo> branches();
   ListRefsRequest<TagInfo> tags();
@@ -138,6 +140,12 @@
     }
 
     @Override
+    public ProjectAccessInfo access(ProjectAccessInput p)
+      throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public void description(PutDescriptionInput in)
         throws RestApiException {
       throw new NotImplementedException();
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..c20c9e0 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,7 +14,9 @@
 
 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
@@ -28,6 +30,10 @@
     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-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
index 903bf2e..fe6d719 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
@@ -169,7 +169,7 @@
     if (extId == null) {
       return CheckResult.bad("Key is not associated with any users");
     }
-    IdentifiedUser user = userFactory.create(db, extId.getAccountId());
+    IdentifiedUser user = userFactory.create(extId.getAccountId());
     Set<String> allowedUserIds = getAllowedUserIds(user);
     if (allowedUserIds.isEmpty()) {
       return CheckResult.bad("No identities found for user");
diff --git a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
index ebe8105..749360c 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
@@ -151,12 +151,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() {
     accountCache.evict(userId);
-    user = userFactory.create(Providers.of(db), userId);
+    user = userFactory.create(userId);
     return user;
   }
 
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 d222942..cc5c9b7 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
@@ -215,6 +215,11 @@
         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 (!Gerrit.info().change().isSubmitWholeTopicEnabled()
         && info.topic() != null && !"".equals(info.topic())) {
       StringBuilder topicQuery = new StringBuilder();
@@ -226,11 +231,6 @@
                      ListChangesOption.DETAILED_LABELS,
                      ListChangesOption.LABELS),
           new TabChangeListCallback(Tab.SAME_TOPIC, info.project(), revision));
-    } else {
-      // TODO(sbeller): show only on latest revision
-      ChangeApi.change(info.legacyId().get()).view("submitted_together")
-          .get(new TabChangeListCallback(Tab.SUBMITTED_TOGETHER,
-              info.project(), revision));
     }
   }
 
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 e7fba34..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);
   }
@@ -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/diff/CommentGroup.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentGroup.java
index 20dd883..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
@@ -31,10 +31,11 @@
  */
 abstract class CommentGroup extends Composite {
 
+  final DisplaySide side;
+  final int line;
+
   private final CommentManager manager;
   private final CodeMirror cm;
-  private final DisplaySide side;
-  private final int line;
   private final FlowPanel comments;
   private LineWidget lineWidget;
   private Timer resizeTimer;
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
index 216fbda..ae6d3c1 100644
--- 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
@@ -21,6 +21,8 @@
 
 import net.codemirror.lib.CodeMirror;
 
+import java.util.PriorityQueue;
+
 /**
  * LineWidget attached to a CodeMirror container.
  *
@@ -28,14 +30,15 @@
  * 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 {
+class SideBySideCommentGroup extends CommentGroup
+    implements Comparable<SideBySideCommentGroup> {
   static void pair(SideBySideCommentGroup a, SideBySideCommentGroup b) {
-    a.peer = b;
-    b.peer = a;
+    a.peers.add(b);
+    b.peers.add(a);
   }
 
   private final Element padding;
-  private SideBySideCommentGroup peer;
+  private final PriorityQueue<SideBySideCommentGroup> peers;
 
   SideBySideCommentGroup(SideBySideCommentManager manager, CodeMirror cm, DisplaySide side,
       int line) {
@@ -45,29 +48,42 @@
     padding.setClassName(SideBySideTable.style.padding());
     SideBySideChunkManager.focusOnClick(padding, cm.side());
     getElement().appendChild(padding);
+    peers = new PriorityQueue<>();
   }
 
   SideBySideCommentGroup getPeer() {
-    return peer;
+    return peers.peek();
   }
 
   @Override
   void remove(DraftBox box) {
     super.remove(box);
 
-    if (0 < getBoxCount() || 0 < peer.getBoxCount()) {
-      resize();
-    } else {
+    if (getBoxCount() == 0 && peers.size() == 1
+        && peers.peek().peers.size() > 1) {
+      SideBySideCommentGroup peer = peers.peek();
+      peer.peers.remove(this);
       detach();
-      peer.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 && peer.getLineWidget() == null) {
-      this.attach(parent);
-      peer.attach(parent);
+    if (getLineWidget() == null) {
+      attach(parent);
+    }
+    for (CommentGroup peer : peers) {
+      if (peer.getLineWidget() == null) {
+        peer.attach(parent);
+      }
     }
   }
 
@@ -76,20 +92,20 @@
     getLineWidget().onRedraw(new Runnable() {
       @Override
       public void run() {
-        if (canComputeHeight() && peer.canComputeHeight()) {
+        if (canComputeHeight() && peers.peek().canComputeHeight()) {
           if (getResizeTimer() != null) {
             getResizeTimer().cancel();
             setResizeTimer(null);
           }
-          adjustPadding(SideBySideCommentGroup.this, peer);
+          adjustPadding(SideBySideCommentGroup.this, peers.peek());
         } else if (getResizeTimer() == null) {
           setResizeTimer(new Timer() {
             @Override
             public void run() {
-              if (canComputeHeight() && peer.canComputeHeight()) {
+              if (canComputeHeight() && peers.peek().canComputeHeight()) {
                 cancel();
                 setResizeTimer(null);
-                adjustPadding(SideBySideCommentGroup.this, peer);
+                adjustPadding(SideBySideCommentGroup.this, peers.peek());
               }
             }
           });
@@ -102,7 +118,7 @@
   @Override
   void resize() {
     if (getLineWidget() != null) {
-      adjustPadding(this, peer);
+      adjustPadding(this, peers.peek());
     }
   }
 
@@ -117,6 +133,16 @@
   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);
@@ -125,4 +151,14 @@
     a.updateSelection();
     b.updateSelection();
   }
+
+  @Override
+  public int compareTo(SideBySideCommentGroup o) {
+    if (side == o.side) {
+      return line - o.line;
+    } else {
+      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
index a2af3a1c..2b83b71 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentManager.java
@@ -23,6 +23,7 @@
 import net.codemirror.lib.TextMarker.FromTo;
 
 import java.util.Collection;
+import java.util.Map;
 import java.util.SortedMap;
 
 /** Tracks comment widgets for {@link SideBySide}. */
@@ -94,36 +95,33 @@
 
   @Override
   CommentGroup group(DisplaySide side, int line) {
-    SideBySideCommentGroup w = (SideBySideCommentGroup) map(side).get(line);
-    if (w != null) {
-      return w;
+    CommentGroup existing = map(side).get(line);
+    if (existing != null) {
+      return existing;
     }
 
-    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;
+    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 {
-      lineA = host.lineOnOther(side, line - 1).getLine() + 1;
-      lineB = line;
+      otherGroup = newGroup(side.otherSide(), otherLine);
+      otherMap.put(otherLine, otherGroup);
     }
-
-    SideBySideCommentGroup a = newGroup(DisplaySide.A, lineA);
-    SideBySideCommentGroup b = newGroup(DisplaySide.B, lineB);
-    SideBySideCommentGroup.pair(a, b);
-
-    sideA.put(lineA, a);
-    sideB.put(lineB, b);
+    SideBySideCommentGroup.pair(newGroup, (SideBySideCommentGroup) otherGroup);
 
     if (isAttached()) {
-      a.init(host.getDiffTable());
-      b.handleRedraw();
+      newGroup.init(host.getDiffTable());
+      otherGroup.handleRedraw();
     }
-
-    return side == DisplaySide.A ? a : b;
+    return newGroup;
   }
 
   private SideBySideCommentGroup newGroup(DisplaySide side, int line) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
index aa76128..a96624a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
@@ -35,7 +35,9 @@
   @Override
   public void _onRequestSuggestions(final Request req, final Callback callback) {
     GroupMap.suggestAccountGroupForProject(
-        projectName.get(), req.getQuery(), req.getLimit(),
+        projectName == null ? null : projectName.get(),
+        req.getQuery(),
+        req.getLimit(),
         new GerritCallback<GroupMap>() {
           @Override
           public void onSuccess(GroupMap result) {
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/addon/Addons.java b/gerrit-gwtui/src/main/java/net/codemirror/addon/Addons.java
index 5365cc5..7c8b362 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/addon/Addons.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/addon/Addons.java
@@ -20,7 +20,7 @@
 import com.google.gwt.resources.client.DataResource.DoNotEmbed;
 
 public interface Addons extends ClientBundle {
-  public static final Addons I = GWT.create(Addons.class);
+  Addons I = GWT.create(Addons.class);
 
   @Source("merge_bundled.js") @DoNotEmbed DataResource merge_bundled();
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
index 7d8d22c..fa2e0e3 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
@@ -161,6 +161,7 @@
     key = manager.createKey(id);
     val = manager.createVal(key, id, rememberMe, identity, null, null);
     saveCookie();
+    user = identified.create(val.getAccountId());
   }
 
   /** Set the user account for this current request only. */
@@ -178,6 +179,7 @@
       key = null;
       val = null;
       saveCookie();
+      user = anonymousProvider.get();
     }
   }
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
index 45e5615..2c67182 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
@@ -67,6 +67,8 @@
       serve("/").with(HostPageServlet.class);
       serve("/Gerrit").with(LegacyGerritServlet.class);
       serve("/Gerrit/*").with(legacyGerritScreen());
+      // Forward PolyGerrit URLs to their respective GWT equivalents.
+      serveRegex("^/(c|q|x|admin|dashboard|settings)/(.*)").with(gerritUrl());
     }
     serve("/cat/*").with(CatServlet.class);
 
@@ -87,9 +89,6 @@
     serve("/watched").with(query("is:watched status:open"));
     serve("/starred").with(query("is:starred"));
 
-    // Forward PolyGerrit URLs to their respective GWT equivalents.
-    serveRegex("^/(c|q|x|admin|dashboard|settings)/(.*)").with(gerritUrl());
-
     serveRegex("^/settings/?$").with(screen(PageLinks.SETTINGS));
     serveRegex("^/register/?$").with(screen(PageLinks.REGISTER + "/"));
     serveRegex("^/([1-9][0-9]*)/?$").with(directChangeById());
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
index beb0139..2190fe0 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
@@ -488,7 +488,6 @@
       String t = main.getValue(Attributes.Name.IMPLEMENTATION_TITLE);
       String n = main.getValue(Attributes.Name.IMPLEMENTATION_VENDOR);
       String v = main.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
-      String u = main.getValue(Attributes.Name.IMPLEMENTATION_URL);
       String a = main.getValue("Gerrit-ApiVersion");
 
       html.append("<table class=\"plugin_info\">");
@@ -507,11 +506,6 @@
             .append(v)
             .append("</td></tr>\n");
       }
-      if (!Strings.isNullOrEmpty(u)) {
-        html.append("<tr><th>URL</th><td>")
-            .append(String.format("<a href=\"%s\">%s</a>", u, u))
-            .append("</td></tr>\n");
-      }
       if (!Strings.isNullOrEmpty(a)) {
         html.append("<tr><th>API Version</th><td>")
             .append(a)
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
index 32a80fe..e8efd72 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
@@ -114,7 +114,7 @@
     site = sp;
     refreshHeaderFooter = cfg.getBoolean("site", "refreshHeaderFooter", true);
     staticServlet = ss;
-    isNoteDbEnabled = migration.enabled();
+    isNoteDbEnabled = migration.readChanges();
     pluginsLoadTimeout = getPluginsLoadTimeout(cfg);
     getDiff = diffPref;
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 27f291a..4654655 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -32,6 +32,7 @@
 import static javax.servlet.http.HttpServletResponse.SC_OK;
 import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED;
 
+import com.google.common.base.CharMatcher;
 import com.google.common.base.Function;
 import com.google.common.base.Joiner;
 import com.google.common.base.Splitter;
@@ -130,7 +131,6 @@
 import java.util.Set;
 import java.util.TreeMap;
 import java.util.concurrent.TimeUnit;
-import java.util.regex.Pattern;
 import java.util.zip.GZIPOutputStream;
 
 import javax.servlet.ServletException;
@@ -1039,9 +1039,8 @@
     }
   }
 
-  private static final Pattern IS_HTML = Pattern.compile("[<&]");
   private static boolean isMaybeHTML(String text) {
-    return IS_HTML.matcher(text).find();
+    return CharMatcher.anyOf("<&").matchesAnyOf(text);
   }
 
   private static boolean acceptsJson(HttpServletRequest req) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
index 223629d..da19b5e 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
@@ -87,7 +87,7 @@
     RevCommit commit = config.commit(md);
 
     gitRefUpdated.fire(config.getProject().getNameKey(), RefNames.REFS_CONFIG,
-        base, commit.getId());
+        base, commit.getId(), user.asIdentifiedUser().getAccount());
     hooks.doRefUpdatedHook(
       new Branch.NameKey(config.getProject().getNameKey(), RefNames.REFS_CONFIG),
       base, commit.getId(), user.asIdentifiedUser().getAccount());
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java
index 9ef6956..1c72600 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java
@@ -49,7 +49,7 @@
 import java.util.List;
 
 public class VersionedAuthorizedKeysOnInit extends VersionedMetaData {
-  public static interface Factory {
+  public interface Factory {
     VersionedAuthorizedKeysOnInit create(Account.Id accountId);
   }
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index d701afa..be061c7 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.cache.CacheRemovalListener;
 import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
+import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.ChangeKindCacheImpl;
 import com.google.gerrit.server.change.MergeabilityCacheImpl;
 import com.google.gerrit.server.change.PatchSetInserter;
@@ -139,5 +140,8 @@
     factory(CapabilityControl.Factory.class);
     factory(ChangeData.Factory.class);
     factory(ProjectState.Factory.class);
+
+    bind(ChangeJson.Factory.class).toProvider(
+        Providers.<ChangeJson.Factory>of(null));
   }
 }
diff --git a/gerrit-plugin-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml b/gerrit-plugin-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
index 270e15c..e32a0d6 100644
--- a/gerrit-plugin-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
+++ b/gerrit-plugin-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
@@ -31,9 +31,6 @@
     <requiredProperty key="Implementation-Vendor">
       <defaultValue>Gerrit Code Review</defaultValue>
     </requiredProperty>
-    <requiredProperty key="Implementation-Url">
-      <defaultValue>https://www.gerritcodereview.com/</defaultValue>
-    </requiredProperty>
 
     <requiredProperty key="gerritApiType">
       <defaultValue>plugin</defaultValue>
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml b/gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml
index a6103b1..026e21d 100644
--- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml
@@ -50,7 +50,6 @@
 #end
 
               <Implementation-Vendor>${Implementation-Vendor}</Implementation-Vendor>
-              <Implementation-URL>${Implementation-Url}</Implementation-URL>
 
               <Implementation-Title>${Gerrit-ApiType} ${project.artifactId}</Implementation-Title>
               <Implementation-Version>${project.version}</Implementation-Version>
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml b/gerrit-plugin-gwt-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
index 3c3508c..32a603b 100644
--- a/gerrit-plugin-gwt-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
@@ -21,9 +21,6 @@
     <requiredProperty key="Implementation-Vendor">
       <defaultValue>Gerrit Code Review</defaultValue>
     </requiredProperty>
-    <requiredProperty key="Implementation-Url">
-      <defaultValue>https://www.gerritcodereview.com/</defaultValue>
-    </requiredProperty>
     <requiredProperty key="Gwt-Version">
       <defaultValue>2.7.0</defaultValue>
     </requiredProperty>
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/pom.xml b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/pom.xml
index d67c7cb..2c7fe88 100644
--- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/pom.xml
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/pom.xml
@@ -45,7 +45,6 @@
               <Gerrit-Module>${package}.Module</Gerrit-Module>
               <Gerrit-HttpModule>${package}.HttpModule</Gerrit-HttpModule>
               <Implementation-Vendor>${Implementation-Vendor}</Implementation-Vendor>
-              <Implementation-URL>${Implementation-Url}</Implementation-URL>
 
               <Implementation-Title>${Gerrit-ApiType} ${project.artifactId}</Implementation-Title>
               <Implementation-Version>${project.version}</Implementation-Version>
diff --git a/gerrit-plugin-js-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml b/gerrit-plugin-js-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
index fbf1e46..ef0e96c 100644
--- a/gerrit-plugin-js-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
+++ b/gerrit-plugin-js-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
@@ -21,9 +21,6 @@
     <requiredProperty key="Implementation-Vendor">
       <defaultValue>Gerrit Code Review</defaultValue>
     </requiredProperty>
-    <requiredProperty key="Implementation-Url">
-      <defaultValue>https://gerrit.googlesource.com/</defaultValue>
-    </requiredProperty>
 
     <requiredProperty key="gerritApiType">
       <defaultValue>js</defaultValue>
diff --git a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/pom.xml b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/pom.xml
index f24d81e..8f4aadd 100644
--- a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/pom.xml
+++ b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/pom.xml
@@ -44,7 +44,6 @@
             <manifestEntries>
               <Gerrit-PluginName>${pluginName}</Gerrit-PluginName>
               <Implementation-Vendor>${Implementation-Vendor}</Implementation-Vendor>
-              <Implementation-URL>${Implementation-Url}</Implementation-URL>
 
               <Implementation-Title>${Gerrit-ApiType} ${project.artifactId}</Implementation-Title>
               <Implementation-Version>${project.version}</Implementation-Version>
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
index 88efd50..1864c56 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
@@ -160,7 +160,10 @@
           RefNames.EDIT_PREFIX.length();
       int endChangeId = nextNonDigit(ref, startChangeId);
       String id = ref.substring(startChangeId, endChangeId);
-      return new Change.Id(Integer.parseInt(id));
+      if (id != null && !id.isEmpty()) {
+        return new Change.Id(Integer.parseInt(id));
+      }
+      return null;
     }
 
     public static Id fromRefPart(String ref) {
@@ -277,6 +280,9 @@
   /** ID number of the first patch set in a change. */
   public static final int INITIAL_PATCH_SET_ID = 1;
 
+  /** Change-Id pattern. */
+  public static final String CHANGE_ID_PATTERN = "^[iI][0-9a-f]{4,}.*$";
+
   /**
    * Current state within the basic workflow of the change.
    *
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
index 6d84599..458c99a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
@@ -49,7 +49,6 @@
 import com.google.gerrit.server.events.DraftPublishedEvent;
 import com.google.gerrit.server.events.EventFactory;
 import com.google.gerrit.server.events.HashtagsChangedEvent;
-import com.google.gerrit.server.events.MergeFailedEvent;
 import com.google.gerrit.server.events.PatchSetCreatedEvent;
 import com.google.gerrit.server.events.ProjectCreatedEvent;
 import com.google.gerrit.server.events.RefUpdatedEvent;
@@ -169,9 +168,6 @@
     /** Path of the change merged hook. */
     private final Optional<Path> changeMergedHook;
 
-    /** Path of the merge failed hook. */
-    private final Optional<Path> mergeFailedHook;
-
     /** Path of the change abandoned hook. */
     private final Optional<Path> changeAbandonedHook;
 
@@ -268,7 +264,6 @@
         draftPublishedHook = hook(config, hooksPath, "draft-published");
         commentAddedHook = hook(config, hooksPath, "comment-added");
         changeMergedHook = hook(config, hooksPath, "change-merged");
-        mergeFailedHook = hook(config, hooksPath, "merge-failed");
         changeAbandonedHook = hook(config, hooksPath, "change-abandoned");
         changeRestoredHook = hook(config, hooksPath, "change-restored");
         refUpdatedHook = hook(config, hooksPath, "ref-updated");
@@ -529,41 +524,6 @@
     }
 
     @Override
-    public void doMergeFailedHook(Change change, Account account,
-          PatchSet patchSet, String reason,
-          ReviewDb db) throws OrmException {
-      MergeFailedEvent event = new MergeFailedEvent(change);
-      Supplier<AccountState> owner = getAccountSupplier(change.getOwner());
-
-      event.change = changeAttributeSupplier(change);
-      event.submitter = accountAttributeSupplier(account);
-      event.patchSet = patchSetAttributeSupplier(change, patchSet);
-      event.reason = reason;
-
-      dispatcher.get().postEvent(change, event, db);
-
-      if (!mergeFailedHook.isPresent()) {
-        return;
-      }
-
-      List<String> args = new ArrayList<>();
-      ChangeAttribute c = event.change.get();
-      PatchSetAttribute ps = event.patchSet.get();
-
-      addArg(args, "--change", c.id);
-      addArg(args, "--change-url", c.url);
-      addArg(args, "--change-owner", getDisplayName(owner.get().getAccount()));
-      addArg(args, "--project", c.project);
-      addArg(args, "--branch", c.branch);
-      addArg(args, "--topic", c.topic);
-      addArg(args, "--submitter", getDisplayName(account));
-      addArg(args, "--commit", ps.revision);
-      addArg(args, "--reason",  reason == null ? "" : reason);
-
-      runHook(change.getProject(), mergeFailedHook, args);
-    }
-
-    @Override
     public void doChangeAbandonedHook(Change change, Account account,
           PatchSet patchSet, String reason, ReviewDb db)
           throws OrmException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
index 9b02d18..a7e3583 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
@@ -85,19 +85,6 @@
       PatchSet patchSet, ReviewDb db, String mergeResultRev) throws OrmException;
 
   /**
-   * Fire the Merge Failed Hook.
-   *
-   * @param change The change itself.
-   * @param account The gerrit user who attempted to submit the change.
-   * @param patchSet The patchset that failed to merge.
-   * @param reason The reason that the change failed to merge.
-   * @param db The review database.
-   * @throws OrmException
-   */
-  void doMergeFailedHook(Change change, Account account,
-      PatchSet patchSet, String reason, ReviewDb db) throws OrmException;
-
-  /**
    * Fire the Change Abandoned Hook.
    *
    * @param change The change itself.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java b/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
index dbfa979..2b44946 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
@@ -45,11 +45,6 @@
   }
 
   @Override
-  public void doMergeFailedHook(Change change, Account account,
-      PatchSet patchSet, String reason, ReviewDb db) {
-  }
-
-  @Override
   public void doChangeRestoredHook(Change change, Account account,
       PatchSet patchSet, String reason, ReviewDb db) {
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/AnonymousUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/AnonymousUser.java
index ff09df5..e916aff 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/AnonymousUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/AnonymousUser.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server;
 
-import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.GroupMembership;
@@ -22,7 +21,6 @@
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.inject.Inject;
 
-import java.util.Collection;
 import java.util.Collections;
 import java.util.Set;
 
@@ -44,11 +42,6 @@
   }
 
   @Override
-  public Collection<AccountProjectWatch> getNotificationFilters() {
-    return Collections.emptySet();
-  }
-
-  @Override
   public String toString() {
     return "ANONYMOUS";
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
index dc3a6b1..847d559 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.server;
 
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.gerrit.server.notedb.ReviewerStateInternal.CC;
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
 
 import com.google.common.annotations.VisibleForTesting;
@@ -23,13 +21,10 @@
 import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.ImmutableSetMultimap;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.LinkedHashMultimap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Ordering;
-import com.google.common.collect.SetMultimap;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
@@ -45,7 +40,6 @@
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.notedb.NotesMigration;
-import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.util.LabelVote;
 import com.google.gwtorm.server.OrmException;
@@ -115,15 +109,14 @@
    *
    * @param db review database.
    * @param notes change notes.
-   * @return multimap of reviewers keyed by state, where each account appears
-   *     exactly once in {@link SetMultimap#values()}, and
-   *     {@link ReviewerStateInternal#REMOVED} is not present.
+   * @return reviewers for the change.
    * @throws OrmException if reviewers for the change could not be read.
    */
-  public ImmutableSetMultimap<ReviewerStateInternal, Account.Id> getReviewers(
-      ReviewDb db, ChangeNotes notes) throws OrmException {
+  public ReviewerSet getReviewers(ReviewDb db, ChangeNotes notes)
+      throws OrmException {
     if (!migration.readChanges()) {
-      return getReviewers(db.patchSetApprovals().byChange(notes.getChangeId()));
+      return ReviewerSet.fromApprovals(
+          db.patchSetApprovals().byChange(notes.getChangeId()));
     }
     return notes.load().getReviewers();
   }
@@ -133,44 +126,18 @@
    *
    * @param allApprovals all approvals to consider; must all belong to the same
    *     change.
-   * @return multimap of reviewers keyed by state, where each account appears
-   *     exactly once in {@link SetMultimap#values()}, and
-   *     {@link ReviewerStateInternal#REMOVED} is not present.
+   * @return reviewers for the change.
+   * @throws OrmException if reviewers for the change could not be read.
    */
-  public ImmutableSetMultimap<ReviewerStateInternal, Account.Id> getReviewers(
-      ChangeNotes notes, Iterable<PatchSetApproval> allApprovals)
+  public ReviewerSet getReviewers(ChangeNotes notes,
+      Iterable<PatchSetApproval> allApprovals)
       throws OrmException {
     if (!migration.readChanges()) {
-      return getReviewers(allApprovals);
+      return ReviewerSet.fromApprovals(allApprovals);
     }
     return notes.load().getReviewers();
   }
 
-  private static ImmutableSetMultimap<ReviewerStateInternal, Account.Id> getReviewers(
-      Iterable<PatchSetApproval> allApprovals) {
-    PatchSetApproval first = null;
-    SetMultimap<ReviewerStateInternal, Account.Id> reviewers =
-        LinkedHashMultimap.create();
-    for (PatchSetApproval psa : allApprovals) {
-      if (first == null) {
-        first = psa;
-      } else {
-        checkArgument(
-            first.getKey().getParentKey().getParentKey().equals(
-              psa.getKey().getParentKey().getParentKey()),
-            "multiple change IDs: %s, %s", first.getKey(), psa.getKey());
-      }
-      Account.Id id = psa.getAccountId();
-      if (psa.getValue() != 0) {
-        reviewers.put(REVIEWER, id);
-        reviewers.remove(CC, id);
-      } else if (!reviewers.containsEntry(REVIEWER, id)) {
-        reviewers.put(CC, id);
-      }
-    }
-    return ImmutableSetMultimap.copyOf(reviewers);
-  }
-
   public List<PatchSetApproval> addReviewers(ReviewDb db,
       ChangeUpdate update, LabelTypes labelTypes, Change change, PatchSet ps,
       PatchSetInfo info, Iterable<Account.Id> wantReviewers,
@@ -185,7 +152,7 @@
       Iterable<Account.Id> wantReviewers) throws OrmException {
     PatchSet.Id psId = change.currentPatchSetId();
     return addReviewers(db, update, labelTypes, change, psId, false, null, null,
-        wantReviewers, getReviewers(db, notes).values());
+        wantReviewers, getReviewers(db, notes).all());
   }
 
   private List<PatchSetApproval> addReviewers(ReviewDb db, ChangeUpdate update,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
index f1242b9..16e868f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
@@ -14,14 +14,13 @@
 
 package com.google.gerrit.server;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.inject.servlet.RequestScoped;
 
-import java.util.Collection;
 import java.util.Set;
 
 /**
@@ -33,6 +32,16 @@
  * @see IdentifiedUser
  */
 public abstract class CurrentUser {
+  /** Unique key for plugin/extension specific data on a CurrentUser. */
+  public static final class PropertyKey<T> {
+    public static <T> PropertyKey<T> create() {
+      return new PropertyKey<>();
+    }
+
+    private PropertyKey() {
+    }
+  }
+
   private final CapabilityControl.Factory capabilityControlFactory;
   private AccessPath accessPath = AccessPath.UNKNOWN;
 
@@ -82,9 +91,6 @@
   @Deprecated
   public abstract Set<Change.Id> getStarredChanges();
 
-  /** Filters selecting changes the user wants to monitor. */
-  public abstract Collection<AccountProjectWatch> getNotificationFilters();
-
   /** Unique name of the user on this server, if one has been assigned. */
   public String getUserName() {
     return null;
@@ -119,4 +125,24 @@
   public boolean isInternalUser() {
     return false;
   }
+
+  /**
+   * Lookup a previously stored property.
+   *
+   * @param key unique property key.
+   * @return previously stored value, or {@code null}.
+   */
+  @Nullable
+  public <T> T get(PropertyKey<T> key) {
+    return null;
+  }
+
+  /**
+   * Store a property for later retrieval.
+   *
+   * @param key unique property key.
+   * @param value value to store; or {@code null} to clear the value.
+   */
+  public <T> void put(PropertyKey<T> key, @Nullable T value) {
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
index f6fae8c..c7f4c4a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
@@ -19,9 +19,7 @@
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.CapabilityControl;
@@ -34,28 +32,23 @@
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.DisableReverseDnsLookup;
 import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
-import com.google.inject.OutOfScopeException;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import com.google.inject.util.Providers;
 
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.util.SystemReader;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.MalformedURLException;
 import java.net.SocketAddress;
 import java.net.URL;
-import java.util.Collection;
-import java.util.Collections;
 import java.util.Date;
-import java.util.List;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.Set;
 import java.util.TimeZone;
 
@@ -96,29 +89,20 @@
       this.disableReverseDnsLookup = disableReverseDnsLookup;
     }
 
-    public IdentifiedUser create(final Account.Id id) {
+    public IdentifiedUser create(Account.Id id) {
       return create((SocketAddress) null, id);
     }
 
-    public IdentifiedUser create(Provider<ReviewDb> db, Account.Id id) {
-      return new IdentifiedUser(capabilityControlFactory, starredChangesUtil,
-          authConfig, realm, anonymousCowardName, canonicalUrl, accountCache,
-          groupBackend, disableReverseDnsLookup, null, db, id, null);
-    }
-
     public IdentifiedUser create(SocketAddress remotePeer, Account.Id id) {
-      return new IdentifiedUser(capabilityControlFactory, starredChangesUtil,
-          authConfig, realm, anonymousCowardName, canonicalUrl, accountCache,
-          groupBackend, disableReverseDnsLookup, Providers.of(remotePeer), null,
-          id, null);
+      return runAs(remotePeer, id, null);
     }
 
-    public CurrentUser runAs(SocketAddress remotePeer, Account.Id id,
+    public IdentifiedUser runAs(SocketAddress remotePeer, Account.Id id,
         @Nullable CurrentUser caller) {
       return new IdentifiedUser(capabilityControlFactory, starredChangesUtil,
           authConfig, realm, anonymousCowardName, canonicalUrl, accountCache,
-          groupBackend, disableReverseDnsLookup, Providers.of(remotePeer), null,
-          id, caller);
+          groupBackend, disableReverseDnsLookup, Providers.of(remotePeer), id,
+          caller);
     }
   }
 
@@ -139,23 +123,20 @@
     private final AccountCache accountCache;
     private final GroupBackend groupBackend;
     private final Boolean disableReverseDnsLookup;
-
     private final Provider<SocketAddress> remotePeerProvider;
-    private final Provider<ReviewDb> dbProvider;
 
     @Inject
     RequestFactory(
         CapabilityControl.Factory capabilityControlFactory,
         @Nullable StarredChangesUtil starredChangesUtil,
-        final AuthConfig authConfig,
+        AuthConfig authConfig,
         Realm realm,
-        @AnonymousCowardName final String anonymousCowardName,
-        @CanonicalWebUrl final Provider<String> canonicalUrl,
-        final AccountCache accountCache,
-        final GroupBackend groupBackend,
-        @DisableReverseDnsLookup final Boolean disableReverseDnsLookup,
-        @RemotePeer final Provider<SocketAddress> remotePeerProvider,
-        final Provider<ReviewDb> dbProvider) {
+        @AnonymousCowardName String anonymousCowardName,
+        @CanonicalWebUrl Provider<String> canonicalUrl,
+        AccountCache accountCache,
+        GroupBackend groupBackend,
+        @DisableReverseDnsLookup Boolean disableReverseDnsLookup,
+        @RemotePeer Provider<SocketAddress> remotePeerProvider) {
       this.capabilityControlFactory = capabilityControlFactory;
       this.starredChangesUtil = starredChangesUtil;
       this.authConfig = authConfig;
@@ -166,27 +147,22 @@
       this.groupBackend = groupBackend;
       this.disableReverseDnsLookup = disableReverseDnsLookup;
       this.remotePeerProvider = remotePeerProvider;
-      this.dbProvider = dbProvider;
     }
 
     public IdentifiedUser create(Account.Id id) {
       return new IdentifiedUser(capabilityControlFactory, starredChangesUtil,
           authConfig, realm, anonymousCowardName, canonicalUrl, accountCache,
-          groupBackend, disableReverseDnsLookup, remotePeerProvider, dbProvider,
-          id, null);
+          groupBackend, disableReverseDnsLookup, remotePeerProvider, id, null);
     }
 
     public IdentifiedUser runAs(Account.Id id, CurrentUser caller) {
       return new IdentifiedUser(capabilityControlFactory, starredChangesUtil,
           authConfig, realm, anonymousCowardName, canonicalUrl, accountCache,
-          groupBackend, disableReverseDnsLookup, remotePeerProvider, dbProvider,
-          id, caller);
+          groupBackend, disableReverseDnsLookup, remotePeerProvider, id,
+          caller);
     }
   }
 
-  private static final Logger log =
-      LoggerFactory.getLogger(IdentifiedUser.class);
-
   private static final GroupMembership registeredGroups =
       new ListGroupMembership(ImmutableSet.of(
           SystemGroupBackend.ANONYMOUS_USERS,
@@ -205,12 +181,7 @@
   private final Set<String> validEmails =
       Sets.newTreeSet(String.CASE_INSENSITIVE_ORDER);
 
-  @Nullable
   private final Provider<SocketAddress> remotePeerProvider;
-
-  @Nullable
-  private final Provider<ReviewDb> dbProvider;
-
   private final Account.Id accountId;
 
   private AccountState state;
@@ -219,8 +190,8 @@
   private GroupMembership effectiveGroups;
   private Set<Change.Id> starredChanges;
   private ResultSet<Change.Id> starredQuery;
-  private Collection<AccountProjectWatch> notificationFilters;
   private CurrentUser realUser;
+  private Map<PropertyKey<Object>, Object> properties;
 
   private IdentifiedUser(
       CapabilityControl.Factory capabilityControlFactory,
@@ -233,7 +204,6 @@
       final GroupBackend groupBackend,
       final Boolean disableReverseDnsLookup,
       @Nullable final Provider<SocketAddress> remotePeerProvider,
-      @Nullable final Provider<ReviewDb> dbProvider,
       final Account.Id id,
       @Nullable CurrentUser realUser) {
     super(capabilityControlFactory);
@@ -246,7 +216,6 @@
     this.anonymousCowardName = anonymousCowardName;
     this.disableReverseDnsLookup = disableReverseDnsLookup;
     this.remotePeerProvider = remotePeerProvider;
-    this.dbProvider = dbProvider;
     this.accountId = id;
     this.realUser = realUser != null ? realUser : this;
   }
@@ -256,7 +225,6 @@
     return realUser;
   }
 
-  // TODO(cranger): maybe get the state through the accountCache instead.
   public AccountState state() {
     if (state == null) {
       state = accountCache.get(getAccountId());
@@ -374,29 +342,6 @@
     }
   }
 
-  private void checkRequestScope() {
-    if (dbProvider == null) {
-      throw new OutOfScopeException("Not in request scoped user");
-    }
-  }
-
-  @Override
-  public Collection<AccountProjectWatch> getNotificationFilters() {
-    if (notificationFilters == null) {
-      checkRequestScope();
-      List<AccountProjectWatch> r;
-      try {
-        r = dbProvider.get().accountProjectWatches() //
-            .byAccount(getAccountId()).toList();
-      } catch (OrmException e) {
-        log.warn("Cannot query notification filters of a user", e);
-        r = Collections.emptyList();
-      }
-      notificationFilters = Collections.unmodifiableList(r);
-    }
-    return notificationFilters;
-  }
-
   public PersonIdent newRefLogIdent() {
     return newRefLogIdent(new Date(), TimeZone.getDefault());
   }
@@ -419,14 +364,11 @@
     user = user + "|" + "account-" + ua.getId().toString();
 
     String host = null;
-    if (remotePeerProvider != null) {
-      final SocketAddress remotePeer = remotePeerProvider.get();
-      if (remotePeer instanceof InetSocketAddress) {
-        final InetSocketAddress sa = (InetSocketAddress) remotePeer;
-        final InetAddress in = sa.getAddress();
-
-        host = in != null ? getHost(in) : sa.getHostName();
-      }
+    SocketAddress remotePeer = remotePeerProvider.get();
+    if (remotePeer instanceof InetSocketAddress) {
+      InetSocketAddress sa = (InetSocketAddress) remotePeer;
+      InetAddress in = sa.getAddress();
+      host = in != null ? getHost(in) : sa.getHostName();
     }
     if (host == null || host.isEmpty()) {
       host = "unknown";
@@ -487,6 +429,41 @@
     return true;
   }
 
+  @Override
+  @Nullable
+  public synchronized <T> T get(PropertyKey<T> key) {
+    if (properties != null) {
+      @SuppressWarnings("unchecked")
+      T value = (T) properties.get(key);
+      return value;
+    }
+    return null;
+  }
+
+  /**
+   * Store a property for later retrieval.
+   *
+   * @param key unique property key.
+   * @param value value to store; or {@code null} to clear the value.
+   */
+  @Override
+  public synchronized <T> void put(PropertyKey<T> key, @Nullable T value) {
+    if (properties == null) {
+      if (value == null) {
+        return;
+      }
+      properties = new HashMap<>();
+    }
+
+    @SuppressWarnings("unchecked")
+    PropertyKey<Object> k = (PropertyKey<Object>) key;
+    if (value != null) {
+      properties.put(k, value);
+    } else {
+      properties.remove(k);
+    }
+  }
+
   private String getHost(final InetAddress in) {
     if (Boolean.FALSE.equals(disableReverseDnsLookup)) {
       return in.getCanonicalHostName();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
index d0c2dc0..3c63bf8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
@@ -15,13 +15,11 @@
 package com.google.gerrit.server;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.inject.Inject;
 
-import java.util.Collection;
 import java.util.Collections;
 import java.util.Set;
 
@@ -57,11 +55,6 @@
   }
 
   @Override
-  public Collection<AccountProjectWatch> getNotificationFilters() {
-    return Collections.emptySet();
-  }
-
-  @Override
   public boolean isInternalUser() {
     return true;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java
index 4d26f02..ea3080d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server;
 
-import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.GroupMembership;
@@ -22,7 +21,6 @@
 import com.google.inject.assistedinject.Assisted;
 
 import java.net.SocketAddress;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.Set;
 
@@ -54,11 +52,6 @@
     return Collections.emptySet();
   }
 
-  @Override
-  public Collection<AccountProjectWatch> getNotificationFilters() {
-    return Collections.emptySet();
-  }
-
   public SocketAddress getRemoteAddress() {
     return peer;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerSet.java
new file mode 100644
index 0000000..515cef7
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerSet.java
@@ -0,0 +1,115 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.gerrit.server.notedb.ReviewerStateInternal.CC;
+import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
+
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableTable;
+import com.google.common.collect.Table;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.server.notedb.ReviewerStateInternal;
+
+import java.sql.Timestamp;
+
+/**
+ * Set of reviewers on a change.
+ * <p>
+ * A given account may appear in multiple states and at different timestamps. No
+ * reviewers with state {@link ReviewerStateInternal#REMOVED} are ever exposed
+ * by this interface.
+ */
+public class ReviewerSet {
+  private static final ReviewerSet EMPTY = new ReviewerSet(
+      ImmutableTable.<ReviewerStateInternal, Account.Id, Timestamp>of());
+
+  public static ReviewerSet fromApprovals(
+      Iterable<PatchSetApproval> approvals) {
+    PatchSetApproval first = null;
+    Table<ReviewerStateInternal, Account.Id, Timestamp> reviewers =
+        HashBasedTable.create();
+    for (PatchSetApproval psa : approvals) {
+      if (first == null) {
+        first = psa;
+      } else {
+        checkArgument(
+            first.getKey().getParentKey().getParentKey().equals(
+              psa.getKey().getParentKey().getParentKey()),
+            "multiple change IDs: %s, %s", first.getKey(), psa.getKey());
+      }
+      Account.Id id = psa.getAccountId();
+      if (psa.getValue() != 0) {
+        reviewers.put(REVIEWER, id, psa.getGranted());
+        reviewers.remove(CC, id);
+      } else if (!reviewers.contains(REVIEWER, id)) {
+        reviewers.put(CC, id, psa.getGranted());
+      }
+    }
+    return new ReviewerSet(reviewers);
+  }
+
+  public static ReviewerSet fromTable(
+      Table<ReviewerStateInternal, Account.Id, Timestamp> table) {
+    return new ReviewerSet(table);
+  }
+
+  public static ReviewerSet empty() {
+    return EMPTY;
+  }
+
+  private final ImmutableTable<ReviewerStateInternal, Account.Id, Timestamp>
+      table;
+  private ImmutableSet<Account.Id> accounts;
+
+  private ReviewerSet(Table<ReviewerStateInternal, Account.Id, Timestamp> table) {
+    this.table = ImmutableTable.copyOf(table);
+  }
+
+  public ImmutableSet<Account.Id> all() {
+    if (accounts == null) {
+      // Idempotent and immutable, don't bother locking.
+      accounts = ImmutableSet.copyOf(table.columnKeySet());
+    }
+    return accounts;
+  }
+
+  public ImmutableSet<Account.Id> byState(ReviewerStateInternal state) {
+    return table.row(state).keySet();
+  }
+
+  public ImmutableTable<ReviewerStateInternal, Account.Id, Timestamp>
+      asTable() {
+    return table;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    return (o instanceof ReviewerSet) && table.equals(((ReviewerSet) o).table);
+  }
+
+  @Override
+  public int hashCode() {
+    return table.hashCode();
+  }
+
+  @Override
+  public String toString() {
+    return getClass().getSimpleName() + table;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java
index 815b519..37e36cd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java
@@ -16,9 +16,14 @@
 
 import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME;
 
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.CurrentUser.PropertyKey;
+import com.google.gerrit.server.IdentifiedUser;
 
 import java.util.Collection;
 import java.util.Set;
@@ -27,6 +32,7 @@
   private final Account account;
   private final Set<AccountGroup.UUID> internalGroups;
   private final Collection<AccountExternalId> externalIds;
+  private Cache<IdentifiedUser.PropertyKey<Object>, Object> properties;
 
   public AccountState(final Account account,
       final Set<AccountGroup.UUID> actualGroups,
@@ -81,4 +87,59 @@
     }
     return null;
   }
+
+  /**
+   * Lookup a previously stored property.
+   * <p>
+   * All properties are automatically cleared when the account cache invalidates
+   * the {@code AccountState}. This method is thread-safe.
+   *
+   * @param key unique property key.
+   * @return previously stored value, or {@code null}.
+   */
+  @Nullable
+  public <T> T get(PropertyKey<T> key) {
+    Cache<PropertyKey<Object>, Object> p = properties(false);
+    if (p != null) {
+      @SuppressWarnings("unchecked")
+      T value = (T) p.getIfPresent(key);
+      return value;
+    }
+    return null;
+  }
+
+  /**
+   * Store a property for later retrieval.
+   * <p>
+   * This method is thread-safe.
+   *
+   * @param key unique property key.
+   * @param value value to store; or {@code null} to clear the value.
+   */
+  public <T> void put(PropertyKey<T> key, @Nullable T value) {
+    Cache<PropertyKey<Object>, Object> p = properties(value != null);
+    if (p != null || value != null) {
+      @SuppressWarnings("unchecked")
+      PropertyKey<Object> k = (PropertyKey<Object>) key;
+      if (value != null) {
+        p.put(k, value);
+      } else {
+        p.invalidate(k);
+      }
+    }
+  }
+
+  private synchronized Cache<PropertyKey<Object>, Object> properties(
+      boolean allocate) {
+    if (properties == null && allocate) {
+      properties = CacheBuilder.newBuilder()
+          .concurrencyLevel(1)
+          .initialCapacity(16)
+          // Use weakKeys to ensure plugins that garbage collect will also
+          // eventually release data held in any still live AccountState.
+          .weakKeys()
+          .build();
+    }
+    return properties;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteWatchedProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteWatchedProjects.java
index 674cba6..b91378e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteWatchedProjects.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteWatchedProjects.java
@@ -64,9 +64,10 @@
     if (input != null) {
       List<AccountProjectWatch.Key> keysToDelete = new LinkedList<>();
       for (String projectKeyToDelete : input) {
-        if (!watchedProjectsMap.containsKey(projectKeyToDelete))
+        if (!watchedProjectsMap.containsKey(projectKeyToDelete)) {
           throw new UnprocessableEntityException(projectKeyToDelete
               + " is not currently watched by this user.");
+        }
         keysToDelete.add(watchedProjectsMap.get(projectKeyToDelete).getKey());
       }
       dbProvider.get().accountProjectWatches().deleteKeys(keysToDelete);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDiffPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDiffPreferences.java
index be87ae7..2c4a840 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDiffPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDiffPreferences.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.account;
 
 import static com.google.gerrit.server.config.ConfigUtil.loadSection;
+import static com.google.gerrit.server.config.ConfigUtil.skipField;
 
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -31,11 +32,17 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Repository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.lang.reflect.Field;
 
 @Singleton
 public class GetDiffPreferences implements RestReadView<AccountResource> {
+  private static final Logger log =
+      LoggerFactory.getLogger(GetDiffPreferences.class);
+
   private final Provider<CurrentUser> self;
   private final Provider<AllUsersName> allUsersName;
   private final GitRepositoryManager gitMgr;
@@ -66,13 +73,41 @@
       DiffPreferencesInfo in)
       throws IOException, ConfigInvalidException, RepositoryNotFoundException {
     try (Repository git = gitMgr.openRepository(allUsersName)) {
+      // Load all users prefs.
+      VersionedAccountPreferences dp =
+          VersionedAccountPreferences.forDefault();
+      dp.load(git);
+      DiffPreferencesInfo allUserPrefs = new DiffPreferencesInfo();
+      loadSection(dp.getConfig(), UserConfigSections.DIFF, null, allUserPrefs,
+          DiffPreferencesInfo.defaults(), in);
+
+      // Load user prefs
       VersionedAccountPreferences p =
           VersionedAccountPreferences.forUser(id);
       p.load(git);
       DiffPreferencesInfo prefs = new DiffPreferencesInfo();
       loadSection(p.getConfig(), UserConfigSections.DIFF, null, prefs,
-          DiffPreferencesInfo.defaults(), in);
+          updateDefaults(allUserPrefs), in);
       return prefs;
     }
   }
+
+  private static DiffPreferencesInfo updateDefaults(DiffPreferencesInfo input) {
+    DiffPreferencesInfo result = DiffPreferencesInfo.defaults();
+    try {
+      for (Field field : input.getClass().getDeclaredFields()) {
+        if (skipField(field)) {
+          continue;
+        }
+        Object newVal = field.get(input);
+        if (newVal != null) {
+          field.set(result, newVal);
+        }
+      }
+    } catch (IllegalAccessException e) {
+      log.warn("Cannot get default diff preferences from All-Users", e);
+      return DiffPreferencesInfo.defaults();
+    }
+    return result;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalAccountDirectory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalAccountDirectory.java
index 4f8eacd..961d554 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalAccountDirectory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalAccountDirectory.java
@@ -12,12 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-
 package com.google.gerrit.server.account;
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ArrayListMultimap;
-import com.google.common.collect.Lists;
 import com.google.common.collect.Multimap;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.AvatarInfo;
@@ -32,6 +30,7 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.Set;
@@ -123,17 +122,36 @@
     if (options.contains(FillOptions.AVATARS)) {
       AvatarProvider ap = avatar.get();
       if (ap != null) {
-        info.avatars = Lists.newArrayListWithCapacity(1);
-        String u = ap.getUrl(
-            userFactory.create(account.getId()),
-            AvatarInfo.DEFAULT_SIZE);
-        if (u != null) {
-          AvatarInfo a = new AvatarInfo();
-          a.url = u;
-          a.height = AvatarInfo.DEFAULT_SIZE;
-          info.avatars.add(a);
+        info.avatars = new ArrayList<>(3);
+        IdentifiedUser user = userFactory.create(account.getId());
+
+        // GWT UI uses DEFAULT_SIZE (26px).
+        addAvatar(ap, info, user, AvatarInfo.DEFAULT_SIZE);
+
+        // PolyGerrit UI prefers 32px and 100px.
+        if (!info.avatars.isEmpty()) {
+          if (32 != AvatarInfo.DEFAULT_SIZE) {
+            addAvatar(ap, info, user, 32);
+          }
+          if (100 != AvatarInfo.DEFAULT_SIZE) {
+            addAvatar(ap, info, user, 100);
+          }
         }
       }
     }
   }
+
+  private static void addAvatar(
+      AvatarProvider provider,
+      AccountInfo account,
+      IdentifiedUser user,
+      int size) {
+    String url = provider.getUrl(user, size);
+    if (url != null) {
+      AvatarInfo avatar = new AvatarInfo();
+      avatar.url = url;
+      avatar.height = size;
+      account.avatars.add(avatar);
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
index 4662f87..dc96d49 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
@@ -165,7 +165,7 @@
     }
   }
 
-  public static interface Factory {
+  public interface Factory {
     VersionedAuthorizedKeys create(Account.Id accountId);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
index b8bd905..e8baefe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.server.account.CapabilityUtils.checkRequiresCapability;
 
 import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
+import com.google.gerrit.extensions.api.access.ProjectAccessInput;
 import com.google.gerrit.extensions.api.projects.BranchApi;
 import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.api.projects.ChildProjectApi;
@@ -44,6 +45,7 @@
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gerrit.server.project.ProjectsCollection;
 import com.google.gerrit.server.project.PutDescription;
+import com.google.gerrit.server.project.SetAccess;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 
@@ -74,6 +76,7 @@
   private final GetAccess getAccess;
   private final ListBranches listBranches;
   private final ListTags listTags;
+  private final SetAccess setAccess;
 
   @AssistedInject
   ProjectApiImpl(CurrentUser user,
@@ -88,12 +91,13 @@
       BranchApiImpl.Factory branchApiFactory,
       TagApiImpl.Factory tagApiFactory,
       GetAccess getAccess,
+      SetAccess setAccess,
       ListBranches listBranches,
       ListTags listTags,
       @Assisted ProjectResource project) {
     this(user, createProjectFactory, projectApi, projects, getDescription,
         putDescription, childApi, children, projectJson, branchApiFactory,
-        tagApiFactory, getAccess, listBranches, listTags,
+        tagApiFactory, getAccess, setAccess, listBranches, listTags,
         project, null);
   }
 
@@ -110,12 +114,13 @@
       BranchApiImpl.Factory branchApiFactory,
       TagApiImpl.Factory tagApiFactory,
       GetAccess getAccess,
+      SetAccess setAccess,
       ListBranches listBranches,
       ListTags listTags,
       @Assisted String name) {
     this(user, createProjectFactory, projectApi, projects, getDescription,
         putDescription, childApi, children, projectJson, branchApiFactory,
-        tagApiFactory, getAccess, listBranches, listTags,
+        tagApiFactory, getAccess, setAccess, listBranches, listTags,
         null, name);
   }
 
@@ -131,6 +136,7 @@
       BranchApiImpl.Factory branchApiFactory,
       TagApiImpl.Factory tagApiFactory,
       GetAccess getAccess,
+      SetAccess setAccess,
       ListBranches listBranches,
       ListTags listTags,
       ProjectResource project,
@@ -149,6 +155,7 @@
     this.branchApi = branchApiFactory;
     this.tagApi = tagApiFactory;
     this.getAccess = getAccess;
+    this.setAccess = setAccess;
     this.listBranches = listBranches;
     this.listTags = listTags;
   }
@@ -199,6 +206,16 @@
   }
 
   @Override
+  public ProjectAccessInfo access(ProjectAccessInput p)
+      throws RestApiException {
+    try {
+      return setAccess.apply(checkExists(), p);
+    } catch (IOException e) {
+      throw new RestApiException("Cannot put access rights", e);
+    }
+  }
+
+  @Override
   public void description(PutDescriptionInput in)
       throws RestApiException {
     try {
@@ -260,7 +277,8 @@
   }
 
   @Override
-  public List<ProjectInfo> children(boolean recursive) throws RestApiException {
+  public List<ProjectInfo> children(boolean recursive)
+      throws RestApiException {
     ListChildProjects list = children.list();
     list.setRecursive(recursive);
     return list.apply(checkExists());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/oauth/OAuthRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/oauth/OAuthRealm.java
index 6e39411..cf9000d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/oauth/OAuthRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/oauth/OAuthRealm.java
@@ -103,10 +103,14 @@
     if (userInfo == null) {
       throw new AccountException("Cannot authenticate");
     }
-    if (!Strings.isNullOrEmpty(userInfo.getEmailAddress())) {
+    if (!Strings.isNullOrEmpty(userInfo.getEmailAddress())
+        && (Strings.isNullOrEmpty(who.getUserName())
+            || !allowsEdit(FieldName.REGISTER_NEW_EMAIL))) {
       who.setEmailAddress(userInfo.getEmailAddress());
     }
-    if (!Strings.isNullOrEmpty(userInfo.getDisplayName())) {
+    if (!Strings.isNullOrEmpty(userInfo.getDisplayName())
+        && (Strings.isNullOrEmpty(who.getDisplayName())
+            || !allowsEdit(FieldName.FULL_NAME))) {
       who.setDisplayName(userInfo.getDisplayName());
     }
     return who;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
index 88b6960..0c2366f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
@@ -450,10 +450,10 @@
       out.removableReviewers = removableReviewers(ctl, out.labels.values());
 
       out.reviewers = new HashMap<>();
-      for (Map.Entry<ReviewerStateInternal, Collection<Account.Id>> e
-          : cd.reviewers().asMap().entrySet()) {
+      for (Map.Entry<ReviewerStateInternal, Map<Account.Id, Timestamp>> e
+          : cd.reviewers().asTable().rowMap().entrySet()) {
         out.reviewers.put(e.getKey().asReviewerState(),
-            toAccountInfo(e.getValue()));
+            toAccountInfo(e.getValue().keySet()));
       }
     }
 
@@ -625,7 +625,7 @@
     //  - They are an explicit reviewer.
     //  - They ever voted on this change.
     Set<Account.Id> allUsers = new HashSet<>();
-    allUsers.addAll(cd.reviewers().values());
+    allUsers.addAll(cd.reviewers().all());
     for (PatchSetApproval psa : cd.approvals().values()) {
       allUsers.add(psa.getAccountId());
     }
@@ -975,7 +975,7 @@
       if (in.getPushCertificate() != null) {
         out.pushCertificate = gpgApi.checkPushCertificate(
             in.getPushCertificate(),
-            userFactory.create(db, in.getUploader()));
+            userFactory.create(in.getUploader()));
       } else {
         out.pushCertificate = new PushCertificateInfo();
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListReviewers.java
index 1e3480f..9a0c691 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListReviewers.java
@@ -51,7 +51,7 @@
     Map<Account.Id, ReviewerResource> reviewers = new LinkedHashMap<>();
     ReviewDb db = dbProvider.get();
     for (Account.Id accountId
-        : approvalsUtil.getReviewers(db, rsrc.getNotes()).values()) {
+        : approvalsUtil.getReviewers(db, rsrc.getNotes()).all()) {
       if (!reviewers.containsKey(accountId)) {
         reviewers.put(accountId, resourceFactory.create(rsrc, accountId));
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
index 3defdd7..ffbfc36 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -19,10 +19,8 @@
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.CC;
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
 
-import com.google.common.collect.SetMultimap;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-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.PatchSet;
@@ -33,6 +31,7 @@
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.events.CommitReceivedEvent;
 import com.google.gerrit.server.git.BanCommit;
 import com.google.gerrit.server.git.BatchUpdate;
@@ -43,7 +42,6 @@
 import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.mail.ReplacePatchSetSender;
 import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.RefControl;
@@ -106,7 +104,7 @@
   private PatchSet patchSet;
   private PatchSetInfo patchSetInfo;
   private ChangeMessage changeMessage;
-  private SetMultimap<ReviewerStateInternal, Account.Id> oldReviewers;
+  private ReviewerSet oldReviewers;
 
   @AssistedInject
   public PatchSetInserter(ChangeHooks hooks,
@@ -264,8 +262,8 @@
         cm.setFrom(ctx.getUser().getAccountId());
         cm.setPatchSet(patchSet, patchSetInfo);
         cm.setChangeMessage(changeMessage);
-        cm.addReviewers(oldReviewers.get(REVIEWER));
-        cm.addExtraCC(oldReviewers.get(CC));
+        cm.addReviewers(oldReviewers.byState(REVIEWER));
+        cm.addExtraCC(oldReviewers.byState(CC));
         cm.send();
       } catch (Exception err) {
         log.error("Cannot send email for new patch set on change "
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
index ff5185e..4304669 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
@@ -207,7 +207,7 @@
         throws OrmException, IOException {
       LabelTypes labelTypes = ctx.getControl().getLabelTypes();
       Collection<Account.Id> oldReviewers = approvalsUtil.getReviewers(
-          ctx.getDb(), ctx.getNotes()).values();
+          ctx.getDb(), ctx.getNotes()).all();
       RevCommit commit = ctx.getRevWalk().parseCommit(
           ObjectId.fromString(patchSet.getRevision().get()));
       patchSetInfo = patchSetInfoFactory.get(ctx.getRevWalk(), commit, psId);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
index 5f48daa..86e068c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
@@ -24,12 +24,14 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
@@ -66,6 +68,8 @@
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.text.MessageFormat;
+import java.util.HashSet;
+import java.util.Set;
 
 @Singleton
 public class Revert implements RestModifyView<ChangeResource, RevertInput>,
@@ -82,6 +86,7 @@
   private final RevertedSender.Factory revertedSenderFactory;
   private final ChangeJson.Factory json;
   private final PersonIdent serverIdent;
+  private final ApprovalsUtil approvalsUtil;
 
   @Inject
   Revert(Provider<ReviewDb> db,
@@ -93,7 +98,8 @@
       PatchSetUtil psUtil,
       RevertedSender.Factory revertedSenderFactory,
       ChangeJson.Factory json,
-      @GerritPersonIdent PersonIdent serverIdent) {
+      @GerritPersonIdent PersonIdent serverIdent,
+      ApprovalsUtil approvalsUtil) {
     this.db = db;
     this.repoManager = repoManager;
     this.changeInserterFactory = changeInserterFactory;
@@ -104,6 +110,7 @@
     this.revertedSenderFactory = revertedSenderFactory;
     this.json = json;
     this.serverIdent = serverIdent;
+    this.approvalsUtil = approvalsUtil;
   }
 
   @Override
@@ -182,6 +189,13 @@
             .setTopic(changeToRevert.getTopic());
         ins.setMessage("Uploaded patch set 1.");
 
+        Set<Account.Id> reviewers = new HashSet<>();
+        reviewers.add(changeToRevert.getOwner());
+        reviewers.addAll(
+            approvalsUtil.getReviewers(db.get(), ctl.getNotes()).all());
+        reviewers.remove(user.getAccountId());
+        ins.setReviewers(reviewers);
+
         try (BatchUpdate bu = updateFactory.create(
             db.get(), project, user, now)) {
           bu.setRepository(git, revWalk, oi);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewers.java
index 74a8866..d45d260 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewers.java
@@ -83,6 +83,6 @@
   private Collection<Account.Id> fetchAccountIds(ChangeResource rsrc)
       throws OrmException {
     return approvalsUtil.getReviewers(
-        dbProvider.get(), rsrc.getNotes()).values();
+        dbProvider.get(), rsrc.getNotes()).all();
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetHashtagsOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetHashtagsOp.java
index 5846400..4e6bd2d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetHashtagsOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetHashtagsOp.java
@@ -35,6 +35,7 @@
 import com.google.gerrit.server.git.BatchUpdate.Context;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.validators.HashtagValidationListener;
 import com.google.gerrit.server.validators.ValidationException;
 import com.google.gwtorm.server.OrmException;
@@ -51,6 +52,7 @@
     SetHashtagsOp create(HashtagsInput input);
   }
 
+  private final NotesMigration notesMigration;
   private final ChangeMessagesUtil cmUtil;
   private final ChangeHooks hooks;
   private final DynamicSet<HashtagValidationListener> validationListeners;
@@ -65,10 +67,12 @@
 
   @AssistedInject
   SetHashtagsOp(
+      NotesMigration notesMigration,
       ChangeMessagesUtil cmUtil,
       ChangeHooks hooks,
       DynamicSet<HashtagValidationListener> validationListeners,
       @Assisted @Nullable HashtagsInput input) {
+    this.notesMigration = notesMigration;
     this.cmUtil = cmUtil;
     this.hooks = hooks;
     this.validationListeners = validationListeners;
@@ -83,6 +87,9 @@
   @Override
   public boolean updateChange(ChangeContext ctx)
       throws AuthException, BadRequestException, OrmException, IOException {
+    if (!notesMigration.readChanges()) {
+      throw new BadRequestException("Cannot add hashtags; NoteDb is disabled");
+    }
     if (input == null
         || (input.add == null && input.remove == null)) {
       updatedHashtags = ImmutableSortedSet.of();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java
index 8cbdf14..40df57c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java
@@ -64,8 +64,7 @@
       return new VisibilityControl() {
         @Override
         public boolean isVisibleTo(Account.Id account) throws OrmException {
-          IdentifiedUser who =
-              identifiedUserFactory.create(dbProvider, account);
+          IdentifiedUser who = identifiedUserFactory.create(account);
           // we can't use changeControl directly as it won't suggest reviewers
           // to drafts
           return rsrc.getControl().forUser(who).isRefVisible();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
index e124e48..eaeb850 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
@@ -382,16 +382,16 @@
     return s;
   }
 
+  public static boolean skipField(Field field) {
+    int modifiers = field.getModifiers();
+    return Modifier.isFinal(modifiers) || Modifier.isTransient(modifiers);
+  }
+
   private static boolean isCollectionOrMap(Class<?> t) {
     return Collection.class.isAssignableFrom(t)
         || Map.class.isAssignableFrom(t);
   }
 
-  private static boolean skipField(Field field) {
-    int modifiers = field.getModifiers();
-    return Modifier.isFinal(modifiers) || Modifier.isTransient(modifiers);
-  }
-
   private static boolean isString(Class<?> t) {
     return String.class == t;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 81fca18..5bbfd3d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -115,7 +115,6 @@
 import com.google.gerrit.server.mail.EmailModule;
 import com.google.gerrit.server.mail.FromAddressGenerator;
 import com.google.gerrit.server.mail.FromAddressGeneratorProvider;
-import com.google.gerrit.server.mail.MergeFailSender;
 import com.google.gerrit.server.mail.MergedSender;
 import com.google.gerrit.server.mail.RegisterNewEmailSender;
 import com.google.gerrit.server.mail.ReplacePatchSetSender;
@@ -143,7 +142,6 @@
 import com.google.gerrit.server.ssh.SshAddressesModule;
 import com.google.gerrit.server.tools.ToolsCatalog;
 import com.google.gerrit.server.util.IdGenerator;
-import com.google.gerrit.server.util.SubmoduleSectionParser;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.server.validators.GroupCreationValidationListener;
 import com.google.gerrit.server.validators.HashtagValidationListener;
@@ -221,7 +219,6 @@
     factory(GroupMembers.Factory.class);
     factory(EmailMerge.Factory.class);
     factory(MergedSender.Factory.class);
-    factory(MergeFailSender.Factory.class);
     factory(MergeUtil.Factory.class);
     factory(PatchScriptFactory.Factory.class);
     factory(PluginUser.Factory.class);
@@ -334,7 +331,6 @@
     factory(MergeValidators.Factory.class);
     factory(ProjectConfigValidator.Factory.class);
     factory(NotesBranchUtil.Factory.class);
-    factory(SubmoduleSectionParser.Factory.class);
     factory(ReplaceOp.Factory.class);
     factory(GitModules.Factory.class);
     factory(VersionedAuthorizedKeys.Factory.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetDiffPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetDiffPreferences.java
new file mode 100644
index 0000000..f0bab6e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetDiffPreferences.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.gerrit.server.config;
+
+import static com.google.gerrit.server.config.ConfigUtil.loadSection;
+
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.account.VersionedAccountPreferences;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.UserConfigSections;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+
+@Singleton
+public class GetDiffPreferences implements RestReadView<ConfigResource> {
+
+  private final AllUsersName allUsersName;
+  private final GitRepositoryManager gitManager;
+
+  @Inject
+  GetDiffPreferences(GitRepositoryManager gitManager,
+      AllUsersName allUsersName) {
+    this.allUsersName = allUsersName;
+    this.gitManager = gitManager;
+  }
+
+  @Override
+  public DiffPreferencesInfo apply(ConfigResource configResource)
+      throws BadRequestException, ResourceConflictException, Exception {
+    return readFromGit(gitManager, allUsersName, null);
+  }
+
+  static DiffPreferencesInfo readFromGit(GitRepositoryManager gitMgr,
+             AllUsersName allUsersName, DiffPreferencesInfo in)
+      throws IOException, ConfigInvalidException, RepositoryNotFoundException {
+    try (Repository git = gitMgr.openRepository(allUsersName)) {
+      // Load all users prefs.
+      VersionedAccountPreferences dp =
+          VersionedAccountPreferences.forDefault();
+      dp.load(git);
+      DiffPreferencesInfo allUserPrefs = new DiffPreferencesInfo();
+      loadSection(dp.getConfig(), UserConfigSections.DIFF, null, allUserPrefs,
+          DiffPreferencesInfo.defaults(), in);
+      return allUserPrefs;
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java
index e909f17..a05058e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java
@@ -38,6 +38,8 @@
     get(CONFIG_KIND, "info").to(GetServerInfo.class);
     get(CONFIG_KIND, "preferences").to(GetPreferences.class);
     put(CONFIG_KIND, "preferences").to(SetPreferences.class);
+    get(CONFIG_KIND, "preferences.diff").to(GetDiffPreferences.class);
+    put(CONFIG_KIND, "preferences.diff").to(SetDiffPreferences.class);
     put(CONFIG_KIND, "email.confirm").to(ConfirmEmail.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/SetDiffPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/SetDiffPreferences.java
new file mode 100644
index 0000000..155ffc2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/SetDiffPreferences.java
@@ -0,0 +1,106 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.gerrit.server.config;
+
+import static com.google.gerrit.server.config.ConfigUtil.loadSection;
+import static com.google.gerrit.server.config.ConfigUtil.skipField;
+import static com.google.gerrit.server.config.ConfigUtil.storeSection;
+import static com.google.gerrit.server.config.GetDiffPreferences.readFromGit;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.account.VersionedAccountPreferences;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.UserConfigSections;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@Singleton
+public class SetDiffPreferences implements
+    RestModifyView<ConfigResource, DiffPreferencesInfo> {
+  private static final Logger log =
+      LoggerFactory.getLogger(SetDiffPreferences.class);
+
+  private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
+  private final AllUsersName allUsersName;
+  private final GitRepositoryManager gitManager;
+
+  @Inject
+  SetDiffPreferences(GitRepositoryManager gitManager,
+      Provider<MetaDataUpdate.User> metaDataUpdateFactory,
+      AllUsersName allUsersName) {
+    this.gitManager = gitManager;
+    this.metaDataUpdateFactory = metaDataUpdateFactory;
+    this.allUsersName = allUsersName;
+  }
+
+  @Override
+  public Object apply(ConfigResource configResource, DiffPreferencesInfo in)
+      throws BadRequestException, Exception {
+    if (in == null) {
+      throw new BadRequestException("input must be provided");
+    }
+    if (!hasSetFields(in)) {
+      throw new BadRequestException("unsupported option");
+    }
+    return writeToGit(readFromGit(gitManager, allUsersName, in));
+  }
+
+  private DiffPreferencesInfo writeToGit(DiffPreferencesInfo in)
+      throws RepositoryNotFoundException, IOException, ConfigInvalidException {
+    DiffPreferencesInfo out = new DiffPreferencesInfo();
+    try (MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName)) {
+      VersionedAccountPreferences prefs =
+          VersionedAccountPreferences.forDefault();
+      prefs.load(md);
+      DiffPreferencesInfo defaults = DiffPreferencesInfo.defaults();
+      storeSection(prefs.getConfig(), UserConfigSections.DIFF, null, in,
+          defaults);
+      prefs.commit(md);
+      loadSection(prefs.getConfig(), UserConfigSections.DIFF, null, out,
+          DiffPreferencesInfo.defaults(), null);
+    }
+    return out;
+  }
+
+  private static boolean hasSetFields(DiffPreferencesInfo in) {
+    try {
+      for (Field field : in.getClass().getDeclaredFields()) {
+        if (skipField(field)) {
+          continue;
+        }
+        if (field.get(in) != null) {
+          return true;
+        }
+      }
+    } catch (IllegalAccessException e) {
+      log.warn("Unable to verify input", e);
+    }
+    return false;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
index 5807c26..56daccc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
@@ -203,7 +203,7 @@
   public void addAllReviewers(ReviewDb db, ChangeAttribute a, ChangeNotes notes)
       throws OrmException {
     Collection<Account.Id> reviewers =
-        approvalsUtil.getReviewers(db, notes).values();
+        approvalsUtil.getReviewers(db, notes).all();
     if (!reviewers.isEmpty()) {
       a.allReviewers = Lists.newArrayListWithCapacity(reviewers.size());
       for (Account.Id id : reviewers) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventTypes.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventTypes.java
index 05eb5d8..9ade5ec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventTypes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventTypes.java
@@ -29,7 +29,6 @@
     register(CommitReceivedEvent.TYPE, CommitReceivedEvent.class);
     register(DraftPublishedEvent.TYPE, DraftPublishedEvent.class);
     register(HashtagsChangedEvent.TYPE, HashtagsChangedEvent.class);
-    register(MergeFailedEvent.TYPE, MergeFailedEvent.class);
     register(RefUpdatedEvent.TYPE, RefUpdatedEvent.class);
     register(RefReceivedEvent.TYPE, RefReceivedEvent.class);
     register(ReviewerAddedEvent.TYPE, ReviewerAddedEvent.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/MergeFailedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/MergeFailedEvent.java
deleted file mode 100644
index 47525b8..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/MergeFailedEvent.java
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.events;
-
-import com.google.common.base.Supplier;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.data.AccountAttribute;
-
-public class MergeFailedEvent extends PatchSetEvent {
-  static final String TYPE = "merge-failed";
-  public Supplier<AccountAttribute> submitter;
-  public String reason;
-
-  public MergeFailedEvent(Change change) {
-    super(TYPE, change);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/EventUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/EventUtil.java
new file mode 100644
index 0000000..cb30eea
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/EventUtil.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.extensions.events;
+
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.inject.Inject;
+
+public class EventUtil {
+
+  private final AccountCache accountCache;
+
+  @Inject
+  EventUtil(AccountCache accountCache) {
+    this.accountCache = accountCache;
+  }
+
+  public AccountInfo accountInfo(Account a) {
+    if (a == null || a.getId() == null) {
+      return null;
+    }
+    AccountInfo ai = new AccountInfo(a.getId().get());
+    ai.email = a.getPreferredEmail();
+    ai.name = a.getFullName();
+    ai.username = a.getUserName();
+    return ai;
+  }
+
+  public AccountInfo accountInfo(Account.Id accountId) {
+    return accountInfo(accountCache.get(accountId).getAccount());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
index 7eef0ee..6eac07c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
@@ -14,8 +14,10 @@
 
 package com.google.gerrit.server.extensions.events;
 
+import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.inject.Inject;
 
@@ -26,42 +28,111 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.Collections;
-
 public class GitReferenceUpdated {
   private static final Logger log = LoggerFactory
       .getLogger(GitReferenceUpdated.class);
 
-  public static final GitReferenceUpdated DISABLED = new GitReferenceUpdated(
-      Collections.<GitReferenceUpdatedListener> emptyList());
+  public static final GitReferenceUpdated DISABLED = new GitReferenceUpdated() {
+    @Override
+    public void fire(Project.NameKey project, RefUpdate refUpdate,
+        ReceiveCommand.Type type, Account updater) {}
+
+    @Override
+    public void fire(Project.NameKey project, RefUpdate refUpdate,
+        ReceiveCommand.Type type, Account.Id updater) {}
+
+    @Override
+    public void fire(Project.NameKey project, RefUpdate refUpdate,
+        Account updater) {}
+
+    @Override
+    public void fire(Project.NameKey project, RefUpdate refUpdate,
+        AccountInfo updater) {}
+
+    @Override
+    public void fire(Project.NameKey project, String ref, ObjectId oldObjectId,
+        ObjectId newObjectId, Account updater) {}
+
+    @Override
+    public void fire(Project.NameKey project, ReceiveCommand cmd,
+        Account updater) {}
+
+    @Override
+    public void fire(Project.NameKey project, BatchRefUpdate batchRefUpdate,
+        Account.Id updater) {}
+  };
 
   private final Iterable<GitReferenceUpdatedListener> listeners;
+  private final EventUtil util;
 
   @Inject
-  GitReferenceUpdated(DynamicSet<GitReferenceUpdatedListener> listeners) {
+  GitReferenceUpdated(DynamicSet<GitReferenceUpdatedListener> listeners,
+      EventUtil util) {
     this.listeners = listeners;
+    this.util = util;
   }
 
-  GitReferenceUpdated(Iterable<GitReferenceUpdatedListener> listeners) {
-    this.listeners = listeners;
+  private GitReferenceUpdated() {
+    this.listeners = null;
+    this.util = null;
   }
 
   public void fire(Project.NameKey project, RefUpdate refUpdate,
-      ReceiveCommand.Type type) {
+      ReceiveCommand.Type type, Account updater) {
     fire(project, refUpdate.getName(), refUpdate.getOldObjectId(),
-        refUpdate.getNewObjectId(), type);
+        refUpdate.getNewObjectId(), type, util.accountInfo(updater));
   }
 
-  public void fire(Project.NameKey project, RefUpdate refUpdate) {
+  public void fire(Project.NameKey project, RefUpdate refUpdate,
+      ReceiveCommand.Type type, Account.Id updater) {
     fire(project, refUpdate.getName(), refUpdate.getOldObjectId(),
-        refUpdate.getNewObjectId(), ReceiveCommand.Type.UPDATE);
+        refUpdate.getNewObjectId(), type, util.accountInfo(updater));
+  }
+
+  public void fire(Project.NameKey project, RefUpdate refUpdate,
+      Account updater) {
+    fire(project, refUpdate.getName(), refUpdate.getOldObjectId(),
+        refUpdate.getNewObjectId(), ReceiveCommand.Type.UPDATE,
+        util.accountInfo(updater));
+  }
+
+  public void fire(Project.NameKey project, RefUpdate refUpdate,
+      AccountInfo updater) {
+    fire(project, refUpdate.getName(), refUpdate.getOldObjectId(),
+        refUpdate.getNewObjectId(), ReceiveCommand.Type.UPDATE, updater);
   }
 
   public void fire(Project.NameKey project, String ref, ObjectId oldObjectId,
-      ObjectId newObjectId, ReceiveCommand.Type type) {
+      ObjectId newObjectId, Account updater) {
+    fire(project, ref, oldObjectId, newObjectId, ReceiveCommand.Type.UPDATE,
+        util.accountInfo(updater));
+  }
+
+  public void fire(Project.NameKey project, ReceiveCommand cmd, Account updater) {
+    fire(project, cmd.getRefName(), cmd.getOldId(), cmd.getNewId(), cmd.getType(),
+        util.accountInfo(updater));
+  }
+
+  public void fire(Project.NameKey project, BatchRefUpdate batchRefUpdate,
+      Account.Id updater) {
+    for (ReceiveCommand cmd : batchRefUpdate.getCommands()) {
+      if (cmd.getResult() == ReceiveCommand.Result.OK) {
+        fire(project, cmd, util.accountInfo(updater));
+      }
+    }
+  }
+
+  private void fire(Project.NameKey project, ReceiveCommand cmd,
+      AccountInfo updater) {
+    fire(project, cmd.getRefName(), cmd.getOldId(), cmd.getNewId(), cmd.getType(),
+        updater);
+  }
+
+  private void fire(Project.NameKey project, String ref, ObjectId oldObjectId,
+      ObjectId newObjectId, ReceiveCommand.Type type, AccountInfo updater) {
     ObjectId o = oldObjectId != null ? oldObjectId : ObjectId.zeroId();
     ObjectId n = newObjectId != null ? newObjectId : ObjectId.zeroId();
-    Event event = new Event(project, ref, o.name(), n.name(), type);
+    Event event = new Event(project, ref, o.name(), n.name(), type, updater);
     for (GitReferenceUpdatedListener l : listeners) {
       try {
         l.onGitReferenceUpdated(event);
@@ -71,38 +142,24 @@
     }
   }
 
-  public void fire(Project.NameKey project, String ref, ObjectId oldObjectId,
-      ObjectId newObjectId) {
-    fire(project, ref, oldObjectId, newObjectId, ReceiveCommand.Type.UPDATE);
-  }
-
-  public void fire(Project.NameKey project, ReceiveCommand cmd) {
-    fire(project, cmd.getRefName(), cmd.getOldId(), cmd.getNewId(), cmd.getType());
-  }
-
-  public void fire(Project.NameKey project, BatchRefUpdate batchRefUpdate) {
-    for (ReceiveCommand cmd : batchRefUpdate.getCommands()) {
-      if (cmd.getResult() == ReceiveCommand.Result.OK) {
-        fire(project, cmd);
-      }
-    }
-  }
-
   private static class Event implements GitReferenceUpdatedListener.Event {
     private final String projectName;
     private final String ref;
     private final String oldObjectId;
     private final String newObjectId;
     private final ReceiveCommand.Type type;
+    private final AccountInfo updater;
 
     Event(Project.NameKey project, String ref,
         String oldObjectId, String newObjectId,
-        ReceiveCommand.Type type) {
+        ReceiveCommand.Type type,
+        AccountInfo updater) {
       this.projectName = project.get();
       this.ref = ref;
       this.oldObjectId = oldObjectId;
       this.newObjectId = newObjectId;
       this.type = type;
+      this.updater = updater;
     }
 
     @Override
@@ -141,6 +198,11 @@
     }
 
     @Override
+    public AccountInfo getUpdater() {
+      return updater;
+    }
+
+    @Override
     public String toString() {
       return String.format("%s[%s,%s: %s -> %s]", getClass().getSimpleName(),
           projectName, ref, oldObjectId, newObjectId);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
index 86b269b..3b3c78e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
@@ -49,6 +49,8 @@
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.NoSuchRefException;
 import com.google.gerrit.server.schema.DisabledChangesReviewDbWrapper;
+import com.google.gwtorm.server.OrmConcurrencyException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 
@@ -164,7 +166,8 @@
       return BatchUpdate.this.getObjectInserter();
     }
 
-    public void addRefUpdate(ReceiveCommand cmd) {
+    public void addRefUpdate(ReceiveCommand cmd) throws IOException {
+      initRepository();
       commands.add(cmd);
     }
 
@@ -347,7 +350,10 @@
           // callers may assume a patch set ref being created means the change
           // was created, or a branch advancing meaning some changes were
           // closed.
-          u.gitRefUpdated.fire(u.project, u.batchRefUpdate);
+          u.gitRefUpdated.fire(
+              u.project,
+              u.batchRefUpdate,
+              u.getUser().isIdentifiedUser() ? u.getUser().getAccountId() : null);
         }
       }
 
@@ -399,7 +405,7 @@
   private Repository repo;
   private ObjectInserter inserter;
   private RevWalk revWalk;
-  private ChainedReceiveCommands commands = new ChainedReceiveCommands();
+  private ChainedReceiveCommands commands;
   private BatchRefUpdate batchRefUpdate;
   private boolean closeRepo;
   private Order order;
@@ -452,6 +458,7 @@
     this.repo = checkNotNull(repo, "repo");
     this.revWalk = checkNotNull(revWalk, "revWalk");
     this.inserter = checkNotNull(inserter, "inserter");
+    commands = new ChainedReceiveCommands(repo);
     return this;
   }
 
@@ -466,6 +473,7 @@
       closeRepo = true;
       inserter = repo.newObjectInserter();
       revWalk = new RevWalk(inserter.newReader());
+      commands = new ChainedReceiveCommands(repo);
     }
   }
 
@@ -529,7 +537,7 @@
   }
 
   private void executeRefUpdates() throws IOException, UpdateException {
-    if (commands.isEmpty()) {
+    if (commands == null || commands.isEmpty()) {
       return;
     }
     // May not be opened if the caller added ref updates but no new objects.
@@ -569,13 +577,7 @@
 
           // Stage the NoteDb update and store its state in the Change.
           if (!ctx.deleted && notesMigration.writeChanges()) {
-            updateManager = updateManagerFactory.create(ctx.getProject());
-            for (ChangeUpdate u : ctx.updates.values()) {
-              updateManager.add(u);
-            }
-            NoteDbChangeState.applyDelta(
-                ctx.getChange(),
-                updateManager.stage().get(id));
+            updateManager = stageNoteDbUpdate(ctx);
           }
 
           // Bump lastUpdatedOn or rowVersion and commit.
@@ -616,6 +618,25 @@
     }
   }
 
+  private NoteDbUpdateManager stageNoteDbUpdate(ChangeContext ctx)
+      throws OrmException, IOException {
+    NoteDbUpdateManager updateManager =
+        updateManagerFactory.create(ctx.getProject());
+    for (ChangeUpdate u : ctx.updates.values()) {
+      updateManager.add(u);
+    }
+    try {
+      NoteDbChangeState.applyDelta(
+          ctx.getChange(),
+          updateManager.stage().get(ctx.getChange().getId()));
+    } catch (OrmConcurrencyException ex) {
+      // Refused to apply update because NoteDb was out of sync. Go ahead with
+      // this ReviewDb update; it's still out of sync, but this is no worse than
+      // before, and it will eventually get rebuilt.
+    }
+    return updateManager;
+  }
+
   private static Iterable<Change> changesToUpdate(ChangeContext ctx) {
     Change c = ctx.getChange();
     if (ctx.bumpLastUpdatedOn && c.getLastUpdatedOn().before(ctx.getWhen())) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChainedReceiveCommands.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChainedReceiveCommands.java
index f2b6a86..a16c5c06 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChainedReceiveCommands.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChainedReceiveCommands.java
@@ -14,18 +14,18 @@
 
 package com.google.gerrit.server.git;
 
-import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.base.Optional;
 
 import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.transport.ReceiveCommand;
 
 import java.io.IOException;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.Map;
 
@@ -37,9 +37,17 @@
  * works around that limitation by allowing multiple updates per ref, as long as
  * the previous new SHA-1 matches the next old SHA-1.
  */
-public class ChainedReceiveCommands {
+public class ChainedReceiveCommands implements RefCache {
   private final Map<String, ReceiveCommand> commands = new LinkedHashMap<>();
-  private final Map<String, ObjectId> oldIds = new HashMap<>();
+  private final RepoRefCache refCache;
+
+  public ChainedReceiveCommands(Repository repo) {
+    this(new RepoRefCache(repo));
+  }
+
+  public ChainedReceiveCommands(RepoRefCache refCache) {
+    this.refCache = checkNotNull(refCache);
+  }
 
   public boolean isEmpty() {
     return commands.isEmpty();
@@ -73,38 +81,20 @@
   /**
    * Get the latest value of a ref according to this sequence of commands.
    * <p>
-   * Once the value for a ref is read once, it is cached in this instance, so
-   * that multiple callers using this instance for lookups see a single
-   * consistent snapshot.
+   * After the value for a ref is read from the repo once, it is cached as in
+   * {@link RepoRefCache}.
    *
-   * @param repo repository to read from, if result is not cached.
-   * @param refName name of the ref.
-   * @return value of the ref, taking into account commands that have already
-   *     been added to this instance. Null if the ref is deleted, matching the
-   *     behavior of {@link Repository#exactRef(String)}.
+   * @see RefCache#get(String)
    */
-  public ObjectId getObjectId(Repository repo, String refName)
-      throws IOException {
+  @Override
+  public Optional<ObjectId> get(String refName) throws IOException {
     ReceiveCommand cmd = commands.get(refName);
     if (cmd != null) {
-      return zeroToNull(cmd.getNewId());
+      return !cmd.getNewId().equals(ObjectId.zeroId())
+          ? Optional.of(cmd.getNewId())
+          : Optional.<ObjectId>absent();
     }
-    ObjectId old = oldIds.get(refName);
-    if (old != null) {
-      return zeroToNull(old);
-    }
-    Ref ref = repo.exactRef(refName);
-    ObjectId id = ref != null ? ref.getObjectId() : null;
-    // Cache missing ref as zeroId to match value in commands map.
-    oldIds.put(refName, firstNonNull(id, ObjectId.zeroId()));
-    return id;
-  }
-
-  private static ObjectId zeroToNull(ObjectId id) {
-    if (id == null || id.equals(ObjectId.zeroId())) {
-      return null;
-    }
-    return id;
+    return refCache.get(refName);
   }
 
   /**
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/EmailMerge.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/EmailMerge.java
index d236682..f19c0aa 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/EmailMerge.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/EmailMerge.java
@@ -112,8 +112,7 @@
   @Override
   public CurrentUser getUser() {
     if (submitter != null) {
-      return identifiedUserFactory.create(
-          getReviewDbProvider(), submitter).getRealUser();
+      return identifiedUserFactory.create(submitter).getRealUser();
     }
     throw new OutOfScopeException("No user on email thread");
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModules.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModules.java
index eb359e6..e6ce074 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModules.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModules.java
@@ -35,8 +35,6 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-import java.net.URI;
-import java.net.URISyntaxException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Set;
@@ -55,8 +53,7 @@
 
   private static final String GIT_MODULES = ".gitmodules";
 
-  private final String thisServer;
-  private final SubmoduleSectionParser.Factory subSecParserFactory;
+  private final String canonicalWebUrl;
   private final Branch.NameKey branch;
   private final String submissionId;
   private final MergeOpRepoManager orm;
@@ -66,20 +63,13 @@
   @AssistedInject
   GitModules(
       @CanonicalWebUrl @Nullable String canonicalWebUrl,
-      SubmoduleSectionParser.Factory subSecParserFactory,
       @Assisted Branch.NameKey branch,
       @Assisted String submissionId,
-      @Assisted MergeOpRepoManager orm) throws SubmoduleException {
-    this.subSecParserFactory = subSecParserFactory;
+      @Assisted MergeOpRepoManager orm) {
     this.orm = orm;
     this.branch = branch;
     this.submissionId = submissionId;
-    try {
-      this.thisServer = new URI(canonicalWebUrl).getHost();
-    } catch (URISyntaxException e) {
-      throw new SubmoduleException("Incorrect Gerrit canonical web url " +
-          "provided in gerrit.config file.", e);
-    }
+    this.canonicalWebUrl = canonicalWebUrl;
   }
 
   void load() throws IOException {
@@ -106,7 +96,7 @@
     try {
       BlobBasedConfig bbc =
           new BlobBasedConfig(null, or.repo, commit, GIT_MODULES);
-      subscriptions = subSecParserFactory.create(bbc, thisServer,
+      subscriptions = new SubmoduleSectionParser(bbc, canonicalWebUrl,
           branch).parseAllSections();
     } catch (ConfigInvalidException e) {
       throw new IOException(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index 26db045..30cf0a9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -729,7 +729,7 @@
     logDebug("Updating superprojects");
     SubmoduleOp subOp = subOpProvider.get();
     try {
-      subOp.updateSuperProjects(db, branches, submissionId, orm);
+      subOp.updateSuperProjects(branches, submissionId, orm);
       logDebug("Updating superprojects done");
     } catch (SubmoduleException e) {
       logError("The gitlinks were not updated according to the "
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
index 540d479..477da3f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
@@ -125,8 +125,8 @@
     public MetaDataUpdate create(Project.NameKey name, Repository repository,
         IdentifiedUser user, BatchRefUpdate batch) {
       MetaDataUpdate md = factory.create(name, repository, batch);
-      md.getCommitBuilder().setAuthor(createPersonIdent(user));
       md.getCommitBuilder().setCommitter(serverIdent);
+      md.setAuthor(user);
       return md;
     }
 
@@ -176,6 +176,7 @@
   private final CommitBuilder commit;
   private boolean allowEmpty;
   private boolean insertChangeId;
+  private IdentifiedUser author;
 
   @AssistedInject
   public MetaDataUpdate(GitReferenceUpdated gitRefUpdated,
@@ -198,8 +199,9 @@
     getCommitBuilder().setMessage(message);
   }
 
-  public void setAuthor(IdentifiedUser user) {
-    getCommitBuilder().setAuthor(user.newCommitterIdent(
+  public void setAuthor(IdentifiedUser author) {
+    this.author = author;
+    getCommitBuilder().setAuthor(author.newCommitterIdent(
         getCommitBuilder().getCommitter().getWhen(),
         getCommitBuilder().getCommitter().getTimeZone()));
   }
@@ -244,6 +246,7 @@
   }
 
   void fireGitRefUpdatedEvent(RefUpdate ru) {
-    gitRefUpdated.fire(projectName, ru);
+    gitRefUpdated.fire(
+        projectName, ru, author == null ? null : author.getAccount());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java
index 2c5e512..d7424c6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.git;
 
+import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
@@ -261,7 +262,7 @@
         throw new IOException("Couldn't update " + notesBranch + ". "
             + result.name());
       } else {
-        gitRefUpdated.fire(project, refUpdate);
+        gitRefUpdated.fire(project, refUpdate, (AccountInfo) null);
         break;
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
index 56b7b90..a0bc332 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
@@ -284,6 +284,41 @@
     }
   }
 
+  public void remove(AccessSection section, Permission permission) {
+    if (permission == null) {
+      remove(section);
+    } else if (section != null) {
+      AccessSection a = accessSections.get(section.getName());
+      a.remove(permission);
+      if (a.getPermissions().isEmpty()) {
+        remove(a);
+      }
+    }
+  }
+
+  public void remove(AccessSection section,
+      Permission permission, PermissionRule rule) {
+    if (rule == null) {
+      remove(section, permission);
+    } else if (section != null && permission != null) {
+      AccessSection a = accessSections.get(section.getName());
+      if (a == null) {
+        return;
+      }
+      Permission p = a.getPermission(permission.getName());
+      if (p == null) {
+        return;
+      }
+      p.remove(rule);
+      if (p.getRules().isEmpty()) {
+        a.remove(permission);
+      }
+      if (a.getPermissions().isEmpty()) {
+        remove(a);
+      }
+    }
+  }
+
   public void replace(AccessSection section) {
     for (Permission permission : section.getPermissions()) {
       for (PermissionRule rule : permission.getRules()) {
@@ -575,7 +610,7 @@
       Config rc, Map<String, GroupReference> groupsByName) {
     accessSections = new HashMap<>();
     for (String refName : rc.getSubsections(ACCESS)) {
-      if (RefConfigSection.isValid(refName) & isValidRegex(refName)) {
+      if (RefConfigSection.isValid(refName) && isValidRegex(refName)) {
         AccessSection as = getAccessSection(refName, true);
 
         for (String varName : rc.getStringList(ACCESS, refName, KEY_GROUP_PERMISSIONS)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index 9e1165f..b52844d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -667,7 +667,7 @@
             // We only fire gitRefUpdated for direct refs updates.
             // Events for change refs are fired when they are created.
             //
-            gitRefUpdated.fire(project.getNameKey(), c);
+            gitRefUpdated.fire(project.getNameKey(), c, user.getAccount());
             hooks.doRefUpdatedHook(
                 new Branch.NameKey(project.getNameKey(), c.getRefName()),
                 c.getOldId(),
@@ -680,7 +680,7 @@
     SubmoduleOp op = subOpProvider.get();
     try (MergeOpRepoManager orm = ormProvider.get()) {
       orm.setContext(db, TimeUtil.nowTs(), user);
-      op.updateSuperProjects(db, branches, "receiveID", orm);
+      op.updateSuperProjects(branches, "receiveID", orm);
     } catch (SubmoduleException e) {
       log.error("Can't update the superprojects", e);
     }
@@ -937,7 +937,7 @@
           case UPDATE_NONFASTFORWARD:
             try {
               ProjectConfig cfg = new ProjectConfig(project.getNameKey());
-              cfg.load(repo, cmd.getNewId());
+              cfg.load(rp.getRevWalk(), cmd.getNewId());
               if (!cfg.getValidationErrors().isEmpty()) {
                 addError("Invalid project configuration:");
                 for (ValidationError err : cfg.getValidationErrors()) {
@@ -1208,7 +1208,7 @@
     @Option(name = "--hashtag", aliases = {"-t"}, metaVar = "HASHTAG",
         usage = "add hashtag to changes")
     void addHashtag(String token) throws CmdLineException {
-      if (!notesMigration.enabled()) {
+      if (!notesMigration.readChanges()) {
         throw clp.reject("cannot add hashtags; noteDb is disabled");
       }
       String hashtag = cleanupHashtag(token);
@@ -1787,10 +1787,12 @@
             .setRequestScopePropagator(requestScopePropagator)
             .setSendMail(true)
             .setUpdateRef(true));
-        bu.addOp(
-            changeId,
-            hashtagsFactory.create(new HashtagsInput(magicBranch.hashtags))
-              .setRunHooks(false));
+        if (!magicBranch.hashtags.isEmpty()) {
+          bu.addOp(
+              changeId,
+              hashtagsFactory.create(new HashtagsInput(magicBranch.hashtags))
+                .setRunHooks(false));
+        }
         if (!Strings.isNullOrEmpty(magicBranch.topic)) {
           bu.addOp(
               changeId,
@@ -2178,7 +2180,7 @@
 
       PatchSet newPatchSet = replaceOp.getPatchSet();
       gitRefUpdated.fire(project.getNameKey(), newPatchSet.getRefName(),
-          ObjectId.zeroId(), newCommit);
+          ObjectId.zeroId(), newCommit, user.getAccount());
 
       if (magicBranch != null && magicBranch.submit) {
         submit(changeCtl, newPatchSet);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/RefCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/RefCache.java
new file mode 100644
index 0000000..562db08
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RefCache.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.server.git;
+
+import com.google.common.base.Optional;
+
+import org.eclipse.jgit.lib.ObjectId;
+
+import java.io.IOException;
+
+/**
+ * Simple short-lived cache of individual refs read from a repo.
+ * <p>
+ * Within a single request that is known to read a small bounded number of refs,
+ * this class can be used to ensure a consistent view of one ref, and avoid
+ * multiple system calls to read refs multiple times.
+ * <p>
+ * <strong>Note:</strong> Implementations of this class are only appropriate
+ * for short-term caching, and do not support invalidation. It is also not
+ * threadsafe.
+ */
+public interface RefCache {
+  /**
+   * Get the possibly-cached value of a ref.
+   *
+   * @param refName name of the ref.
+   * @return value of the ref; absent if the ref does not exist in the repo.
+   *     Never null, and never present with a value of {@link
+   *     ObjectId#zeroId()}.
+   */
+  Optional<ObjectId> get(String refName) throws IOException;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/RepoRefCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/RepoRefCache.java
new file mode 100644
index 0000000..1dfa51e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RepoRefCache.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import com.google.common.base.Optional;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+/** {@link RefCache} backed directly by a repository. */
+public class RepoRefCache implements RefCache {
+  private final RefDatabase refdb;
+  private final Map<String, Optional<ObjectId>> ids;
+
+  public RepoRefCache(Repository repo) {
+    this.refdb = repo.getRefDatabase();
+    this.ids = new HashMap<>();
+  }
+
+  @Override
+  public Optional<ObjectId> get(String refName) throws IOException {
+    Optional<ObjectId> id = ids.get(refName);
+    if (id != null) {
+      return id;
+    }
+    Ref ref = refdb.exactRef(refName);
+    id = ref != null
+        ? Optional.of(ref.getObjectId())
+        : Optional.<ObjectId>absent();
+    ids.put(refName, id);
+    return id;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
index 63c71a1..0a9c839 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
@@ -16,7 +16,6 @@
 
 import com.google.common.collect.HashMultimap;
 import com.google.common.collect.Multimap;
-import com.google.common.collect.Sets;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.SubscribeSection;
@@ -25,13 +24,13 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.MergeOpRepoManager.OpenRepo;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.dircache.DirCache;
@@ -71,7 +70,7 @@
   private final PersonIdent myIdent;
   private final GitReferenceUpdated gitRefUpdated;
   private final ProjectCache projectCache;
-  private final Set<Branch.NameKey> updatedSubscribers;
+  private final ProjectState.Factory projectStateFactory;
   private final Account account;
   private final ChangeHooks changeHooks;
   private final boolean verboseSuperProject;
@@ -85,19 +84,20 @@
       @GerritServerConfig Config cfg,
       GitReferenceUpdated gitRefUpdated,
       ProjectCache projectCache,
+      ProjectState.Factory projectStateFactory,
       @Nullable Account account,
       ChangeHooks changeHooks) {
     this.gitmodulesFactory = gitmodulesFactory;
     this.myIdent = myIdent;
     this.gitRefUpdated = gitRefUpdated;
     this.projectCache = projectCache;
+    this.projectStateFactory = projectStateFactory;
     this.account = account;
     this.changeHooks = changeHooks;
     this.verboseSuperProject = cfg.getBoolean("submodule",
         "verboseSuperprojectUpdate", true);
     this.enableSuperProjectSubscriptions = cfg.getBoolean("submodule",
         "enableSuperProjectSubscriptions", true);
-    updatedSubscribers = new HashSet<>();
   }
 
   public Collection<Branch.NameKey> getDestinationBranches(Branch.NameKey src,
@@ -143,20 +143,28 @@
     Collection<SubmoduleSubscription> ret = new ArrayList<>();
     Project.NameKey project = branch.getParentKey();
     ProjectConfig cfg = projectCache.get(project).getConfig();
-    for (SubscribeSection s : cfg.getSubscribeSections(branch)) {
+    for (SubscribeSection s : projectStateFactory.create(cfg)
+        .getSubscribeSections(branch)) {
+      logDebug("Checking subscribe section " + s);
       Collection<Branch.NameKey> branches =
           getDestinationBranches(branch, s, orm);
       for (Branch.NameKey targetBranch : branches) {
         GitModules m = gitmodulesFactory.create(targetBranch, updateId, orm);
         m.load();
-        ret.addAll(m.subscribedTo(branch));
+        for (SubmoduleSubscription ss : m.subscribedTo(branch)) {
+          logDebug("Checking SubmoduleSubscription " + ss);
+          if (projectCache.get(ss.getSubmodule().getParentKey()) != null) {
+            logDebug("Adding SubmoduleSubscription " + ss);
+            ret.add(ss);
+          }
+        }
       }
     }
     logDebug("Calculated superprojects for " + branch + " are " + ret);
     return ret;
   }
 
-  protected void updateSuperProjects(ReviewDb db,
+  protected void updateSuperProjects(
       Collection<Branch.NameKey> updatedBranches, String updateId,
       MergeOpRepoManager orm) throws SubmoduleException {
     if (!enableSuperProjectSubscriptions) {
@@ -165,32 +173,50 @@
     }
     this.updateId = updateId;
     logDebug("Updating superprojects");
-    // These (repo/branch) will be updated later with all the given
-    // individual submodule subscriptions
+
     Multimap<Branch.NameKey, SubmoduleSubscription> targets =
         HashMultimap.create();
 
-    try {
-      for (Branch.NameKey updatedBranch : updatedBranches) {
-        for (SubmoduleSubscription sub :
-          superProjectSubscriptionsForSubmoduleBranch(updatedBranch, orm)) {
-          targets.put(sub.getSuperProject(), sub);
+    for (Branch.NameKey updatedBranch : updatedBranches) {
+      logDebug("Now processing " + updatedBranch);
+      Set<Branch.NameKey> checkedTargets = new HashSet<>();
+      Set<Branch.NameKey> targetsToProcess = new HashSet<>();
+      targetsToProcess.add(updatedBranch);
+
+      while (!targetsToProcess.isEmpty()) {
+        Set<Branch.NameKey> newTargets = new HashSet<>();
+        for (Branch.NameKey b : targetsToProcess) {
+          try {
+            Collection<SubmoduleSubscription> subs =
+                superProjectSubscriptionsForSubmoduleBranch(b, orm);
+            for (SubmoduleSubscription sub : subs) {
+              Branch.NameKey dst = sub.getSuperProject();
+              targets.put(dst, sub);
+              newTargets.add(dst);
+            }
+          } catch (IOException e) {
+            throw new SubmoduleException("Cannot find superprojects for " + b, e);
+          }
         }
+        logDebug("adding to done " + targetsToProcess);
+        checkedTargets.addAll(targetsToProcess);
+        logDebug("completely done with " + checkedTargets);
+
+        Set<Branch.NameKey> intersection = new HashSet<>(checkedTargets);
+        intersection.retainAll(newTargets);
+        if (!intersection.isEmpty()) {
+          throw new SubmoduleException("Possible circular subscription involving " + updatedBranch);
+        }
+
+        targetsToProcess = newTargets;
       }
-    } catch (IOException e) {
-      throw new SubmoduleException("Could not calculate all superprojects");
     }
-    updatedSubscribers.addAll(updatedBranches);
-    // Update subscribers.
-    for (Branch.NameKey dest : targets.keySet()) {
+
+    for (Branch.NameKey dst : targets.keySet()) {
       try {
-        if (!updatedSubscribers.add(dest)) {
-          log.error("Possible circular subscription involving " + dest);
-        } else {
-          updateGitlinks(db, dest, targets.get(dest), orm);
-        }
+        updateGitlinks(dst, targets.get(dst), orm);
       } catch (SubmoduleException e) {
-        log.warn("Cannot update gitlinks for " + dest, e);
+        throw new SubmoduleException("Cannot update gitlinks for " + dst, e);
       }
     }
   }
@@ -202,7 +228,7 @@
    * @param updates submodule updates which should be updated to.
    * @throws SubmoduleException
    */
-  private void updateGitlinks(ReviewDb db, Branch.NameKey subscriber,
+  private void updateGitlinks(Branch.NameKey subscriber,
       Collection<SubmoduleSubscription> updates, MergeOpRepoManager orm)
           throws SubmoduleException {
     PersonIdent author = null;
@@ -324,7 +350,7 @@
       switch (rfu.update()) {
         case NEW:
         case FAST_FORWARD:
-          gitRefUpdated.fire(subscriber.getParentKey(), rfu);
+          gitRefUpdated.fire(subscriber.getParentKey(), rfu, account);
           changeHooks.doRefUpdatedHook(subscriber, rfu, account);
           // TODO since this is performed "in the background" no mail will be
           // sent to inform users about the updated branch
@@ -340,8 +366,6 @@
         default:
           throw new IOException(rfu.getResult().name());
       }
-      // Recursive call: update subscribers of the subscriber
-      updateSuperProjects(db, Sets.newHashSet(subscriber), updateId, orm);
     } catch (IOException e) {
       throw new SubmoduleException("Cannot update gitlinks for "
           + subscriber.get(), e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
index ecba568..dc927a6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
@@ -137,12 +137,36 @@
    */
   public void load(Repository db, ObjectId id) throws IOException,
       ConfigInvalidException {
-    reader = db.newObjectReader();
+    try (RevWalk walk = new RevWalk(db)) {
+      load(walk, id);
+    }
+  }
+
+  /**
+   * Load a specific version from an open walk.
+   * <p>
+   * This method is primarily useful for applying updates to a specific revision
+   * that was shown to an end-user in the user interface. If there are conflicts
+   * with another user's concurrent changes, these will be automatically
+   * detected at commit time.
+   * <p>
+   * The caller retains ownership of the walk and is responsible for closing
+   * it. However, this instance does not hold a reference to the walk or the
+   * repository after the call completes, allowing the application to retain
+   * this object for long periods of time.
+   *
+   * @param walk open walk to access to access.
+   * @param id revision to load.
+   * @throws IOException
+   * @throws ConfigInvalidException
+   */
+  public void load(RevWalk walk, ObjectId id) throws IOException,
+     ConfigInvalidException {
+    this.reader = walk.getObjectReader();
     try {
       revision = id != null ? new RevWalk(reader).parseCommit(id) : null;
       onLoad();
     } finally {
-      reader.close();
       reader = null;
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java
index 96cfffd..b840861 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java
@@ -34,6 +34,9 @@
     List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size());
     CodeReviewCommit firstFastForward;
     if (args.mergeTip.getInitialTip() == null) {
+      if (sorted.isEmpty()) {
+        throw new IntegrationException("nothing to merge");
+      }
       firstFastForward = sorted.remove(0);
     } else {
       firstFastForward = args.mergeUtil.getFirstFastForward(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
index 01ae0b8..5e70b91 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
@@ -145,7 +145,7 @@
       logDebug("Loading new configuration from {}", RefNames.REFS_CONFIG);
       try {
         ProjectConfig cfg = new ProjectConfig(getProject());
-        cfg.load(ctx.getRepository(), commit);
+        cfg.load(ctx.getRevWalk(), commit);
       } catch (Exception e) {
         throw new IntegrationException("Submit would store invalid"
             + " project configuration " + commit.name() + " for "
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
index 5e84fd3..74411ad 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.git.validators;
 
+import static com.google.gerrit.reviewdb.client.Change.CHANGE_ID_PATTERN;
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_CHANGES;
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_CONFIG;
 import static org.eclipse.jgit.lib.Constants.R_HEADS;
@@ -61,6 +62,7 @@
 import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.regex.Pattern;
 
 public class CommitValidators {
   private static final Logger log = LoggerFactory
@@ -182,6 +184,27 @@
   }
 
   public static class ChangeIdValidator implements CommitValidationListener {
+    private static final int SHA1_LENGTH = 7;
+    private static final String CHANGE_ID_PREFIX =
+        FooterConstants.CHANGE_ID.getName() + ":";
+    private static final String MISSING_CHANGE_ID_MSG =
+        "[%s] missing "
+        + FooterConstants.CHANGE_ID.getName()
+        + " in commit message footer";
+    private static final String MISSING_SUBJECT_MSG =
+        "[%s] missing subject; "
+        + FooterConstants.CHANGE_ID.getName()
+        + " must be in commit message footer";
+    private static final String MULTIPLE_CHANGE_ID_MSG =
+        "[%s] multiple "
+        + FooterConstants.CHANGE_ID.getName()
+        + " lines in commit message footer";
+    private static final String INVALID_CHANGE_ID_MSG =
+        "[%s] invalid "
+        + FooterConstants.CHANGE_ID.getName() +
+        " line format in commit message footer";
+    private static final Pattern CHANGE_ID = Pattern.compile(CHANGE_ID_PATTERN);
+
     private final ProjectControl projectControl;
     private final String canonicalWebUrl;
     private final String installCommitMsgHookCommand;
@@ -200,63 +223,62 @@
     @Override
     public List<CommitValidationMessage> onCommitReceived(
         CommitReceivedEvent receiveEvent) throws CommitValidationException {
-      final List<String> idList = receiveEvent.commit.getFooterLines(
-          FooterConstants.CHANGE_ID);
-
+      RevCommit commit = receiveEvent.commit;
       List<CommitValidationMessage> messages = new LinkedList<>();
+      List<String> idList = commit.getFooterLines(FooterConstants.CHANGE_ID);
+      String sha1 = commit.abbreviate(SHA1_LENGTH).name();
 
       if (idList.isEmpty()) {
         if (projectControl.getProjectState().isRequireChangeID()) {
-          String shortMsg = receiveEvent.commit.getShortMessage();
-          String changeIdPrefix = FooterConstants.CHANGE_ID.getName() + ":";
-          if (shortMsg.startsWith(changeIdPrefix)
-              && shortMsg.substring(changeIdPrefix.length()).trim()
-                  .matches("^I[0-9a-f]{8,}.*$")) {
-            throw new CommitValidationException(
-                "missing subject; Change-Id must be in commit message footer");
-          } else {
-            String errMsg = "missing Change-Id in commit message footer";
-            messages.add(getMissingChangeIdErrorMsg(
-                errMsg, receiveEvent.commit));
-            throw new CommitValidationException(errMsg, messages);
+          String shortMsg = commit.getShortMessage();
+          if (shortMsg.startsWith(CHANGE_ID_PREFIX)
+              && CHANGE_ID.matcher(shortMsg.substring(
+                  CHANGE_ID_PREFIX.length()).trim()).matches()) {
+            String errMsg = String.format(MISSING_SUBJECT_MSG, sha1);
+            throw new CommitValidationException(errMsg);
           }
-        }
-      } else if (idList.size() > 1) {
-        throw new CommitValidationException(
-            "multiple Change-Id lines in commit message footer", messages);
-      } else {
-        final String v = idList.get(idList.size() - 1).trim();
-        if (!v.matches("^I[0-9a-f]{8,}.*$")) {
-          final String errMsg =
-              "missing or invalid Change-Id line format in commit message footer";
-          messages.add(
-              getMissingChangeIdErrorMsg(errMsg, receiveEvent.commit));
+          String errMsg = String.format(MISSING_CHANGE_ID_MSG, sha1);
+          messages.add(getMissingChangeIdErrorMsg(errMsg, commit));
           throw new CommitValidationException(errMsg, messages);
         }
+      } else if (idList.size() > 1) {
+        String errMsg = String.format(
+            MULTIPLE_CHANGE_ID_MSG, sha1);
+        throw new CommitValidationException(errMsg, messages);
+      }
+      String v = idList.get(idList.size() - 1).trim();
+      if (!CHANGE_ID.matcher(v).matches()) {
+        String errMsg = String.format(INVALID_CHANGE_ID_MSG, sha1);
+        messages.add(
+          getMissingChangeIdErrorMsg(errMsg, receiveEvent.commit));
+        throw new CommitValidationException(errMsg, messages);
       }
       return Collections.emptyList();
     }
 
     private CommitValidationMessage getMissingChangeIdErrorMsg(
         final String errMsg, final RevCommit c) {
-      final String changeId = "Change-Id:";
       StringBuilder sb = new StringBuilder();
       sb.append("ERROR: ").append(errMsg);
 
-      if (c.getFullMessage().indexOf(changeId) >= 0) {
+      if (c.getFullMessage().indexOf(CHANGE_ID_PREFIX) >= 0) {
         String[] lines = c.getFullMessage().trim().split("\n");
         String lastLine = lines.length > 0 ? lines[lines.length - 1] : "";
 
-        if (lastLine.indexOf(changeId) == -1) {
+        if (lastLine.indexOf(CHANGE_ID_PREFIX) == -1) {
           sb.append('\n');
           sb.append('\n');
-          sb.append("Hint: A potential Change-Id was found, but it was not in the ");
+          sb.append("Hint: A potential ");
+          sb.append(FooterConstants.CHANGE_ID.getName());
+          sb.append("Change-Id was found, but it was not in the ");
           sb.append("footer (last paragraph) of the commit message.");
         }
       }
       sb.append('\n');
       sb.append('\n');
-      sb.append("Hint: To automatically insert Change-Id, install the hook:\n");
+      sb.append("Hint: To automatically insert ");
+      sb.append(FooterConstants.CHANGE_ID.getName());
+      sb.append(", install the hook:\n");
       sb.append(getCommitMessageHookInstallationHint());
       sb.append('\n');
       sb.append("And then amend the commit:\n");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
index 803bcf8..0684d8f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
@@ -115,7 +115,7 @@
     appendText(velocifyFile("ChangeFooter.vm"));
     try {
       TreeSet<String> names = new TreeSet<>();
-      for (Account.Id who : changeData.reviewers().values()) {
+      for (Account.Id who : changeData.reviewers().all()) {
         names.add(getNameEmailFor(who));
       }
       for (String name : names) {
@@ -352,7 +352,7 @@
     }
 
     try {
-      for (Account.Id id : changeData.reviewers().values()) {
+      for (Account.Id id : changeData.reviewers().all()) {
         add(RecipientType.CC, id);
       }
     } catch (OrmException err) {
@@ -368,7 +368,7 @@
     }
 
     try {
-      for (Account.Id id : changeData.reviewers().get(REVIEWER)) {
+      for (Account.Id id : changeData.reviewers().byState(REVIEWER)) {
         add(RecipientType.CC, id);
       }
     } catch (OrmException err) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeleteReviewerSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeleteReviewerSender.java
index 89daa1d..75f9f82 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeleteReviewerSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeleteReviewerSender.java
@@ -34,7 +34,7 @@
 public class DeleteReviewerSender extends ReplyToChangeSender {
   private final Set<Account.Id> reviewers = new HashSet<>();
 
-  public static interface Factory extends
+  public interface Factory extends
       ReplyToChangeSender.Factory<DeleteReviewerSender> {
     @Override
     DeleteReviewerSender create(Project.NameKey project, Change.Id change);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java
index af8a3f69..a1274c3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java
@@ -17,12 +17,11 @@
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.CC;
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
 
-import com.google.common.collect.Multimap;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.errors.NoSuchAccountException;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.account.AccountResolver;
-import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gwtorm.server.OrmException;
 
 import org.eclipse.jgit.revwalk.FooterKey;
@@ -58,10 +57,10 @@
   }
 
   public static MailRecipients getRecipientsFromReviewers(
-      Multimap<ReviewerStateInternal, Account.Id> reviewers) {
+      ReviewerSet reviewers) {
     MailRecipients recipients = new MailRecipients();
-    recipients.reviewers.addAll(reviewers.get(REVIEWER));
-    recipients.cc.addAll(reviewers.get(CC));
+    recipients.reviewers.addAll(reviewers.byState(REVIEWER));
+    recipients.cc.addAll(reviewers.byState(CC));
     return recipients;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergeFailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergeFailSender.java
deleted file mode 100644
index 3883786..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergeFailSender.java
+++ /dev/null
@@ -1,49 +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.server.mail;
-
-import com.google.gerrit.common.errors.EmailException;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-/** Send notice about a change failing to merged. */
-public class MergeFailSender extends ReplyToChangeSender {
-  public interface Factory {
-    MergeFailSender create(Project.NameKey project, Change.Id id);
-  }
-
-  @Inject
-  public MergeFailSender(EmailArguments ea,
-      @Assisted Project.NameKey project,
-      @Assisted Change.Id id)
-      throws OrmException {
-    super(ea, "merge-failed", newChangeData(ea, project, id));
-  }
-
-  @Override
-  protected void init() throws EmailException {
-    super.init();
-
-    ccExistingReviewers();
-  }
-
-  @Override
-  protected void formatChange() throws EmailException {
-    appendText(velocifyFile("MergeFail.vm"));
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java
index 374b2e9..8a80bfe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java
@@ -174,8 +174,7 @@
 
   private boolean add(Watchers matching, AccountProjectWatch w, NotifyType type)
       throws OrmException {
-    IdentifiedUser user =
-        args.identifiedUserFactory.create(args.db, w.getAccountId());
+    IdentifiedUser user = args.identifiedUserFactory.create(w.getAccountId());
 
     try {
       if (filterMatch(user, w.getFilter())) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
index 94dcede..e763e0c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
@@ -97,14 +97,16 @@
   }
 
   protected final Args args;
+  protected final boolean autoRebuild;
   private final Change.Id changeId;
 
   private ObjectId revision;
   private boolean loaded;
 
-  AbstractChangeNotes(Args args, Change.Id changeId) {
+  AbstractChangeNotes(Args args, Change.Id changeId, boolean autoRebuild) {
     this.args = checkNotNull(args);
     this.changeId = checkNotNull(changeId);
+    this.autoRebuild = autoRebuild;
   }
 
   public Change.Id getChangeId() {
@@ -120,7 +122,9 @@
     if (loaded) {
       return self();
     }
-    if (!args.migration.readChanges()) {
+    boolean read = args.migration.readChanges();
+    boolean readOrWrite = read || args.migration.writeChanges();
+    if (!readOrWrite && !autoRebuild) {
       loadDefaults();
       return self();
     }
@@ -129,9 +133,15 @@
     }
     try (Timer1.Context timer = args.metrics.readLatency.start(CHANGES);
         Repository repo = args.repoManager.openRepository(getProjectName());
+        // Call openHandle even if reading is disabled, to trigger
+        // auto-rebuilding before this object may get passed to a ChangeUpdate.
         LoadHandle handle = openHandle(repo)) {
-      revision = handle.id();
-      onLoad(handle);
+      if (read) {
+        revision = handle.id();
+        onLoad(handle);
+      } else {
+        loadDefaults();
+      }
       loaded = true;
     } catch (ConfigInvalidException | IOException e) {
       throw new OrmException(e);
@@ -145,7 +155,11 @@
   }
 
   protected LoadHandle openHandle(Repository repo) throws IOException {
-    return LoadHandle.create(ChangeNotesCommit.newRevWalk(repo), readRef(repo));
+    return openHandle(repo, readRef(repo));
+  }
+
+  protected LoadHandle openHandle(Repository repo, ObjectId id) {
+    return LoadHandle.create(ChangeNotesCommit.newRevWalk(repo), id);
   }
 
   public T reload() throws OrmException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
index b55b416..0e62600 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -43,10 +44,12 @@
   protected final NotesMigration migration;
   protected final ChangeNoteUtil noteUtil;
   protected final String anonymousCowardName;
-  protected final ChangeNotes notes;
   protected final Account.Id accountId;
   protected final PersonIdent authorIdent;
   protected final Date when;
+
+  @Nullable private final ChangeNotes notes;
+  private final Change change;
   private final PersonIdent serverIdent;
 
   protected PatchSet.Id psId;
@@ -59,15 +62,16 @@
       String anonymousCowardName,
       ChangeNoteUtil noteUtil,
       Date when) {
-    this(
-        migration,
-        noteUtil,
-        serverIdent,
-        anonymousCowardName,
-        ctl.getNotes(),
-        accountId(ctl.getUser()),
-        ident(noteUtil, serverIdent, anonymousCowardName, ctl.getUser(), when),
-        when);
+    this.migration = migration;
+    this.noteUtil = noteUtil;
+    this.serverIdent = new PersonIdent(serverIdent, when);
+    this.anonymousCowardName = anonymousCowardName;
+    this.notes = ctl.getNotes();
+    this.change = notes.getChange();
+    this.accountId = accountId(ctl.getUser());
+    this.authorIdent =
+        ident(noteUtil, serverIdent, anonymousCowardName, ctl.getUser(), when);
+    this.when = when;
   }
 
   protected AbstractChangeUpdate(
@@ -75,15 +79,21 @@
       ChangeNoteUtil noteUtil,
       PersonIdent serverIdent,
       String anonymousCowardName,
-      ChangeNotes notes,
+      @Nullable ChangeNotes notes,
+      @Nullable Change change,
       Account.Id accountId,
       PersonIdent authorIdent,
       Date when) {
+    checkArgument(
+        (notes != null && change == null)
+            || (notes == null && change != null),
+        "exactly one of notes or change required");
     this.migration = migration;
     this.noteUtil = noteUtil;
     this.serverIdent = new PersonIdent(serverIdent, when);
     this.anonymousCowardName = anonymousCowardName;
     this.notes = notes;
+    this.change = change != null ? change : notes.getChange();
     this.accountId = accountId;
     this.authorIdent = authorIdent;
     this.when = when;
@@ -114,15 +124,16 @@
   }
 
   public Change.Id getId() {
-    return notes.getChangeId();
+    return change.getId();
   }
 
+  @Nullable
   public ChangeNotes getNotes() {
     return notes;
   }
 
   public Change getChange() {
-    return notes.getChange();
+    return change;
   }
 
   public Date getWhen() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundle.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundle.java
index 4d2a719..e15af9d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundle.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundle.java
@@ -27,6 +27,8 @@
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Function;
 import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.base.Strings;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.ComparisonChain;
 import com.google.common.collect.ImmutableCollection;
@@ -50,6 +52,7 @@
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.PatchLineCommentsUtil;
+import com.google.gerrit.server.ReviewerSet;
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.server.OrmException;
 
@@ -84,12 +87,15 @@
       throws OrmException {
     db.changes().beginTransaction(id);
     try {
+      List<PatchSetApproval> approvals =
+          db.patchSetApprovals().byChange(id).toList();
       return new ChangeBundle(
           db.changes().get(id),
           db.changeMessages().byChange(id),
           db.patchSets().byChange(id),
-          db.patchSetApprovals().byChange(id),
+          approvals,
           db.patchComments().byChange(id),
+          ReviewerSet.fromApprovals(approvals),
           Source.REVIEW_DB);
     } finally {
       db.rollback();
@@ -106,6 +112,7 @@
         Iterables.concat(
             plcUtil.draftByChange(null, notes),
             plcUtil.publishedByChange(null, notes)),
+        notes.getReviewers(),
         Source.NOTE_DB);
   }
 
@@ -156,7 +163,6 @@
     return CHANGE_MESSAGE_ORDER.immutableSortedCopy(in);
   }
 
-
   private static TreeMap<PatchSet.Id, PatchSet> patchSetMap(Iterable<PatchSet> in) {
     TreeMap<PatchSet.Id, PatchSet> out = new TreeMap<>(
         new Comparator<PatchSet.Id>() {
@@ -256,6 +262,7 @@
       patchSetApprovals;
   private final ImmutableMap<PatchLineComment.Key, PatchLineComment>
       patchLineComments;
+  private final ReviewerSet reviewers;
   private final Source source;
 
   public ChangeBundle(
@@ -264,6 +271,7 @@
       Iterable<PatchSet> patchSets,
       Iterable<PatchSetApproval> patchSetApprovals,
       Iterable<PatchLineComment> patchLineComments,
+      ReviewerSet reviewers,
       Source source) {
     this.change = checkNotNull(change);
     this.changeMessages = changeMessageList(changeMessages);
@@ -272,6 +280,7 @@
         ImmutableMap.copyOf(patchSetApprovalMap(patchSetApprovals));
     this.patchLineComments =
         ImmutableMap.copyOf(patchLineCommentMap(patchLineComments));
+    this.reviewers = checkNotNull(reviewers);
     this.source = checkNotNull(source);
 
     for (ChangeMessage m : this.changeMessages) {
@@ -309,6 +318,10 @@
     return patchLineComments.values();
   }
 
+  public ReviewerSet getReviewers() {
+    return reviewers;
+  }
+
   public Source getSource() {
     return source;
   }
@@ -319,6 +332,7 @@
     diffChangeMessages(diffs, this, o);
     diffPatchSets(diffs, this, o);
     diffPatchSetApprovals(diffs, this, o);
+    diffReviewers(diffs, this, o);
     diffPatchLineComments(diffs, this, o);
     return ImmutableList.copyOf(diffs);
   }
@@ -375,30 +389,41 @@
 
   private <K, V> Map<K, V> limitToValidPatchSets(Map<K, V> in,
       final Function<K, PatchSet.Id> func) {
-    final Predicate<PatchSet.Id> upToCurrent = upToCurrentPredicate();
     return Maps.filterKeys(
-        in, new Predicate<K>() {
-          @Override
-          public boolean apply(K in) {
-            PatchSet.Id psId = func.apply(in);
-            return upToCurrent.apply(psId) && patchSets.containsKey(psId);
-          }
-        });
+        in, Predicates.compose(validPatchSetPredicate(), func));
+  }
+
+  private Predicate<PatchSet.Id> validPatchSetPredicate() {
+    final Predicate<PatchSet.Id> upToCurrent = upToCurrentPredicate();
+    return new Predicate<PatchSet.Id>() {
+      @Override
+      public boolean apply(PatchSet.Id in) {
+        return upToCurrent.apply(in) && patchSets.containsKey(in);
+      }
+    };
   }
 
   private Collection<ChangeMessage> filterChangeMessages() {
+    final Predicate<PatchSet.Id> validPatchSet = validPatchSetPredicate();
     return Collections2.filter(changeMessages,
         new Predicate<ChangeMessage>() {
           @Override
           public boolean apply(ChangeMessage in) {
             PatchSet.Id psId = in.getPatchSetId();
-            return psId == null || patchSets.containsKey(psId);
+            if (psId == null) {
+              return true;
+            }
+            return validPatchSet.apply(psId);
           }
         });
   }
 
   private Predicate<PatchSet.Id> upToCurrentPredicate() {
-    final int max = change.currentPatchSetId().get();
+    PatchSet.Id current = change.currentPatchSetId();
+    if (current == null) {
+      return Predicates.alwaysFalse();
+    }
+    final int max = current.get();
     return new Predicate<PatchSet.Id>() {
       @Override
       public boolean apply(PatchSet.Id in) {
@@ -418,15 +443,17 @@
     String desc = a.getId().equals(b.getId()) ? describe(a.getId()) : "Changes";
 
     boolean excludeCreatedOn = false;
+    boolean excludeCurrentPatchSetId = false;
     boolean excludeTopic = false;
     Timestamp aUpdated = a.getLastUpdatedOn();
     Timestamp bUpdated = b.getLastUpdatedOn();
 
-    CharMatcher s = CharMatcher.is(' ');
     boolean excludeSubject = false;
     boolean excludeOrigSubj = false;
-    String aSubj = a.getSubject();
-    String bSubj = b.getSubject();
+    // Subject is not technically a nullable field, but we observed some null
+    // subjects in the wild on googlesource.com, so treat null as empty.
+    String aSubj = Strings.nullToEmpty(a.getSubject());
+    String bSubj = Strings.nullToEmpty(b.getSubject());
 
     // Allow created timestamp in NoteDb to be either the created timestamp of
     // the change, or the timestamp of the first remaining patch set.
@@ -460,22 +487,33 @@
     //
     // Ignore empty topic on the ReviewDb side if it is null on the NoteDb side.
     //
+    // Ignore currentPatchSetId on NoteDb side if ReviewDb does not point to a
+    // valid patch set.
+    //
     // Use max timestamp of all ReviewDb entities when comparing with NoteDb.
     if (bundleA.source == REVIEW_DB && bundleB.source == NOTE_DB) {
       excludeCreatedOn = !timestampsDiffer(
           bundleA, bundleA.getFirstPatchSetTime(), bundleB, b.getCreatedOn());
-      aSubj = s.trimLeadingFrom(aSubj);
-      excludeSubject = bSubj.startsWith(aSubj);
+      aSubj = cleanReviewDbSubject(aSubj);
+      excludeCurrentPatchSetId =
+          !bundleA.validPatchSetPredicate().apply(a.currentPatchSetId());
+      excludeSubject = bSubj.startsWith(aSubj) || excludeCurrentPatchSetId;
       excludeOrigSubj = true;
-      excludeTopic = "".equals(a.getTopic()) && b.getTopic() == null;
+      String aTopic = trimLeadingOrNull(a.getTopic());
+      excludeTopic = Objects.equals(aTopic, b.getTopic())
+          || "".equals(aTopic) && b.getTopic() == null;
       aUpdated = bundleA.getLatestTimestamp();
     } else if (bundleA.source == NOTE_DB && bundleB.source == REVIEW_DB) {
       excludeCreatedOn = !timestampsDiffer(
           bundleA, a.getCreatedOn(), bundleB, bundleB.getFirstPatchSetTime());
-      bSubj = s.trimLeadingFrom(bSubj);
-      excludeSubject = aSubj.startsWith(bSubj);
+      bSubj = cleanReviewDbSubject(bSubj);
+      excludeCurrentPatchSetId =
+          !bundleB.validPatchSetPredicate().apply(b.currentPatchSetId());
+      excludeSubject = aSubj.startsWith(bSubj) || excludeCurrentPatchSetId;
       excludeOrigSubj = true;
-      excludeTopic = a.getTopic() == null && "".equals(b.getTopic());
+      String bTopic = trimLeadingOrNull(b.getTopic());
+      excludeTopic = Objects.equals(bTopic, a.getTopic())
+          || a.getTopic() == null && "".equals(bTopic);
       bUpdated = bundleB.getLatestTimestamp();
     }
 
@@ -486,6 +524,9 @@
     if (excludeCreatedOn) {
       exclude.add("createdOn");
     }
+    if (excludeCurrentPatchSetId) {
+      exclude.add("currentPatchSetId");
+    }
     if (excludeOrigSubj) {
       exclude.add("originalSubject");
     }
@@ -508,6 +549,26 @@
     }
   }
 
+  private static String trimLeadingOrNull(String s) {
+    return s != null ? CharMatcher.whitespace().trimLeadingFrom(s) : null;
+  }
+
+  private static String cleanReviewDbSubject(String s) {
+    s = CharMatcher.is(' ').trimLeadingFrom(s);
+
+    // An old JGit bug failed to extract subjects from commits with "\r\n"
+    // terminators: https://bugs.eclipse.org/bugs/show_bug.cgi?id=400707
+    // Changes created with this bug may have "\r\n" converted to "\r " and the
+    // entire commit in the subject. The version of JGit used to read NoteDb
+    // changes parses these subjects correctly, so we need to clean up old
+    // ReviewDb subjects before comparing.
+    int rn = s.indexOf("\r \r ");
+    if (rn >= 0) {
+      s = s.substring(0, rn);
+    }
+    return s;
+  }
+
   /**
    * Set of fields that must always exactly match between ReviewDb and NoteDb.
    * <p>
@@ -515,7 +576,7 @@
    * messages below.
    */
   @AutoValue
-  static abstract class ChangeMessageCandidate {
+  abstract static class ChangeMessageCandidate {
     static ChangeMessageCandidate create(ChangeMessage cm) {
       return new AutoValue_ChangeBundle_ChangeMessageCandidate(
           cm.getAuthor(),
@@ -661,6 +722,12 @@
     }
   }
 
+  private static void diffReviewers(List<String> diffs,
+      ChangeBundle bundleA, ChangeBundle bundleB) {
+    diffSets(
+        diffs, bundleA.reviewers.all(), bundleB.reviewers.all(), "reviewer");
+  }
+
   private static void diffPatchLineComments(List<String> diffs,
       ChangeBundle bundleA, ChangeBundle bundleB) {
     Map<PatchLineComment.Key, PatchLineComment> as =
@@ -677,19 +744,26 @@
 
   private static <T> Set<T> diffKeySets(List<String> diffs, Map<T, ?> a,
       Map<T, ?> b) {
-    Set<T> as = a.keySet();
-    Set<T> bs = b.keySet();
+    if (a.isEmpty() && b.isEmpty()) {
+      return a.keySet();
+    }
+    String clazz =
+        keyClass((!a.isEmpty() ? a.keySet() : b.keySet()).iterator().next());
+    return diffSets(diffs, a.keySet(), b.keySet(), clazz);
+  }
+
+  private static <T> Set<T> diffSets(List<String> diffs, Set<T> as,
+      Set<T> bs, String desc) {
     if (as.isEmpty() && bs.isEmpty()) {
       return as;
     }
-    String clazz = keyClass((!as.isEmpty() ? as : bs).iterator().next());
 
     Set<T> aNotB = Sets.difference(as, bs);
     Set<T> bNotA = Sets.difference(bs, as);
     if (aNotB.isEmpty() && bNotA.isEmpty()) {
       return as;
     }
-    diffs.add(clazz + " sets differ: " + aNotB + " only in A; "
+    diffs.add(desc + " sets differ: " + aNotB + " only in A; "
         + bNotA + " only in B");
     return Sets.intersection(as, bs);
   }
@@ -825,9 +899,14 @@
   private static String keyClass(Object obj) {
     Class<?> clazz = obj.getClass();
     String name = clazz.getSimpleName();
-    checkArgument(name.equals("Key") || name.equals("Id"),
+    checkArgument(name.endsWith("Key") || name.endsWith("Id"),
         "not an Id/Key class: %s", name);
-    return clazz.getEnclosingClass().getSimpleName() + "." + name;
+    if (name.equals("Key") || name.equals("Id")) {
+      return clazz.getEnclosingClass().getSimpleName() + "." + name;
+    } else if (name.startsWith("AutoValue_")) {
+      return name.substring(name.lastIndexOf('_') + 1);
+    }
+    return name;
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
index 84e647e..7b59a47 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
@@ -21,6 +21,7 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -62,6 +63,8 @@
   public interface Factory {
     ChangeDraftUpdate create(ChangeNotes notes, Account.Id accountId,
         PersonIdent authorIdent, Date when);
+    ChangeDraftUpdate create(Change change, Account.Id accountId,
+        PersonIdent authorIdent, Date when);
   }
 
   @AutoValue
@@ -76,8 +79,8 @@
 
   private final AllUsersName draftsProject;
 
-  private List<PatchLineComment> put;
-  private Set<Key> delete;
+  private List<PatchLineComment> put = new ArrayList<>();
+  private Set<Key> delete = new HashSet<>();
 
   @AssistedInject
   private ChangeDraftUpdate(
@@ -90,11 +93,25 @@
       @Assisted Account.Id accountId,
       @Assisted PersonIdent authorIdent,
       @Assisted Date when) {
-    super(migration, noteUtil, serverIdent, anonymousCowardName, notes,
+    super(migration, noteUtil, serverIdent, anonymousCowardName, notes, null,
         accountId, authorIdent, when);
     this.draftsProject = allUsers;
-    this.put = new ArrayList<>();
-    this.delete = new HashSet<>();
+  }
+
+  @AssistedInject
+  private ChangeDraftUpdate(
+      @GerritPersonIdent PersonIdent serverIdent,
+      @AnonymousCowardName String anonymousCowardName,
+      NotesMigration migration,
+      AllUsersName allUsers,
+      ChangeNoteUtil noteUtil,
+      @Assisted Change change,
+      @Assisted Account.Id accountId,
+      @Assisted PersonIdent authorIdent,
+      @Assisted Date when) {
+    super(migration, noteUtil, serverIdent, anonymousCowardName, null, change,
+        accountId, authorIdent, when);
+    this.draftsProject = allUsers;
   }
 
   public void putComment(PatchLineComment c) {
@@ -179,14 +196,17 @@
       // If reading from changes is enabled, then the old DraftCommentNotes
       // already parsed the revision notes. We can reuse them as long as the ref
       // hasn't advanced.
-      DraftCommentNotes draftNotes =
-          getNotes().load().getDraftCommentNotes();
-      if (draftNotes != null) {
-        ObjectId idFromNotes =
-            firstNonNull(draftNotes.getRevision(), ObjectId.zeroId());
-        RevisionNoteMap rnm = draftNotes.getRevisionNoteMap();
-        if (idFromNotes.equals(curr) && rnm != null) {
-          return rnm;
+      ChangeNotes changeNotes = getNotes();
+      if (changeNotes != null) {
+        DraftCommentNotes draftNotes =
+            changeNotes.load().getDraftCommentNotes();
+        if (draftNotes != null) {
+          ObjectId idFromNotes =
+              firstNonNull(draftNotes.getRevision(), ObjectId.zeroId());
+          RevisionNoteMap rnm = draftNotes.getRevisionNoteMap();
+          if (idFromNotes.equals(curr) && rnm != null) {
+            return rnm;
+          }
         }
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
index aad2bcd..926194b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -28,7 +28,6 @@
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.ImmutableSetMultimap;
 import com.google.common.collect.ImmutableSortedMap;
 import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.collect.Iterables;
@@ -36,6 +35,7 @@
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Multimaps;
 import com.google.common.collect.Ordering;
+import com.google.common.collect.Tables;
 import com.google.common.util.concurrent.AsyncFunction;
 import com.google.common.util.concurrent.CheckedFuture;
 import com.google.common.util.concurrent.Futures;
@@ -55,7 +55,9 @@
 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.git.ChainedReceiveCommands;
+import com.google.gerrit.server.ReviewerSet;
+import com.google.gerrit.server.git.RefCache;
+import com.google.gerrit.server.git.RepoRefCache;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -202,8 +204,8 @@
     }
 
     public ChangeNotes createWithAutoRebuildingDisabled(Change change,
-        ChainedReceiveCommands cmds) throws OrmException {
-      return new ChangeNotes(args, change.getProject(), change, false, cmds)
+        RefCache refs) throws OrmException {
+      return new ChangeNotes(args, change.getProject(), change, false, refs)
           .load();
     }
 
@@ -376,12 +378,11 @@
 
   private final Project.NameKey project;
   private final Change change;
-  private final boolean autoRebuild;
-  private final ChainedReceiveCommands cmds;
+  private final RefCache refs;
 
   private ImmutableSortedMap<PatchSet.Id, PatchSet> patchSets;
   private ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvals;
-  private ImmutableSetMultimap<ReviewerStateInternal, Account.Id> reviewers;
+  private ReviewerSet reviewers;
   private ImmutableList<Account.Id> allPastReviewers;
   private ImmutableList<SubmitRecord> submitRecords;
   private ImmutableList<ChangeMessage> allChangeMessages;
@@ -401,12 +402,11 @@
   }
 
   private ChangeNotes(Args args, Project.NameKey project, Change change,
-      boolean autoRebuild, @Nullable ChainedReceiveCommands cmds) {
-    super(args, change.getId());
+      boolean autoRebuild, @Nullable RefCache refs) {
+    super(args, change.getId(), autoRebuild);
     this.project = project;
     this.change = new Change(change);
-    this.autoRebuild = autoRebuild;
-    this.cmds = cmds;
+    this.refs = refs;
   }
 
   public Change getChange() {
@@ -421,7 +421,7 @@
     return approvals;
   }
 
-  public ImmutableSetMultimap<ReviewerStateInternal, Account.Id> getReviewers() {
+  public ReviewerSet getReviewers() {
     return reviewers;
   }
 
@@ -584,13 +584,7 @@
     } else {
       hashtags = ImmutableSet.of();
     }
-    ImmutableSetMultimap.Builder<ReviewerStateInternal, Account.Id> reviewers =
-        ImmutableSetMultimap.builder();
-    for (Map.Entry<Account.Id, ReviewerStateInternal> e
-        : parser.reviewers.entrySet()) {
-      reviewers.put(e.getValue(), e.getKey());
-    }
-    this.reviewers = reviewers.build();
+    this.reviewers = ReviewerSet.fromTable(Tables.transpose(parser.reviewers));
     this.allPastReviewers = ImmutableList.copyOf(parser.allPastReviewers);
 
     submitRecords = ImmutableList.copyOf(parser.submitRecords);
@@ -599,7 +593,7 @@
   @Override
   protected void loadDefaults() {
     approvals = ImmutableListMultimap.of();
-    reviewers = ImmutableSetMultimap.of();
+    reviewers = ReviewerSet.empty();
     submitRecords = ImmutableList.of();
     allChangeMessages = ImmutableList.of();
     changeMessagesByPatchSet = ImmutableListMultimap.of();
@@ -616,8 +610,8 @@
 
   @Override
   protected ObjectId readRef(Repository repo) throws IOException {
-    return cmds != null
-        ? cmds.getObjectId(repo, getRefName())
+    return refs != null
+        ? refs.get(getRefName()).orNull()
         : super.readRef(repo);
   }
 
@@ -625,25 +619,31 @@
   protected LoadHandle openHandle(Repository repo) throws IOException {
     if (autoRebuild) {
       NoteDbChangeState state = NoteDbChangeState.parse(change);
-      if (state == null || !state.isChangeUpToDate(repo)) {
-        return rebuildAndOpen(repo);
+      ObjectId id = readRef(repo);
+      if (state == null && id == null) {
+        return super.openHandle(repo, id);
+      }
+      RefCache refs = this.refs != null ? this.refs : new RepoRefCache(repo);
+      if (!NoteDbChangeState.isChangeUpToDate(state, refs, getChangeId())) {
+        return rebuildAndOpen(repo, id);
       }
     }
     return super.openHandle(repo);
   }
 
-  private LoadHandle rebuildAndOpen(Repository repo) throws IOException {
+  private LoadHandle rebuildAndOpen(Repository repo, ObjectId oldId)
+      throws IOException {
     try {
       NoteDbChangeState newState =
           args.rebuilder.get().rebuild(args.db.get(), getChangeId());
       if (newState == null) {
-        return super.openHandle(repo); // May be null in tests.
+        return super.openHandle(repo, oldId); // May be null in tests.
       }
       repo.scanForRepoChanges();
       return LoadHandle.create(
           ChangeNotesCommit.newRevWalk(repo), newState.getChangeMetaId());
     } catch (NoSuchChangeException e) {
-      return super.openHandle(repo);
+      return super.openHandle(repo, oldId);
     } catch (OrmException | ConfigInvalidException e) {
       throw new IOException(e);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index 9d9e180..02dd441 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -36,6 +36,7 @@
 import com.google.common.base.Splitter;
 import com.google.common.base.Supplier;
 import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.HashBasedTable;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ImmutableSet;
@@ -94,7 +95,7 @@
   private static final RevId PARTIAL_PATCH_SET =
       new RevId("INVALID PARTIAL PATCH SET");
 
-  final Map<Account.Id, ReviewerStateInternal> reviewers;
+  final Table<Account.Id, ReviewerStateInternal, Timestamp> reviewers;
   final List<Account.Id> allPastReviewers;
   final List<SubmitRecord> submitRecords;
   final Multimap<RevId, PatchLineComment> comments;
@@ -134,7 +135,7 @@
     this.noteUtil = noteUtil;
     this.metrics = metrics;
     approvals = new HashMap<>();
-    reviewers = new LinkedHashMap<>();
+    reviewers = HashBasedTable.create();
     allPastReviewers = new ArrayList<>();
     submitRecords = Lists.newArrayListWithExpectedSize(1);
     allChangeMessages = new ArrayList<>();
@@ -156,7 +157,7 @@
         parse(commit);
       }
       parseNotes();
-      allPastReviewers.addAll(reviewers.keySet());
+      allPastReviewers.addAll(reviewers.rowKeySet());
       pruneReviewers();
       updatePatchSetStates();
       checkMandatoryFooters();
@@ -262,7 +263,7 @@
 
     for (ReviewerStateInternal state : ReviewerStateInternal.values()) {
       for (String line : commit.getFooterLineValues(state.getFooterKey())) {
-        parseReviewer(state, line);
+        parseReviewer(ts, state, line);
       }
       // Don't update timestamp when a reviewer was added, matching RevewDb
       // behavior.
@@ -698,27 +699,27 @@
     return noteUtil.parseIdent(commit.getAuthorIdent(), id);
   }
 
-  private void parseReviewer(ReviewerStateInternal state, String line)
-      throws ConfigInvalidException {
+  private void parseReviewer(Timestamp ts, ReviewerStateInternal state,
+      String line) throws ConfigInvalidException {
     PersonIdent ident = RawParseUtils.parsePersonIdent(line);
     if (ident == null) {
       throw invalidFooter(state.getFooterKey(), line);
     }
     Account.Id accountId = noteUtil.parseIdent(ident, id);
-    if (!reviewers.containsKey(accountId)) {
-      reviewers.put(accountId, state);
+    if (!reviewers.containsRow(accountId)) {
+      reviewers.put(accountId, state, ts);
     }
   }
 
   private void pruneReviewers() {
-    Iterator<Map.Entry<Account.Id, ReviewerStateInternal>> rit =
-        reviewers.entrySet().iterator();
+    Iterator<Table.Cell<Account.Id, ReviewerStateInternal, Timestamp>> rit =
+        reviewers.cellSet().iterator();
     while (rit.hasNext()) {
-      Map.Entry<Account.Id, ReviewerStateInternal> e = rit.next();
-      if (e.getValue() == ReviewerStateInternal.REMOVED) {
+      Table.Cell<Account.Id, ReviewerStateInternal, Timestamp> e = rit.next();
+      if (e.getColumnKey() == ReviewerStateInternal.REMOVED) {
         rit.remove();
         for (Table<Account.Id, ?, ?> curr : approvals.values()) {
-          curr.rowKeySet().remove(e.getKey());
+          curr.rowKeySet().remove(e.getRowKey());
         }
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilderImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilderImpl.java
index 1a44f081..5105fc0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilderImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilderImpl.java
@@ -25,6 +25,7 @@
 import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.common.base.MoreObjects;
+import com.google.common.base.Optional;
 import com.google.common.base.Predicate;
 import com.google.common.base.Splitter;
 import com.google.common.collect.ArrayListMultimap;
@@ -35,6 +36,7 @@
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Ordering;
 import com.google.common.collect.Sets;
+import com.google.common.collect.Table;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.FormatUtil;
 import com.google.gerrit.common.Nullable;
@@ -46,6 +48,7 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.server.GerritPersonIdent;
@@ -53,6 +56,7 @@
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.git.ChainedReceiveCommands;
+import com.google.gerrit.server.notedb.NoteDbUpdateManager.OpenRepo;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
@@ -68,6 +72,7 @@
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.TextProgressMonitor;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -111,7 +116,6 @@
 
   private final AccountCache accountCache;
   private final ChangeDraftUpdate.Factory draftUpdateFactory;
-  private final ChangeNotes.Factory notesFactory;
   private final ChangeNoteUtil changeNoteUtil;
   private final ChangeUpdate.Factory updateFactory;
   private final NoteDbUpdateManager.Factory updateManagerFactory;
@@ -124,7 +128,6 @@
   ChangeRebuilderImpl(SchemaFactory<ReviewDb> schemaFactory,
       AccountCache accountCache,
       ChangeDraftUpdate.Factory draftUpdateFactory,
-      ChangeNotes.Factory notesFactory,
       ChangeNoteUtil changeNoteUtil,
       ChangeUpdate.Factory updateFactory,
       NoteDbUpdateManager.Factory updateManagerFactory,
@@ -135,7 +138,6 @@
     super(schemaFactory);
     this.accountCache = accountCache;
     this.draftUpdateFactory = draftUpdateFactory;
-    this.notesFactory = notesFactory;
     this.changeNoteUtil = changeNoteUtil;
     this.updateFactory = updateFactory;
     this.updateManagerFactory = updateManagerFactory;
@@ -208,7 +210,7 @@
     try (ObjectInserter allUsersInserter = allUsersRepo.newObjectInserter();
         RevWalk allUsersRw = new RevWalk(allUsersInserter.newReader())) {
       manager.setAllUsersRepo(allUsersRepo, allUsersRw, allUsersInserter,
-          new ChainedReceiveCommands());
+          new ChainedReceiveCommands(allUsersRepo));
       for (Change.Id changeId : allChanges.get(project)) {
         try {
           buildUpdates(manager, ChangeBundle.fromReviewDb(db, changeId));
@@ -227,6 +229,7 @@
 
   private void buildUpdates(NoteDbUpdateManager manager, ChangeBundle bundle)
       throws IOException, OrmException {
+    manager.setCheckExpectedState(false);
     Change change = new Change(bundle.getChange());
     PatchSet.Id currPsId = change.currentPatchSetId();
     // We will rebuild all events, except for draft comments, in buckets based
@@ -235,11 +238,11 @@
     Multimap<Account.Id, PatchLineCommentEvent> draftCommentEvents =
         ArrayListMultimap.create();
 
-    Repository changeMetaRepo = manager.getChangeRepo().repo;
     events.addAll(getHashtagsEvents(change, manager));
 
     // Delete ref only after hashtags have been read
-    deleteRef(change, changeMetaRepo, manager.getChangeRepo().cmds);
+    deleteChangeMetaRef(change, manager.getChangeRepo().cmds);
+    deleteDraftRefs(change, manager.getAllUsersRepo());
 
     Integer minPsNum = getMinPatchSetNum(bundle);
     Set<PatchSet.Id> psIds =
@@ -272,6 +275,11 @@
       }
     }
 
+    for (Table.Cell<ReviewerStateInternal, Account.Id, Timestamp> r :
+        bundle.getReviewers().asTable().cellSet()) {
+      events.add(new ReviewerEvent(r, change.getCreatedOn()));
+    }
+
     Change noteDbChange = new Change(null, null, null, null, null);
     for (ChangeMessage msg : bundle.getChangeMessages()) {
       if (msg.getPatchSetId() == null || psIds.contains(msg.getPatchSetId())) {
@@ -377,8 +385,7 @@
       labelNameComparator = Ordering.natural();
     }
     ChangeUpdate update = updateFactory.create(
-        notesFactory.createWithAutoRebuildingDisabled(
-            change, manager.getChangeRepo().cmds),
+        change,
         events.getAccountId(),
         events.newAuthorIdent(),
         events.getWhen(),
@@ -395,13 +402,12 @@
 
   private void flushEventsToDraftUpdate(NoteDbUpdateManager manager,
       EventList<PatchLineCommentEvent> events, Change change)
-      throws OrmException, IOException {
+      throws OrmException {
     if (events.isEmpty()) {
       return;
     }
     ChangeDraftUpdate update = draftUpdateFactory.create(
-        notesFactory.createWithAutoRebuildingDisabled(
-            change, manager.getChangeRepo().cmds),
+        change,
         events.getAccountId(),
         events.newAuthorIdent(),
         events.getWhen());
@@ -416,15 +422,15 @@
   private List<HashtagsEvent> getHashtagsEvents(Change change,
       NoteDbUpdateManager manager) throws IOException {
     String refName = changeMetaRef(change.getId());
-    ObjectId old = manager.getChangeRepo().getObjectId(refName);
-    if (old == null) {
+    Optional<ObjectId> old = manager.getChangeRepo().getObjectId(refName);
+    if (!old.isPresent()) {
       return Collections.emptyList();
     }
 
     RevWalk rw = manager.getChangeRepo().rw;
     List<HashtagsEvent> events = new ArrayList<>();
     rw.reset();
-    rw.markStart(rw.parseCommit(old));
+    rw.markStart(rw.parseCommit(old.get()));
     for (RevCommit commit : rw) {
       Account.Id authorId;
       try {
@@ -472,12 +478,21 @@
     return new PatchSet.Id(change.getId(), psId);
   }
 
-  private void deleteRef(Change change, Repository repo,
-      ChainedReceiveCommands cmds) throws IOException {
+  private void deleteChangeMetaRef(Change change, ChainedReceiveCommands cmds)
+      throws IOException {
     String refName = changeMetaRef(change.getId());
-    ObjectId old = cmds.getObjectId(repo, refName);
-    if (old != null) {
-      cmds.add(new ReceiveCommand(old, ObjectId.zeroId(), refName));
+    Optional<ObjectId> old = cmds.get(refName);
+    if (old.isPresent()) {
+      cmds.add(new ReceiveCommand(old.get(), ObjectId.zeroId(), refName));
+    }
+  }
+
+  private void deleteDraftRefs(Change change, OpenRepo allUsersRepo)
+      throws IOException {
+    for (Ref r : allUsersRepo.repo.getRefDatabase()
+        .getRefs(RefNames.refsDraftCommentsPrefix(change.getId())).values()) {
+      allUsersRepo.cmds.add(
+          new ReceiveCommand(r.getObjectId(), ObjectId.zeroId(), r.getName()));
     }
   }
 
@@ -700,6 +715,33 @@
     }
   }
 
+  private static class ReviewerEvent extends Event {
+    private Table.Cell<ReviewerStateInternal, Account.Id, Timestamp> reviewer;
+
+    ReviewerEvent(
+        Table.Cell<ReviewerStateInternal, Account.Id, Timestamp> reviewer,
+        Timestamp changeCreatedOn) {
+      super(
+          // Reviewers aren't generally associated with a particular patch set
+          // (although as an implementation detail they were in ReviewDb). Just
+          // use the latest patch set at the time of the event.
+          null,
+          reviewer.getColumnKey(), reviewer.getValue(), changeCreatedOn, null);
+      this.reviewer = reviewer;
+    }
+
+    @Override
+    boolean uniquePerUpdate() {
+      return false;
+    }
+
+    @Override
+    void apply(ChangeUpdate update) throws IOException, OrmException {
+      checkUpdate(update);
+      update.putReviewer(reviewer.getColumnKey(), reviewer.getRowKey());
+    }
+  }
+
   private static class PatchSetEvent extends Event {
     private final Change change;
     private final PatchSet ps;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 3c7f909..2af7bbf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -95,7 +95,7 @@
   public interface Factory {
     ChangeUpdate create(ChangeControl ctl);
     ChangeUpdate create(ChangeControl ctl, Date when);
-    ChangeUpdate create(ChangeNotes notes, @Nullable Account.Id accountId,
+    ChangeUpdate create(Change change, @Nullable Account.Id accountId,
         PersonIdent authorIdent, Date when,
         Comparator<String> labelNameComparator);
 
@@ -204,12 +204,12 @@
       NoteDbUpdateManager.Factory updateManagerFactory,
       ChangeDraftUpdate.Factory draftUpdateFactory,
       ChangeNoteUtil noteUtil,
-      @Assisted ChangeNotes notes,
+      @Assisted Change change,
       @Assisted @Nullable Account.Id accountId,
       @Assisted PersonIdent authorIdent,
       @Assisted Date when,
       @Assisted Comparator<String> labelNameComparator) {
-    super(migration, noteUtil, serverIdent, anonymousCowardName, notes,
+    super(migration, noteUtil, serverIdent, anonymousCowardName, null, change,
         accountId, authorIdent, when);
     this.accountCache = accountCache;
     this.draftUpdateFactory = draftUpdateFactory;
@@ -328,8 +328,14 @@
   @VisibleForTesting
   ChangeDraftUpdate createDraftUpdateIfNull() {
     if (draftUpdate == null) {
-      draftUpdate =
-          draftUpdateFactory.create(getNotes(), accountId, authorIdent, when);
+      ChangeNotes notes = getNotes();
+      if (notes != null) {
+        draftUpdate =
+            draftUpdateFactory.create(notes, accountId, authorIdent, when);
+      } else {
+        draftUpdate = draftUpdateFactory.create(
+            getChange(), accountId, authorIdent, when);
+      }
     }
     return draftUpdate;
   }
@@ -428,10 +434,13 @@
       // If reading from changes is enabled, then the old ChangeNotes already
       // parsed the revision notes. We can reuse them as long as the ref hasn't
       // advanced.
-      ObjectId idFromNotes =
-          firstNonNull(getNotes().load().getRevision(), ObjectId.zeroId());
-      if (idFromNotes.equals(curr)) {
-        return checkNotNull(getNotes().revisionNoteMap);
+      ChangeNotes notes = getNotes();
+      if (notes != null) {
+        ObjectId idFromNotes =
+            firstNonNull(notes.load().getRevision(), ObjectId.zeroId());
+        if (idFromNotes.equals(curr)) {
+          return checkNotNull(getNotes().revisionNoteMap);
+        }
       }
     }
     NoteMap noteMap = NoteMap.read(rw.getObjectReader(), rw.parseCommit(curr));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
index e325911..74c27bd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.server.git.RepoRefCache;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.assistedinject.Assisted;
@@ -51,7 +52,6 @@
 
   private final Change change;
   private final Account.Id author;
-  private final boolean autoRebuild;
 
   private ImmutableListMultimap<RevId, PatchLineComment> comments;
   private RevisionNoteMap revisionNoteMap;
@@ -69,10 +69,9 @@
       Args args,
       @Assisted Change.Id changeId,
       @Assisted Account.Id author) {
-    super(args, changeId);
+    super(args, changeId, true);
     this.change = null;
     this.author = author;
-    this.autoRebuild = true;
   }
 
   DraftCommentNotes(
@@ -80,10 +79,9 @@
       Change change,
       Account.Id author,
       boolean autoRebuild) {
-    super(args, change.getId());
+    super(args, change.getId(), autoRebuild);
     this.change = change;
     this.author = author;
-    this.autoRebuild = autoRebuild;
   }
 
   RevisionNoteMap getRevisionNoteMap() {
@@ -152,7 +150,8 @@
       NoteDbChangeState state = NoteDbChangeState.parse(change);
       // Only check if this particular user's drafts are up to date, to avoid
       // reading unnecessary refs.
-      if (state == null || !state.areDraftsUpToDate(repo, author)) {
+      if (!NoteDbChangeState.areDraftsUpToDate(
+          state, new RepoRefCache(repo), getChangeId(), author)) {
         return rebuildAndOpen(repo);
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java
index f1912a4..c08bdd8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
+import static com.google.gerrit.reviewdb.client.RefNames.refsDraftComments;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.annotations.VisibleForTesting;
@@ -25,14 +26,13 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDbUtil;
+import com.google.gerrit.server.git.RefCache;
 
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
 
 import java.io.IOException;
 import java.util.Collections;
@@ -141,6 +141,24 @@
     return state;
   }
 
+  public static boolean isChangeUpToDate(@Nullable NoteDbChangeState state,
+      RefCache changeRepoRefs, Change.Id changeId) throws IOException {
+    if (state == null) {
+      return !changeRepoRefs.get(changeMetaRef(changeId)).isPresent();
+    }
+    return state.isChangeUpToDate(changeRepoRefs);
+  }
+
+  public static boolean areDraftsUpToDate(@Nullable NoteDbChangeState state,
+      RefCache draftsRepoRefs, Change.Id changeId, Account.Id accountId)
+      throws IOException {
+    if (state == null) {
+      return !draftsRepoRefs.get(refsDraftComments(changeId, accountId))
+          .isPresent();
+    }
+    return state.areDraftsUpToDate(draftsRepoRefs, accountId);
+  }
+
   public static String toString(ObjectId changeMetaId,
       Map<Account.Id, ObjectId> draftIds) {
     List<Account.Id> accountIds = Lists.newArrayList(draftIds.keySet());
@@ -166,22 +184,22 @@
     this.draftIds = ImmutableMap.copyOf(draftIds);
   }
 
-  public boolean isChangeUpToDate(Repository changeRepo) throws IOException {
-    Ref ref = changeRepo.exactRef(changeMetaRef(changeId));
-    if (ref == null) {
+  public boolean isChangeUpToDate(RefCache changeRepoRefs) throws IOException {
+    Optional<ObjectId> id = changeRepoRefs.get(changeMetaRef(changeId));
+    if (!id.isPresent()) {
       return changeMetaId.equals(ObjectId.zeroId());
     }
-    return ref.getObjectId().equals(changeMetaId);
+    return id.get().equals(changeMetaId);
   }
 
-  public boolean areDraftsUpToDate(Repository draftsRepo, Account.Id accountId)
+  public boolean areDraftsUpToDate(RefCache draftsRepoRefs, Account.Id accountId)
       throws IOException {
-    Ref ref = draftsRepo.exactRef(
-        RefNames.refsDraftComments(changeId, accountId));
-    if (ref == null) {
+    Optional<ObjectId> id =
+        draftsRepoRefs.get(refsDraftComments(changeId, accountId));
+    if (!id.isPresent()) {
       return !draftIds.containsKey(accountId);
     }
-    return ref.getObjectId().equals(draftIds.get(accountId));
+    return id.get().equals(draftIds.get(accountId));
   }
 
   @VisibleForTesting
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
index 5a192f8..edee73a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.notedb;
 
-import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
@@ -33,6 +32,7 @@
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.ChainedReceiveCommands;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gwtorm.server.OrmConcurrencyException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
@@ -82,8 +82,8 @@
       this.close = close;
     }
 
-    ObjectId getObjectId(String refName) throws IOException {
-      return cmds.getObjectId(repo, refName);
+    Optional<ObjectId> getObjectId(String refName) throws IOException {
+      return cmds.get(refName);
     }
 
     @Override
@@ -107,6 +107,7 @@
   private OpenRepo changeRepo;
   private OpenRepo allUsersRepo;
   private Map<Change.Id, NoteDbChangeState.Delta> staged;
+  private boolean checkExpectedState;
 
   @AssistedInject
   NoteDbUpdateManager(GitRepositoryManager repoManager,
@@ -137,6 +138,11 @@
     return this;
   }
 
+  NoteDbUpdateManager setCheckExpectedState(boolean checkExpectedState) {
+    this.checkExpectedState = checkExpectedState;
+    return this;
+  }
+
   OpenRepo getChangeRepo() throws IOException {
     initChangeRepo();
     return changeRepo;
@@ -163,7 +169,7 @@
     Repository repo = repoManager.openRepository(p);
     ObjectInserter ins = repo.newObjectInserter();
     return new OpenRepo(repo, new RevWalk(ins.newReader()), ins,
-        new ChainedReceiveCommands(), true);
+        new ChainedReceiveCommands(repo), true);
   }
 
   private boolean isEmpty() {
@@ -222,6 +228,7 @@
       if (!draftUpdates.isEmpty()) {
         initAllUsersRepo();
       }
+      checkExpectedState();
       addCommands();
 
       Table<Change.Id, Account.Id, ObjectId> allDraftIds = getDraftIds();
@@ -323,6 +330,62 @@
     if (!draftUpdates.isEmpty()) {
       addUpdates(draftUpdates, allUsersRepo);
     }
+    checkExpectedState();
+  }
+
+  private void checkExpectedState() throws OrmException, IOException {
+    if (!checkExpectedState) {
+      return;
+    }
+
+    // Refuse to apply an update unless the state in NoteDb matches the state
+    // claimed in the ref. This means we may have failed a NoteDb ref update,
+    // and it would be incorrect to claim that the ref is up to date after this
+    // pipeline.
+    //
+    // Generally speaking, this case should be rare; in most cases, we should
+    // have detected and auto-fixed the stale state when creating ChangeNotes
+    // that got passed into the ChangeUpdate.
+    for (Collection<ChangeUpdate> us : changeUpdates.asMap().values()) {
+      ChangeUpdate u = us.iterator().next();
+      NoteDbChangeState expectedState = NoteDbChangeState.parse(u.getChange());
+
+      if (expectedState == null) {
+        // No previous state means we haven't previously written NoteDb graphs
+        // for this change yet. This means either:
+        //  - The change is new, and we'll be creating its ref.
+        //  - We short-circuited before adding any commands that update this
+        //    ref, and we won't stage a delta for this change either.
+        // Either way, it is safe to proceed here rather than throwing
+        // OrmConcurrencyException.
+        continue;
+      }
+
+      if (!expectedState.isChangeUpToDate(changeRepo.cmds)) {
+        throw new OrmConcurrencyException(String.format(
+            "cannot apply NoteDb updates for change %s;"
+            + " change meta ref does not match %s",
+            u.getId(), expectedState.getChangeMetaId().name()));
+      }
+    }
+
+    for (Collection<ChangeDraftUpdate> us : draftUpdates.asMap().values()) {
+      ChangeDraftUpdate u = us.iterator().next();
+      NoteDbChangeState expectedState = NoteDbChangeState.parse(u.getChange());
+
+      if (expectedState == null) {
+        continue; // See above.
+      }
+
+      Account.Id accountId = u.getAccountId();
+      if (!expectedState.areDraftsUpToDate(
+          allUsersRepo.cmds, accountId)) {
+        throw new OrmConcurrencyException(String.format(
+            "cannot apply NoteDb updates for change %s;"
+            + " draft ref for account %s does not match %s",
+            u.getId(), accountId, expectedState.getChangeMetaId().name()));
+      }
+    }
   }
 
   private static <U extends AbstractChangeUpdate> void addUpdates(
@@ -331,8 +394,7 @@
     for (Map.Entry<String, Collection<U>> e : all.asMap().entrySet()) {
       String refName = e.getKey();
       Collection<U> updates = e.getValue();
-      ObjectId old = firstNonNull(
-          or.cmds.getObjectId(or.repo, refName), ObjectId.zeroId());
+      ObjectId old = or.cmds.get(refName).or(ObjectId.zeroId());
       // Only actually write to the ref if one of the updates explicitly allows
       // us to do so, i.e. it is known to represent a new change. This avoids
       // writing partial change meta if the change hasn't been backfilled yet.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/AutoMerger.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/AutoMerger.java
index fd02042..d75a553 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/AutoMerger.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/AutoMerger.java
@@ -198,7 +198,6 @@
       builder.finish();
       treeId = dc.writeTree(ins);
     }
-    ins.flush();
 
     return commit(repo, rw, ins, refName, treeId, merge);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
index 4bda245..62c6d5e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
@@ -340,7 +340,7 @@
   public boolean isReviewer(ReviewDb db, @Nullable ChangeData cd)
       throws OrmException {
     if (getUser().isIdentifiedUser()) {
-      Collection<Account.Id> results = changeData(db, cd).reviewers().values();
+      Collection<Account.Id> results = changeData(db, cd).reviewers().all();
       return results.contains(getUser().getAccountId());
     }
     return false;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
index bc4d8f3..e41acbe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
@@ -148,7 +148,9 @@
           case FAST_FORWARD:
           case NEW:
           case NO_CHANGE:
-            referenceUpdated.fire(name.getParentKey(), u, ReceiveCommand.Type.CREATE);
+            referenceUpdated.fire(
+                name.getParentKey(), u, ReceiveCommand.Type.CREATE,
+                identifiedUser.get().getAccount());
             hooks.doRefUpdatedHook(name, u, identifiedUser.get().getAccount());
             break;
           case LOCK_FAILURE:
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
index a73ed0e..ecc618e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
@@ -370,7 +370,8 @@
         Result result = ru.update();
         switch (result) {
           case NEW:
-            referenceUpdated.fire(project, ru, ReceiveCommand.Type.CREATE);
+            referenceUpdated.fire(project, ru, ReceiveCommand.Type.CREATE,
+                currentUser.get().getAccountId());
             break;
           case FAST_FORWARD:
           case FORCED:
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
index b4c25b4..43e1422 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
@@ -108,7 +108,8 @@
         case NO_CHANGE:
         case FAST_FORWARD:
         case FORCED:
-          referenceUpdated.fire(rsrc.getNameKey(), u, ReceiveCommand.Type.DELETE);
+          referenceUpdated.fire(rsrc.getNameKey(), u, ReceiveCommand.Type.DELETE,
+              identifiedUser.get().getAccount());
           hooks.doRefUpdatedHook(rsrc.getBranchKey(), u, identifiedUser.get().getAccount());
           break;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java
index b851f9e..daecc1d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java
@@ -162,7 +162,8 @@
   }
 
   private void postDeletion(ProjectResource project, ReceiveCommand cmd) {
-    referenceUpdated.fire(project.getNameKey(), cmd);
+    referenceUpdated.fire(project.getNameKey(), cmd,
+        identifiedUser.get().getAccount());
     Branch.NameKey branchKey =
         new Branch.NameKey(project.getNameKey(), cmd.getRefName());
     hooks.doRefUpdatedHook(branchKey, cmd.getOldId(), cmd.getNewId(),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetAccess.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetAccess.java
index b8c5fd8..be6f892 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetAccess.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetAccess.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.server.project;
 
-import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.BiMap;
+import com.google.common.collect.ImmutableBiMap;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.Maps;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
@@ -53,16 +53,14 @@
 @Singleton
 public class GetAccess implements RestReadView<ProjectResource> {
 
-  private static final ImmutableMap<PermissionRule.Action, PermissionRuleInfo.Action> ACTION_TYPE =
-      Maps.immutableEnumMap(
-          new ImmutableMap.Builder<PermissionRule.Action, PermissionRuleInfo.Action>()
-              .put(PermissionRule.Action.ALLOW, PermissionRuleInfo.Action.ALLOW)
-              .put(PermissionRule.Action.BATCH, PermissionRuleInfo.Action.BATCH)
-              .put(PermissionRule.Action.BLOCK, PermissionRuleInfo.Action.BLOCK)
-              .put(PermissionRule.Action.DENY, PermissionRuleInfo.Action.DENY)
-              .put(PermissionRule.Action.INTERACTIVE,
-                  PermissionRuleInfo.Action.INTERACTIVE)
-              .build());
+  public static final BiMap<PermissionRule.Action,
+      PermissionRuleInfo.Action> ACTION_TYPE = ImmutableBiMap.of(
+          PermissionRule.Action.ALLOW, PermissionRuleInfo.Action.ALLOW,
+          PermissionRule.Action.BATCH, PermissionRuleInfo.Action.BATCH,
+          PermissionRule.Action.BLOCK, PermissionRuleInfo.Action.BLOCK,
+          PermissionRule.Action.DENY, PermissionRuleInfo.Action.DENY,
+          PermissionRule.Action.INTERACTIVE,
+          PermissionRuleInfo.Action.INTERACTIVE);
 
   private final Provider<CurrentUser> self;
   private final GroupControl.Factory groupControlFactory;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
index 469312d..3d0ff05c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
@@ -42,10 +42,12 @@
     put(PROJECT_KIND).to(PutProject.class);
     get(PROJECT_KIND).to(GetProject.class);
     get(PROJECT_KIND, "description").to(GetDescription.class);
-    get(PROJECT_KIND, "access").to(GetAccess.class);
     put(PROJECT_KIND, "description").to(PutDescription.class);
     delete(PROJECT_KIND, "description").to(PutDescription.class);
 
+    get(PROJECT_KIND, "access").to(GetAccess.class);
+    post(PROJECT_KIND, "access").to(SetAccess.class);
+
     get(PROJECT_KIND, "parent").to(GetParent.class);
     put(PROJECT_KIND, "parent").to(SetParent.class);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
index 29f97fb..28cb5b4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
@@ -27,8 +27,10 @@
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.data.SubscribeSection;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.rules.PrologEnvironment;
@@ -475,6 +477,15 @@
     return null;
   }
 
+  public Collection<SubscribeSection> getSubscribeSections(
+      Branch.NameKey branch) {
+    Collection<SubscribeSection> ret = new ArrayList<>();
+    for (ProjectState s : tree()) {
+      ret.addAll(s.getConfig().getSubscribeSections(branch));
+    }
+    return ret;
+  }
+
   public ThemeInfo getTheme() {
     ThemeInfo theme = this.theme;
     if (theme == null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
index 86f98ff..4b0ecf0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
@@ -196,7 +196,7 @@
         // Only fire hook if project was actually changed.
         if (!Objects.equals(baseRev, commitRev)) {
           gitRefUpdated.fire(projectName, RefNames.REFS_CONFIG,
-              baseRev, commitRev);
+              baseRev, commitRev, user.get().asIdentifiedUser().getAccount());
           hooks.doRefUpdatedHook(
             new Branch.NameKey(projectName, RefNames.REFS_CONFIG),
             baseRev, commitRev, user.get().asIdentifiedUser().getAccount());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutDescription.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutDescription.java
index 7cf426d..b136821 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutDescription.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutDescription.java
@@ -94,7 +94,7 @@
       // Only fire hook if project was actually changed.
       if (!Objects.equals(baseRev, commitRev)) {
         gitRefUpdated.fire(resource.getNameKey(), RefNames.REFS_CONFIG,
-            baseRev, commitRev);
+            baseRev, commitRev, user.getAccount());
         hooks.doRefUpdatedHook(
           new Branch.NameKey(resource.getNameKey(), RefNames.REFS_CONFIG),
           baseRev, commitRev, user.getAccount());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccess.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccess.java
new file mode 100644
index 0000000..258b386
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccess.java
@@ -0,0 +1,328 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Strings;
+import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.errors.InvalidNameException;
+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.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.group.GroupsCollection;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+@Singleton
+public class SetAccess implements
+    RestModifyView<ProjectResource, ProjectAccessInput> {
+  protected final GroupBackend groupBackend;
+  private final GroupsCollection groupsCollection;
+  private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
+  private final AllProjectsName allProjects;
+  private final Provider<SetParent> setParent;
+  private final ChangeHooks hooks;
+  private final GitReferenceUpdated gitRefUpdated;
+  private final GetAccess getAccess;
+  private final ProjectCache projectCache;
+  private final Provider<IdentifiedUser> identifiedUser;
+
+  @Inject
+  private SetAccess(GroupBackend groupBackend,
+      Provider<MetaDataUpdate.User> metaDataUpdateFactory,
+      AllProjectsName allProjects,
+      Provider<SetParent> setParent,
+      ChangeHooks hooks,
+      GitReferenceUpdated gitRefUpdated,
+      GroupsCollection groupsCollection,
+      ProjectCache projectCache,
+      GetAccess getAccess,
+      Provider<IdentifiedUser> identifiedUser) {
+    this.groupBackend = groupBackend;
+    this.metaDataUpdateFactory = metaDataUpdateFactory;
+    this.allProjects = allProjects;
+    this.setParent = setParent;
+    this.groupsCollection = groupsCollection;
+    this.hooks = hooks;
+    this.gitRefUpdated = gitRefUpdated;
+    this.getAccess = getAccess;
+    this.projectCache = projectCache;
+    this.identifiedUser = identifiedUser;
+  }
+
+  @Override
+  public ProjectAccessInfo apply(ProjectResource rsrc,
+      ProjectAccessInput input)
+      throws ResourceNotFoundException, ResourceConflictException,
+      IOException, AuthException, BadRequestException,
+      UnprocessableEntityException{
+    List<AccessSection> removals = getAccessSections(input.remove);
+    List<AccessSection> additions = getAccessSections(input.add);
+    MetaDataUpdate.User metaDataUpdateUser = metaDataUpdateFactory.get();
+
+    ProjectControl projectControl = rsrc.getControl();
+    ProjectConfig config;
+    ObjectId base;
+
+    Project.NameKey newParentProjectName = input.parent == null ?
+        null : new Project.NameKey(input.parent);
+
+    try (MetaDataUpdate md = metaDataUpdateUser.create(rsrc.getNameKey())) {
+      config = ProjectConfig.read(md);
+      base = config.getRevision();
+
+      // Perform removal checks
+      for (AccessSection section : removals) {
+        boolean isGlobalCapabilities =
+            AccessSection.GLOBAL_CAPABILITIES.equals(section.getName());
+
+        if (isGlobalCapabilities) {
+          checkGlobalCapabilityPermissions(config.getName());
+        } else if (!projectControl.controlForRef(section.getName()).isOwner()) {
+          throw new AuthException("You are not allowed to edit permissions"
+              + "for ref: " + section.getName());
+        }
+      }
+      // Perform addition checks
+      for (AccessSection section : additions) {
+        String name = section.getName();
+        boolean isGlobalCapabilities =
+            AccessSection.GLOBAL_CAPABILITIES.equals(name);
+
+        if (isGlobalCapabilities) {
+          checkGlobalCapabilityPermissions(config.getName());
+        } else {
+          if (!AccessSection.isValid(name)) {
+            throw new BadRequestException("invalid section name");
+          }
+          if (!projectControl.controlForRef(name).isOwner()) {
+            throw new AuthException("You are not allowed to edit permissions"
+                + "for ref: " + name);
+          }
+          RefControl.validateRefPattern(name);
+        }
+
+        // Check all permissions for soundness
+        for (Permission p : section.getPermissions()) {
+          if (isGlobalCapabilities
+              && !GlobalCapability.isCapability(p.getName())) {
+            throw new BadRequestException("Cannot add non-global capability "
+                + p.getName() + " to global capabilities");
+          }
+        }
+      }
+
+      // Apply removals
+      for (AccessSection section : removals) {
+        if (section.getPermissions().isEmpty()) {
+          // Remove entire section
+          config.remove(config.getAccessSection(section.getName()));
+        }
+        // Remove specific permissions
+        for (Permission p : section.getPermissions()) {
+          if (p.getRules().isEmpty()) {
+            config.remove(config.getAccessSection(section.getName()), p);
+          } else {
+            for (PermissionRule r : p.getRules()) {
+              config.remove(config.getAccessSection(section.getName()), p, r);
+            }
+          }
+        }
+      }
+
+      // Apply additions
+      for (AccessSection section : additions) {
+        AccessSection currentAccessSection =
+            config.getAccessSection(section.getName());
+
+        if (currentAccessSection == null) {
+          // Add AccessSection
+          config.replace(section);
+        } else {
+          for (Permission p : section.getPermissions()) {
+            Permission currentPermission =
+                currentAccessSection.getPermission(p.getName());
+            if (currentPermission == null) {
+              // Add Permission
+              currentAccessSection.addPermission(p);
+            } else {
+              for (PermissionRule r : p.getRules()) {
+                // AddPermissionRule
+                currentPermission.add(r);
+              }
+            }
+          }
+        }
+      }
+
+      if (newParentProjectName != null &&
+          !config.getProject().getNameKey().equals(allProjects) &&
+          !config.getProject().getParent(allProjects)
+              .equals(newParentProjectName)) {
+        try {
+          setParent.get().validateParentUpdate(projectControl,
+              MoreObjects.firstNonNull(newParentProjectName, allProjects).get(),
+              true);
+        } catch (UnprocessableEntityException e) {
+          throw new ResourceConflictException(e.getMessage(), e);
+        }
+        config.getProject().setParentName(newParentProjectName);
+      }
+
+      if (!Strings.isNullOrEmpty(input.message)) {
+        if (!input.message.endsWith("\n")) {
+          input.message += "\n";
+        }
+        md.setMessage(input.message);
+      } else {
+        md.setMessage("Modify access rules\n");
+      }
+
+      updateProjectConfig(projectControl.getUser(), config, md, base);
+    } catch (InvalidNameException e) {
+      throw new BadRequestException(e.toString());
+    } catch (ConfigInvalidException e) {
+      throw new ResourceConflictException(rsrc.getName());
+    }
+
+    return getAccess.apply(rsrc.getNameKey());
+  }
+
+  private List<AccessSection> getAccessSections(
+      Map<String, AccessSectionInfo> sectionInfos)
+      throws UnprocessableEntityException {
+    List<AccessSection> sections = new LinkedList<>();
+    if (sectionInfos == null) {
+      return sections;
+    }
+
+    for (Map.Entry<String, AccessSectionInfo> entry :
+      sectionInfos.entrySet()) {
+      AccessSection accessSection = new AccessSection(entry.getKey());
+
+      if (entry.getValue().permissions == null) {
+        continue;
+      }
+
+      for (Map.Entry<String, PermissionInfo> permissionEntry : entry
+          .getValue().permissions
+          .entrySet()) {
+        Permission p = new Permission(permissionEntry.getKey());
+        if (permissionEntry.getValue().exclusive != null) {
+          p.setExclusiveGroup(permissionEntry.getValue().exclusive);
+        }
+
+        if (permissionEntry.getValue().rules == null) {
+          continue;
+        }
+        for (Map.Entry<String, PermissionRuleInfo> permissionRuleInfoEntry :
+            permissionEntry.getValue().rules.entrySet()) {
+          PermissionRuleInfo pri = permissionRuleInfoEntry.getValue();
+
+          GroupDescription.Basic group = groupsCollection
+              .parseId(permissionRuleInfoEntry.getKey());
+          if (group == null) {
+            throw new UnprocessableEntityException(
+              permissionRuleInfoEntry.getKey() + " is not a valid group ID");
+          }
+          PermissionRule r = new PermissionRule(
+              GroupReference.forGroup(group));
+          if (pri != null) {
+            if (pri.max != null) {
+              r.setMax(pri.max);
+            }
+            if (pri.min != null) {
+              r.setMin(pri.min);
+            }
+            r.setAction(GetAccess.ACTION_TYPE.inverse().get(pri.action));
+            r.setForce(pri.force);
+          }
+          p.add(r);
+        }
+        accessSection.getPermissions().add(p);
+      }
+      sections.add(accessSection);
+    }
+    return sections;
+  }
+
+  private void updateProjectConfig(CurrentUser user,
+      ProjectConfig config, MetaDataUpdate md, ObjectId base)
+      throws IOException {
+    RevCommit commit = config.commit(md);
+
+    Account account = user.isIdentifiedUser()
+        ? user.asIdentifiedUser().getAccount()
+        : null;
+    gitRefUpdated.fire(config.getProject().getNameKey(), RefNames.REFS_CONFIG,
+        base, commit.getId(), account);
+
+    projectCache.evict(config.getProject());
+
+    hooks.doRefUpdatedHook(
+        new Branch.NameKey(config.getProject().getNameKey(),
+            RefNames.REFS_CONFIG),
+        base, commit.getId(), user.asIdentifiedUser().getAccount());
+  }
+
+  private void checkGlobalCapabilityPermissions(Project.NameKey projectName)
+    throws BadRequestException, AuthException {
+
+    if (!allProjects.equals(projectName)) {
+      throw new BadRequestException("Cannot edit global capabilities "
+        + "for projects other than " + allProjects.get());
+    }
+
+    if (!identifiedUser.get().getCapabilities().canAdministrateServer()) {
+      throw new AuthException("Editing global capabilities "
+        + "requires " + GlobalCapability.ADMINISTRATE_SERVER);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index 5eed391..93a16ad 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -30,7 +30,6 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Multimap;
-import com.google.common.collect.SetMultimap;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitTypeRecord;
@@ -50,13 +49,13 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchLineCommentsUtil;
 import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.change.MergeabilityCache;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.NotesMigration;
-import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListEntry;
@@ -844,7 +843,7 @@
    * @throws OrmException an error occurred reading the database.
    */
   public Collection<PatchSet> visiblePatchSets() throws OrmException {
-    return FluentIterable.from(patchSets()).filter(new Predicate<PatchSet>() {
+    Predicate<PatchSet> predicate = new Predicate<PatchSet>() {
       @Override
       public boolean apply(PatchSet input) {
         try {
@@ -852,7 +851,9 @@
         } catch (OrmException e) {
           return false;
         }
-      }}).toList();
+      }
+    };
+    return FluentIterable.from(patchSets()).filter(predicate).toList();
   }
 
 public void setPatchSets(Collection<PatchSet> patchSets) {
@@ -903,8 +904,7 @@
     return Optional.absent();
   }
 
-  public SetMultimap<ReviewerStateInternal, Account.Id> reviewers()
-      throws OrmException {
+  public ReviewerSet reviewers() throws OrmException {
     return approvalsUtil.getReviewers(notes(), approvals().values());
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 07c8c72..590be32 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.query.change;
 
+import static com.google.gerrit.reviewdb.client.Change.CHANGE_ID_PATTERN;
 import static com.google.gerrit.server.query.change.ChangeData.asChanges;
 
 import com.google.common.annotations.VisibleForTesting;
@@ -94,8 +95,7 @@
   }
 
   private static final Pattern PAT_LEGACY_ID = Pattern.compile("^[1-9][0-9]*$");
-  private static final Pattern PAT_CHANGE_ID =
-      Pattern.compile("^[iI][0-9a-f]{4,}.*$");
+  private static final Pattern PAT_CHANGE_ID = Pattern.compile(CHANGE_ID_PATTERN);
   private static final Pattern DEF_CHANGE = Pattern.compile(
       "^(?:[1-9][0-9]*|(?:[^~]+~[^~]+~)?[iI][0-9a-f]{4,}.*)$");
 
@@ -301,7 +301,7 @@
       } catch (ProvisionException e) {
         // Doesn't match current user, continue.
       }
-      return asUser(userFactory.create(db, otherId));
+      return asUser(userFactory.create(otherId));
     }
 
     IdentifiedUser getIdentifiedUser() throws QueryParseException {
@@ -736,7 +736,7 @@
     if (!m.isEmpty()) {
       List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(m.size());
       for (Account.Id id : m) {
-        return visibleto(args.userFactory.create(args.db, id));
+        return visibleto(args.userFactory.create(id));
       }
       return Predicate.or(p);
     }
@@ -791,7 +791,7 @@
     if (g == null) {
       throw error("Group " + group + " not found");
     }
-    return new OwnerinPredicate(args.db, args.userFactory, g.getUUID());
+    return new OwnerinPredicate(args.userFactory, g.getUUID());
   }
 
   @Operator
@@ -818,7 +818,7 @@
     if (g == null) {
       throw error("Group " + group + " not found");
     }
-    return new ReviewerinPredicate(args.db, args.userFactory, g.getUUID());
+    return new ReviewerinPredicate(args.userFactory, g.getUUID());
   }
 
   @Operator
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
index ff9c853..b01fdbe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
@@ -112,7 +112,7 @@
     if (psVal == expVal) {
       // Double check the value is still permitted for the user.
       //
-      IdentifiedUser reviewer = userFactory.create(dbProvider, approver);
+      IdentifiedUser reviewer = userFactory.create(approver);
       try {
         ChangeControl cc =
             ccFactory.controlFor(dbProvider.get(), change, reviewer);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
index bb5f8ff..4a8e71b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.query.change;
 
+import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.server.CurrentUser;
@@ -21,11 +22,22 @@
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryBuilder;
 import com.google.gerrit.server.query.QueryParseException;
+import com.google.gwtorm.server.OrmException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 class IsWatchedByPredicate extends AndPredicate<ChangeData> {
+  private static final Logger log =
+      LoggerFactory.getLogger(IsWatchedByPredicate.class);
+
+  private static final CurrentUser.PropertyKey<List<AccountProjectWatch>> PROJECT_WATCHES =
+      CurrentUser.PropertyKey.create();
+
   private static String describe(CurrentUser user) {
     if (user.isIdentifiedUser()) {
       return user.getAccountId().toString();
@@ -44,10 +56,9 @@
   private static List<Predicate<ChangeData>> filters(
       ChangeQueryBuilder.Arguments args,
       boolean checkIsVisible) throws QueryParseException {
-    CurrentUser user = args.getUser();
     List<Predicate<ChangeData>> r = new ArrayList<>();
     ChangeQueryBuilder builder = new ChangeQueryBuilder(args);
-    for (AccountProjectWatch w : user.getNotificationFilters()) {
+    for (AccountProjectWatch w : getWatches(args)) {
       Predicate<ChangeData> f = null;
       if (w.getFilter() != null) {
         try {
@@ -90,6 +101,24 @@
     }
   }
 
+  private static List<AccountProjectWatch> getWatches(
+      ChangeQueryBuilder.Arguments args) throws QueryParseException {
+    CurrentUser user = args.getUser();
+    List<AccountProjectWatch> watches = user.get(PROJECT_WATCHES);
+    if (watches == null && user.isIdentifiedUser()) {
+      try {
+        watches = args.db.get().accountProjectWatches()
+            .byAccount(user.asIdentifiedUser().getAccountId()).toList();
+        user.put(PROJECT_WATCHES, watches);
+      } catch (OrmException e) {
+        log.warn("Cannot load accountProjectWatches", e);
+      }
+    }
+    return MoreObjects.firstNonNull(
+        watches,
+        Collections.<AccountProjectWatch> emptyList());
+  }
+
   private static List<Predicate<ChangeData>> none() {
     Predicate<ChangeData> any = any();
     return ImmutableList.of(not(any));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerinPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerinPredicate.java
index a0c1235..467e4c5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerinPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerinPredicate.java
@@ -16,21 +16,17 @@
 
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.query.OperatorPredicate;
 import com.google.gwtorm.server.OrmException;
-import com.google.inject.Provider;
 
 class OwnerinPredicate extends OperatorPredicate<ChangeData> {
-  private final Provider<ReviewDb> dbProvider;
   private final IdentifiedUser.GenericFactory userFactory;
   private final AccountGroup.UUID uuid;
 
-  OwnerinPredicate(Provider<ReviewDb> dbProvider,
-    IdentifiedUser.GenericFactory userFactory, AccountGroup.UUID uuid) {
+  OwnerinPredicate(IdentifiedUser.GenericFactory userFactory,
+    AccountGroup.UUID uuid) {
     super(ChangeQueryBuilder.FIELD_OWNERIN, uuid.toString());
-    this.dbProvider = dbProvider;
     this.userFactory = userFactory;
     this.uuid = uuid;
   }
@@ -45,8 +41,7 @@
     if (change == null) {
       return false;
     }
-    final IdentifiedUser owner = userFactory.create(dbProvider,
-      change.getOwner());
+    final IdentifiedUser owner = userFactory.create(change.getOwner());
     return owner.getEffectiveGroups().contains(uuid);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
index eb07250..2da8c54 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
@@ -40,7 +40,7 @@
         object.change().getStatus() == Change.Status.DRAFT) {
       return false;
     }
-    for (Account.Id accountId : object.reviewers().values()) {
+    for (Account.Id accountId : object.reviewers().all()) {
       if (id.equals(accountId)) {
         return true;
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerinPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerinPredicate.java
index a29ac62..76a02432 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerinPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerinPredicate.java
@@ -16,21 +16,17 @@
 
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.query.OperatorPredicate;
 import com.google.gwtorm.server.OrmException;
-import com.google.inject.Provider;
 
 class ReviewerinPredicate extends OperatorPredicate<ChangeData> {
-  private final Provider<ReviewDb> dbProvider;
   private final IdentifiedUser.GenericFactory userFactory;
   private final AccountGroup.UUID uuid;
 
-  ReviewerinPredicate(Provider<ReviewDb> dbProvider,
-    IdentifiedUser.GenericFactory userFactory, AccountGroup.UUID uuid) {
+  ReviewerinPredicate(IdentifiedUser.GenericFactory userFactory,
+    AccountGroup.UUID uuid) {
     super(ChangeQueryBuilder.FIELD_REVIEWERIN, uuid.toString());
-    this.dbProvider = dbProvider;
     this.userFactory = userFactory;
     this.uuid = uuid;
   }
@@ -41,8 +37,8 @@
 
   @Override
   public boolean match(final ChangeData object) throws OrmException {
-    for (Account.Id accountId : object.reviewers().values()) {
-      IdentifiedUser reviewer = userFactory.create(dbProvider, accountId);
+    for (Account.Id accountId : object.reviewers().all()) {
+      IdentifiedUser reviewer = userFactory.create(accountId);
       if (reviewer.getEffectiveGroups().contains(uuid)) {
         return true;
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
index 4b71719..ca7c990 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
@@ -15,14 +15,12 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.account.ListGroupMembership;
 
-import java.util.Collection;
 import java.util.Collections;
 import java.util.Set;
 
@@ -49,9 +47,4 @@
   public Set<Change.Id> getStarredChanges() {
     return Collections.emptySet();
   }
-
-  @Override
-  public Collection<AccountProjectWatch> getNotificationFilters() {
-    return Collections.emptySet();
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/SubmoduleSectionParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/SubmoduleSectionParser.java
index 907ef70..6b5c991 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/SubmoduleSectionParser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/SubmoduleSectionParser.java
@@ -17,11 +17,8 @@
 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.project.ProjectCache;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
 
-import org.eclipse.jgit.lib.BlobBasedConfig;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
 
 import java.net.URI;
@@ -48,24 +45,15 @@
  */
 public class SubmoduleSectionParser {
 
-  public interface Factory {
-    SubmoduleSectionParser create(BlobBasedConfig bbc, String thisServer,
-        Branch.NameKey superProjectBranch);
-  }
-
-  private final ProjectCache projectCache;
-  private final BlobBasedConfig bbc;
-  private final String thisServer;
+  private final Config bbc;
+  private final String canonicalWebUrl;
   private final Branch.NameKey superProjectBranch;
 
-  @Inject
-  public SubmoduleSectionParser(ProjectCache projectCache,
-      @Assisted BlobBasedConfig bbc,
-      @Assisted String thisServer,
-      @Assisted Branch.NameKey superProjectBranch) {
-    this.projectCache = projectCache;
+  public SubmoduleSectionParser(Config bbc,
+      String canonicalWebUrl,
+      Branch.NameKey superProjectBranch) {
     this.bbc = bbc;
-    this.thisServer = thisServer;
+    this.canonicalWebUrl = canonicalWebUrl;
     this.superProjectBranch = superProjectBranch;
   }
 
@@ -84,51 +72,74 @@
     final String url = bbc.getString("submodule", id, "url");
     final String path = bbc.getString("submodule", id, "path");
     String branch = bbc.getString("submodule", id, "branch");
-    SubmoduleSubscription ss = null;
 
     try {
       if (url != null && url.length() > 0 && path != null && path.length() > 0
           && branch != null && branch.length() > 0) {
         // All required fields filled.
+        String project;
 
-        boolean urlIsRelative = url.startsWith("../");
-        String server = null;
-        if (!urlIsRelative) {
+        if (branch.equals(".")) {
+          branch = superProjectBranch.get();
+        }
+
+        // relative URL
+        if (url.startsWith("../")) {
+          // prefix with a slash for easier relative path walks
+          project = '/' + superProjectBranch.getParentKey().get();
+          String hostPart = url;
+          while (hostPart.startsWith("../")) {
+            int lastSlash = project.lastIndexOf('/');
+            if (lastSlash < 0) {
+              // too many levels up, ignore for now
+              return null;
+            }
+            project = project.substring(0, lastSlash);
+            hostPart = hostPart.substring(3);
+          }
+          project = project + "/" + hostPart;
+
+          // remove leading '/'
+          project = project.substring(1);
+        } else {
           // It is actually an URI. It could be ssh://localhost/project-a.
-          server = new URI(url).getHost();
-        }
-        if ((urlIsRelative)
-            || (server != null && server.equalsIgnoreCase(thisServer))) {
-          // Subscription really related to this running server.
-          if (branch.equals(".")) {
-            branch = superProjectBranch.get();
+          URI targetServerURI = new URI(url);
+          URI thisServerURI = new URI(canonicalWebUrl);
+          String thisHost = thisServerURI.getHost();
+          String targetHost = targetServerURI.getHost();
+          if (thisHost == null || targetHost == null ||
+              !targetHost.equalsIgnoreCase(thisHost)) {
+            return null;
           }
-
-          final String urlExtractedPath = new URI(url).getPath();
-          String projectName;
-          int fromIndex = urlExtractedPath.length() - 1;
-          while (fromIndex > 0) {
-            fromIndex = urlExtractedPath.lastIndexOf('/', fromIndex - 1);
-            projectName = urlExtractedPath.substring(fromIndex + 1);
-
-            if (projectName.endsWith(Constants.DOT_GIT_EXT)) {
-              projectName = projectName.substring(0, //
-                  projectName.length() - Constants.DOT_GIT_EXT.length());
-            }
-            Project.NameKey projectKey = new Project.NameKey(projectName);
-            if (projectCache.get(projectKey) != null) {
-              ss = new SubmoduleSubscription(
-                  superProjectBranch,
-                  new Branch.NameKey(new Project.NameKey(projectName), branch),
-                  path);
-            }
+          String p1 = targetServerURI.getPath();
+          String p2 = thisServerURI.getPath();
+          if (!p1.startsWith(p2)) {
+            // When we are running the server at
+            // http://server/my-gerrit/ but the subscription is for
+            // http://server/other-teams-gerrit/
+            return null;
           }
+          // skip common part
+          project = p1.substring(p2.length());
         }
+
+        while (project.startsWith("/")) {
+          project = project.substring(1);
+        }
+
+        if (project.endsWith(Constants.DOT_GIT_EXT)) {
+          project = project.substring(0, //
+              project.length() - Constants.DOT_GIT_EXT.length());
+        }
+        Project.NameKey projectKey = new Project.NameKey(project);
+        return new SubmoduleSubscription(
+            superProjectBranch,
+            new Branch.NameKey(projectKey, branch),
+            path);
       }
     } catch (URISyntaxException e) {
       // Error in url syntax (in fact it is uri syntax)
     }
-
-    return ss;
+    return null;
   }
 }
diff --git a/gerrit-server/src/main/java/gerrit/PRED_current_user_2.java b/gerrit-server/src/main/java/gerrit/PRED_current_user_2.java
index 3ee8d82..87c7138 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_current_user_2.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_current_user_2.java
@@ -17,12 +17,10 @@
 import static com.googlecode.prolog_cafe.lang.SymbolTerm.intern;
 
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.rules.PrologEnvironment;
 import com.google.gerrit.rules.StoredValues;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.inject.util.Providers;
 
 import com.googlecode.prolog_cafe.exceptions.IllegalTypeException;
 import com.googlecode.prolog_cafe.exceptions.PInstantiationException;
@@ -90,14 +88,8 @@
       Account.Id accountId = new Account.Id(((IntegerTerm) idTerm).intValue());
       user = cache.get(accountId);
       if (user == null) {
-        ReviewDb db = StoredValues.REVIEW_DB.getOrNull(engine);
         IdentifiedUser.GenericFactory userFactory = userFactory(engine);
-        IdentifiedUser who;
-        if (db != null) {
-          who = userFactory.create(Providers.of(db), accountId);
-        } else {
-          who = userFactory.create(accountId);
-        }
+        IdentifiedUser who = userFactory.create(accountId);
         cache.put(accountId, who);
         user = who;
       }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/LabelNormalizerTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/LabelNormalizerTest.java
index 6b6528e..aa23e50 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/LabelNormalizerTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/LabelNormalizerTest.java
@@ -90,7 +90,7 @@
     schemaCreator.create(db);
     userId = accountManager.authenticate(AuthRequest.forUser("user"))
         .getAccountId();
-    user = userFactory.create(Providers.of(db), userId);
+    user = userFactory.create(userId);
 
     requestContext.setContext(new RequestContext() {
       @Override
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeBundleTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeBundleTest.java
index ea1120f..c093b75 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeBundleTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeBundleTest.java
@@ -14,14 +14,19 @@
 
 package com.google.gerrit.server.notedb;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.common.TimeUtil.roundToSecond;
 import static com.google.gerrit.server.notedb.ChangeBundle.Source.NOTE_DB;
 import static com.google.gerrit.server.notedb.ChangeBundle.Source.REVIEW_DB;
+import static com.google.gerrit.server.notedb.ReviewerStateInternal.CC;
+import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
+import com.google.common.collect.HashBasedTable;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Table;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
@@ -33,6 +38,7 @@
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.testutil.TestChanges;
 import com.google.gerrit.testutil.TestTimeUtil;
 import com.google.gwtorm.client.KeyUtil;
@@ -113,9 +119,9 @@
     Change c2 = TestChanges.newChange(project, accountId);
     int id2 = c2.getId().get();
     ChangeBundle b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     ChangeBundle b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
 
     assertDiffs(b1, b2,
         "changeId differs for Changes: {" + id1 + "} != {" + id2 + "}",
@@ -131,9 +137,9 @@
         new Project.NameKey("project"), new Account.Id(100));
     Change c2 = clone(c1);
     ChangeBundle b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     ChangeBundle b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
 
     assertNoDiffs(b1, b2);
 
@@ -153,9 +159,9 @@
 
     // Both are ReviewDb, exact timestamp match is required.
     ChangeBundle b1 = new ChangeBundle(c1, messages(), patchSets(),
-        approvals(), comments(), REVIEW_DB);
+        approvals(), comments(), reviewers(), REVIEW_DB);
     ChangeBundle b2 = new ChangeBundle(c2, messages(), patchSets(),
-        approvals(), comments(), REVIEW_DB);
+        approvals(), comments(), reviewers(), REVIEW_DB);
     assertDiffs(b1, b2,
         "createdOn differs for Change.Id " + c1.getId() + ":"
             + " {2009-09-30 17:00:01.0} != {2009-09-30 17:00:02.0}",
@@ -164,9 +170,9 @@
 
     // One NoteDb, slop is allowed.
     b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(),
-        comments(), NOTE_DB);
+        comments(), reviewers(), NOTE_DB);
     b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     assertNoDiffs(b1, b2);
     assertNoDiffs(b2, b1);
 
@@ -175,9 +181,9 @@
     Change c3 = clone(c1);
     c3.setLastUpdatedOn(TimeUtil.nowTs());
     b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(),
-        comments(), NOTE_DB);
+        comments(), reviewers(), NOTE_DB);
     ChangeBundle b3 = new ChangeBundle(c3, messages(), patchSets(), approvals(),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     String msg = "effective last updated time differs for Change.Id "
         + c1.getId() + " in NoteDb vs. ReviewDb:"
         + " {2009-09-30 17:00:01.0} != {2009-09-30 17:00:10.0}";
@@ -197,27 +203,27 @@
 
     // Both ReviewDb, exact match required.
     ChangeBundle b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     ChangeBundle b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     assertDiffs(b1, b2,
         "originalSubject differs for Change.Id " + c1.getId() + ":"
             + " {Original A} != {Original B}");
 
     // Both NoteDb, exact match required.
     b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(), comments(),
-        NOTE_DB);
+        reviewers(), NOTE_DB);
     b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(), comments(),
-        NOTE_DB);
+        reviewers(), NOTE_DB);
     assertDiffs(b1, b2,
         "originalSubject differs for Change.Id " + c1.getId() + ":"
             + " {Original A} != {Original B}");
 
     // One ReviewDb, one NoteDb, original subject is ignored.
     b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(), comments(),
-        REVIEW_DB);
+        reviewers(), REVIEW_DB);
     b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(), comments(),
-        NOTE_DB);
+        reviewers(), NOTE_DB);
     assertNoDiffs(b1, b2);
     assertNoDiffs(b2, b1);
   }
@@ -233,25 +239,25 @@
 
     // Both ReviewDb, exact match required.
     ChangeBundle b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     ChangeBundle b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     assertDiffs(b1, b2,
         "topic differs for Change.Id " + c1.getId() + ":"
             + " {} != {null}");
 
     // Topic ignored if ReviewDb is empty and NoteDb is null.
     b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(), comments(),
-        REVIEW_DB);
+        reviewers(), REVIEW_DB);
     b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(), comments(),
-        NOTE_DB);
+        reviewers(), NOTE_DB);
     assertNoDiffs(b1, b2);
 
     // Exact match still required if NoteDb has empty value (not realistic).
     b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(), comments(),
-        NOTE_DB);
+        reviewers(), NOTE_DB);
     b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(), comments(),
-        REVIEW_DB);
+        reviewers(), REVIEW_DB);
     assertDiffs(b1, b2,
         "topic differs for Change.Id " + c1.getId() + ":"
             + " {} != {null}");
@@ -260,12 +266,60 @@
     Change c3 = clone(c1);
     c3.setTopic("topic");
     b1 = new ChangeBundle(c3, messages(), patchSets(), approvals(), comments(),
-        REVIEW_DB);
+        reviewers(), REVIEW_DB);
     b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(), comments(),
-        NOTE_DB);
+        reviewers(), NOTE_DB);
     assertDiffs(b1, b2,
         "topic differs for Change.Id " + c1.getId() + ":"
             + " {topic} != {null}");
+
+    // Null is equal to a string that is all whitespace.
+    Change c4 = clone(c1);
+    c4.setTopic("  ");
+    b1 = new ChangeBundle(c4, messages(), patchSets(), approvals(), comments(),
+        reviewers(), REVIEW_DB);
+    b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(), comments(),
+        reviewers(), NOTE_DB);
+    assertNoDiffs(b1, b2);
+    assertNoDiffs(b2, b1);
+  }
+
+  @Test
+  public void diffChangesIgnoresLeadingWhitespaceInReviewDbTopics()
+      throws Exception {
+    Change c1 = TestChanges.newChange(
+        new Project.NameKey("project"), new Account.Id(100));
+    c1.setTopic(" abc");
+    Change c2 = clone(c1);
+    c2.setTopic("abc");
+
+    // Both ReviewDb, exact match required.
+    ChangeBundle b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(),
+        comments(), reviewers(), REVIEW_DB);
+    ChangeBundle b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(),
+        comments(), reviewers(), REVIEW_DB);
+    assertDiffs(b1, b2,
+        "topic differs for Change.Id " + c1.getId() + ":"
+            + " { abc} != {abc}");
+
+    // Leading whitespace in ReviewDb topic is ignored.
+    b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(), comments(),
+        reviewers(), REVIEW_DB);
+    b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(), comments(),
+        reviewers(), NOTE_DB);
+    assertNoDiffs(b1, b2);
+    assertNoDiffs(b2, b1);
+
+    // Must match except for the leading whitespace.
+    Change c3 = clone(c1);
+    c3.setTopic("cba");
+    b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(), comments(),
+        reviewers(), REVIEW_DB);
+    b2 = new ChangeBundle(c3, messages(), patchSets(), approvals(), comments(),
+        reviewers(), NOTE_DB);
+    assertDiffs(b1, b2,
+        "topic differs for Change.Id " + c1.getId() + ":"
+            + " { abc} != {cba}");
   }
 
   @Test
@@ -284,16 +338,16 @@
 
     // Both ReviewDb, exact match required.
     ChangeBundle b1 = new ChangeBundle(c1, messages(), patchSets(),
-        approvals(a), comments(), REVIEW_DB);
+        approvals(a), comments(), reviewers(), REVIEW_DB);
     ChangeBundle b2 = new ChangeBundle(c2, messages(), patchSets(),
-        approvals(a), comments(), REVIEW_DB);
+        approvals(a), comments(), reviewers(), REVIEW_DB);
     assertDiffs(b1, b2,
         "effective last updated time differs for Change.Id " + c1.getId() + ":"
             + " {2009-09-30 17:00:00.0} != {2009-09-30 17:00:06.0}");
 
     // NoteDb allows latest timestamp from all entities in bundle.
     b2 = new ChangeBundle(c2, messages(), patchSets(),
-        approvals(a), comments(), NOTE_DB);
+        approvals(a), comments(), reviewers(), NOTE_DB);
     assertNoDiffs(b1, b2);
   }
 
@@ -314,18 +368,18 @@
     // ReviewDb has later lastUpdatedOn timestamp than NoteDb, allowed since
     // NoteDb matches the latest timestamp of a non-Change entity.
     ChangeBundle b1 = new ChangeBundle(c2, messages(), patchSets(),
-        approvals(a), comments(), REVIEW_DB);
+        approvals(a), comments(), reviewers(), REVIEW_DB);
     ChangeBundle b2 = new ChangeBundle(c1, messages(), patchSets(),
-        approvals(a), comments(), NOTE_DB);
+        approvals(a), comments(), reviewers(), NOTE_DB);
     assertThat(b1.getChange().getLastUpdatedOn())
         .isGreaterThan(b2.getChange().getLastUpdatedOn());
     assertNoDiffs(b1, b2);
 
     // Timestamps must actually match if Change is the only entity.
     b1 = new ChangeBundle(c2, messages(), patchSets(), approvals(), comments(),
-        REVIEW_DB);
+        reviewers(), REVIEW_DB);
     b2 = new ChangeBundle(c1, messages(), patchSets(), approvals(), comments(),
-        NOTE_DB);
+        reviewers(), NOTE_DB);
     assertDiffs(b1, b2,
         "effective last updated time differs for Change.Id " + c1.getId()
             + " in NoteDb vs. ReviewDb:"
@@ -344,25 +398,25 @@
 
     // Both ReviewDb, exact match required.
     ChangeBundle b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     ChangeBundle b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     assertDiffs(b1, b2,
         "subject differs for Change.Id " + c1.getId() + ":"
             + " {Change subject} != {Change sub}");
 
     // ReviewDb has shorter subject, allowed.
     b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(),
-        comments(), NOTE_DB);
+        comments(), reviewers(), NOTE_DB);
     b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     assertNoDiffs(b1, b2);
 
     // NoteDb has shorter subject, not allowed.
-    b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(),
-        comments(), REVIEW_DB);
-    b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(),
-        comments(), NOTE_DB);
+    b1 = new ChangeBundle(c1, messages(), latest(c1), approvals(),
+        comments(), reviewers(), REVIEW_DB);
+    b2 = new ChangeBundle(c2, messages(), latest(c2), approvals(),
+        comments(), reviewers(), NOTE_DB);
     assertDiffs(b1, b2,
         "subject differs for Change.Id " + c1.getId() + ":"
             + " {Change subject} != {Change sub}");
@@ -379,18 +433,18 @@
 
     // Both ReviewDb, exact match required.
     ChangeBundle b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     ChangeBundle b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     assertDiffs(b1, b2,
         "subject differs for Change.Id " + c1.getId() + ":"
             + " {Change subject} != {   Change subject}");
 
     // ReviewDb is missing leading spaces, allowed.
     b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(),
-        comments(), NOTE_DB);
+        comments(), reviewers(), NOTE_DB);
     b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     assertNoDiffs(b1, b2);
     assertNoDiffs(b2, b1);
   }
@@ -406,18 +460,18 @@
 
     // Both ReviewDb.
     ChangeBundle b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     ChangeBundle b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     assertDiffs(b1, b2,
         "subject differs for Change.Id " + c1.getId() + ":"
             + " {Change subject} != {\tChange subject}");
 
     // One NoteDb.
-    b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(),
-        comments(), NOTE_DB);
-    b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(),
-        comments(), REVIEW_DB);
+    b1 = new ChangeBundle(c1, messages(), latest(c1), approvals(),
+        comments(), reviewers(), NOTE_DB);
+    b2 = new ChangeBundle(c2, messages(), latest(c2), approvals(),
+        comments(), reviewers(), REVIEW_DB);
     assertDiffs(b1, b2,
         "subject differs for Change.Id " + c1.getId() + ":"
             + " {Change subject} != {\tChange subject}");
@@ -427,6 +481,67 @@
   }
 
   @Test
+  public void diffChangesHandlesBuggyJGitSubjectExtraction() throws Exception {
+    Change c1 = TestChanges.newChange(project, accountId);
+    String buggySubject = "Subject\r \r Rest of message.";
+    c1.setCurrentPatchSet(c1.currentPatchSetId(), buggySubject, buggySubject);
+    Change c2 = clone(c1);
+    c2.setCurrentPatchSet(c2.currentPatchSetId(), "Subject", "Subject");
+
+    // Both ReviewDb.
+    ChangeBundle b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(),
+        comments(), reviewers(), REVIEW_DB);
+    ChangeBundle b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(),
+        comments(), reviewers(), REVIEW_DB);
+    assertDiffs(b1, b2,
+        "originalSubject differs for Change.Id " + c1.getId() + ":"
+            + " {Subject\r \r Rest of message.} != {Subject}",
+        "subject differs for Change.Id " + c1.getId() + ":"
+            + " {Subject\r \r Rest of message.} != {Subject}");
+
+    // NoteDb has correct subject without "\r ".
+    b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(),
+        comments(), reviewers(), REVIEW_DB);
+    b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(),
+        comments(), reviewers(), NOTE_DB);
+    assertNoDiffs(b1, b2);
+    assertNoDiffs(b2, b1);
+  }
+
+  @Test
+  public void diffChangesIgnoresInvalidCurrentPatchSetIdInReviewDb()
+      throws Exception {
+    Change c1 = TestChanges.newChange(
+        new Project.NameKey("project"), new Account.Id(100));
+    Change c2 = clone(c1);
+    c2.setCurrentPatchSet(new PatchSet.Id(c2.getId(), 0), "Unrelated subject",
+        c2.getOriginalSubject());
+
+    // Both ReviewDb.
+    ChangeBundle b1 = new ChangeBundle(c1, messages(), patchSets(),
+        approvals(), comments(), reviewers(), REVIEW_DB);
+    ChangeBundle b2 = new ChangeBundle(c2, messages(), patchSets(),
+        approvals(), comments(), reviewers(), REVIEW_DB);
+    assertDiffs(b1, b2,
+        "currentPatchSetId differs for Change.Id " + c1.getId() + ":"
+            + " {1} != {0}",
+        "subject differs for Change.Id " + c1.getId() + ":"
+            + " {Change subject} != {Unrelated subject}");
+
+    // One NoteDb.
+    //
+    // This is based on a real corrupt change where all patch sets were deleted
+    // but the Change entity stuck around, resulting in a currentPatchSetId of 0
+    // after converting to NoteDb.
+    b1 = new ChangeBundle(c1, messages(), patchSets(), approvals(), comments(),
+        reviewers(), REVIEW_DB);
+    b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(), comments(),
+        reviewers(), NOTE_DB);
+    assertNoDiffs(b1, b2);
+    assertNoDiffs(b2, b1);
+  }
+
+  @Test
   public void diffChangeMessageKeySets() throws Exception {
     Change c = TestChanges.newChange(project, accountId);
     int id = c.getId().get();
@@ -437,9 +552,9 @@
         new ChangeMessage.Key(c.getId(), "uuid2"),
         accountId, TimeUtil.nowTs(), c.currentPatchSetId());
     ChangeBundle b1 = new ChangeBundle(c, messages(cm1), latest(c), approvals(),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     ChangeBundle b2 = new ChangeBundle(c, messages(cm2), latest(c), approvals(),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
 
     assertDiffs(b1, b2,
         "ChangeMessage.Key sets differ:"
@@ -455,9 +570,9 @@
     cm1.setMessage("message 1");
     ChangeMessage cm2 = clone(cm1);
     ChangeBundle b1 = new ChangeBundle(c, messages(cm1), latest(c), approvals(),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     ChangeBundle b2 = new ChangeBundle(c, messages(cm2), latest(c), approvals(),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
 
     assertNoDiffs(b1, b2);
 
@@ -479,9 +594,9 @@
     cm2.getKey().set("uuid2");
 
     ChangeBundle b1 = new ChangeBundle(c, messages(cm1), latest(c), approvals(),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     ChangeBundle b2 = new ChangeBundle(c, messages(cm2), latest(c), approvals(),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     // Both are ReviewDb, exact UUID match is required.
     assertDiffs(b1, b2,
         "ChangeMessage.Key sets differ:"
@@ -489,9 +604,9 @@
 
     // One NoteDb, UUIDs are ignored.
     b1 = new ChangeBundle(c, messages(cm1), latest(c), approvals(), comments(),
-        REVIEW_DB);
+        reviewers(), REVIEW_DB);
     b2 = new ChangeBundle(c, messages(cm2), latest(c), approvals(), comments(),
-        NOTE_DB);
+        reviewers(), NOTE_DB);
     assertNoDiffs(b1, b2);
   }
 
@@ -510,18 +625,18 @@
 
     // Both ReviewDb: Uses same keySet diff as other types.
     ChangeBundle b1 = new ChangeBundle(c, messages(cm1, cm2), latest(c),
-        approvals(), comments(), REVIEW_DB);
+        approvals(), comments(), reviewers(), REVIEW_DB);
     ChangeBundle b2 = new ChangeBundle(c, messages(cm1), latest(c), approvals(),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     assertDiffs(b1, b2,
         "ChangeMessage.Key sets differ: [" + id
         + ",uuid2] only in A; [] only in B");
 
     // One NoteDb: UUIDs in keys can't be used for comparison, just diff counts.
     b1 = new ChangeBundle(c, messages(cm1, cm2), latest(c), approvals(),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     b2 = new ChangeBundle(c, messages(cm1), latest(c), approvals(), comments(),
-        NOTE_DB);
+        reviewers(), NOTE_DB);
     assertDiffs(b1, b2,
         "ChangeMessages differ for Change.Id " + id + "\n"
             + "Only in A:\n  " + cm2);
@@ -544,9 +659,9 @@
     cm3.getKey().set("uuid2"); // Differs only in UUID.
 
     ChangeBundle b1 = new ChangeBundle(c, messages(cm1, cm3), latest(c),
-        approvals(), comments(), REVIEW_DB);
+        approvals(), comments(), reviewers(), REVIEW_DB);
     ChangeBundle b2 = new ChangeBundle(c, messages(cm2, cm3), latest(c),
-        approvals(), comments(), NOTE_DB);
+        approvals(), comments(), reviewers(), NOTE_DB);
     // Implementation happens to pair up cm1 in b1 with cm3 in b2 because it
     // depends on iteration order and doesn't care about UUIDs. The important
     // thing is that there's some diff.
@@ -572,18 +687,18 @@
 
     // Both are ReviewDb, exact timestamp match is required.
     ChangeBundle b1 = new ChangeBundle(c, messages(cm1), latest(c), approvals(),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     ChangeBundle b2 = new ChangeBundle(c, messages(cm2), latest(c), approvals(),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     assertDiffs(b1, b2,
         "writtenOn differs for ChangeMessage.Key " + c.getId() + ",uuid1:"
             + " {2009-09-30 17:00:02.0} != {2009-09-30 17:00:03.0}");
 
     // One NoteDb, slop is allowed.
     b1 = new ChangeBundle(c, messages(cm1), latest(c), approvals(), comments(),
-        NOTE_DB);
+        reviewers(), NOTE_DB);
     b2 = new ChangeBundle(c, messages(cm2), latest(c), approvals(), comments(),
-        REVIEW_DB);
+        reviewers(), REVIEW_DB);
     assertNoDiffs(b1, b2);
     assertNoDiffs(b2, b1);
 
@@ -592,9 +707,9 @@
     ChangeMessage cm3 = clone(cm1);
     cm3.setWrittenOn(TimeUtil.nowTs());
     b1 = new ChangeBundle(c, messages(cm1), latest(c), approvals(), comments(),
-        NOTE_DB);
+        reviewers(), NOTE_DB);
     ChangeBundle b3 = new ChangeBundle(c, messages(cm3), latest(c), approvals(),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     int id = c.getId().get();
     assertDiffs(b1, b3,
         "ChangeMessages differ for Change.Id " + id + "\n"
@@ -619,9 +734,9 @@
     cm2.setPatchSetId(null);
 
     ChangeBundle b1 = new ChangeBundle(c, messages(cm1), latest(c), approvals(),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     ChangeBundle b2 = new ChangeBundle(c, messages(cm2), latest(c), approvals(),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
 
     // Both are ReviewDb, exact patch set ID match is required.
     assertDiffs(b1, b2,
@@ -630,16 +745,16 @@
 
     // Null patch set ID on ReviewDb is ignored.
     b1 = new ChangeBundle(c, messages(cm1), latest(c), approvals(), comments(),
-        NOTE_DB);
+        reviewers(), NOTE_DB);
     b2 = new ChangeBundle(c, messages(cm2), latest(c), approvals(), comments(),
-        REVIEW_DB);
+        reviewers(), REVIEW_DB);
     assertNoDiffs(b1, b2);
 
     // Null patch set ID on NoteDb is not ignored (but is not realistic).
     b1 = new ChangeBundle(c, messages(cm1), latest(c), approvals(), comments(),
-        REVIEW_DB);
+        reviewers(), REVIEW_DB);
     b2 = new ChangeBundle(c, messages(cm2), latest(c), approvals(), comments(),
-        NOTE_DB);
+        reviewers(), NOTE_DB);
     assertDiffs(b1, b2,
         "ChangeMessages differ for Change.Id " + id + "\n"
             + "Only in A:\n  " + cm1 + "\n"
@@ -651,6 +766,39 @@
   }
 
   @Test
+  public void diffChangeMessagesIgnoresMessagesOnPatchSetGreaterThanCurrent()
+      throws Exception {
+    Change c = TestChanges.newChange(project, accountId);
+
+    PatchSet ps1 = new PatchSet(new PatchSet.Id(c.getId(), 1));
+    ps1.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
+    ps1.setUploader(accountId);
+    ps1.setCreatedOn(TimeUtil.nowTs());
+    PatchSet ps2 = new PatchSet(new PatchSet.Id(c.getId(), 2));
+    ps2.setRevision(new RevId("badc0feebadc0feebadc0feebadc0feebadc0fee"));
+    ps2.setUploader(accountId);
+    ps2.setCreatedOn(TimeUtil.nowTs());
+
+    assertThat(c.currentPatchSetId()).isEqualTo(ps1.getId());
+
+    ChangeMessage cm1 = new ChangeMessage(
+        new ChangeMessage.Key(c.getId(), "uuid1"),
+        accountId, TimeUtil.nowTs(), ps1.getId());
+    cm1.setMessage("a message");
+    ChangeMessage cm2 = new ChangeMessage(
+        new ChangeMessage.Key(c.getId(), "uuid2"),
+        accountId, TimeUtil.nowTs(), ps2.getId());
+    cm2.setMessage("other message");
+
+    ChangeBundle b1 = new ChangeBundle(c, messages(cm1, cm2),
+        patchSets(ps1, ps2), approvals(), comments(), reviewers(), REVIEW_DB);
+    ChangeBundle b2 = new ChangeBundle(c, messages(cm1), patchSets(ps1),
+        approvals(), comments(), reviewers(), NOTE_DB);
+    assertNoDiffs(b1, b2);
+    assertNoDiffs(b2, b1);
+  }
+
+  @Test
   public void diffPatchSetIdSets() throws Exception {
     Change c = TestChanges.newChange(project, accountId);
     TestChanges.incrementPatchSet(c);
@@ -665,9 +813,9 @@
     ps2.setCreatedOn(TimeUtil.nowTs());
 
     ChangeBundle b1 = new ChangeBundle(c, messages(), patchSets(ps2),
-        approvals(), comments(), REVIEW_DB);
+        approvals(), comments(), reviewers(), REVIEW_DB);
     ChangeBundle b2 = new ChangeBundle(c, messages(), patchSets(ps1, ps2),
-        approvals(), comments(), REVIEW_DB);
+        approvals(), comments(), reviewers(), REVIEW_DB);
 
     assertDiffs(b1, b2,
         "PatchSet.Id sets differ:"
@@ -683,9 +831,9 @@
     ps1.setCreatedOn(TimeUtil.nowTs());
     PatchSet ps2 = clone(ps1);
     ChangeBundle b1 = new ChangeBundle(c, messages(), patchSets(ps1),
-        approvals(), comments(), REVIEW_DB);
+        approvals(), comments(), reviewers(), REVIEW_DB);
     ChangeBundle b2 = new ChangeBundle(c, messages(), patchSets(ps2),
-        approvals(), comments(), REVIEW_DB);
+        approvals(), comments(), reviewers(), REVIEW_DB);
 
     assertNoDiffs(b1, b2);
 
@@ -709,18 +857,18 @@
 
     // Both are ReviewDb, exact timestamp match is required.
     ChangeBundle b1 = new ChangeBundle(c, messages(), patchSets(ps1),
-        approvals(), comments(), REVIEW_DB);
+        approvals(), comments(), reviewers(), REVIEW_DB);
     ChangeBundle b2 = new ChangeBundle(c, messages(), patchSets(ps2),
-        approvals(), comments(), REVIEW_DB);
+        approvals(), comments(), reviewers(), REVIEW_DB);
     assertDiffs(b1, b2,
         "createdOn differs for PatchSet.Id " + c.getId() + ",1:"
             + " {2009-09-30 17:00:02.0} != {2009-09-30 17:00:03.0}");
 
     // One NoteDb, slop is allowed.
     b1 = new ChangeBundle(c, messages(), patchSets(ps1), approvals(),
-        comments(), NOTE_DB);
+        comments(), reviewers(), NOTE_DB);
     b2 = new ChangeBundle(c, messages(), patchSets(ps2), approvals(),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     assertNoDiffs(b1, b2);
 
     // But not too much slop.
@@ -728,9 +876,9 @@
     PatchSet ps3 = clone(ps1);
     ps3.setCreatedOn(TimeUtil.nowTs());
     b1 = new ChangeBundle(c, messages(), patchSets(ps1), approvals(),
-        comments(), NOTE_DB);
+        comments(), reviewers(), NOTE_DB);
     ChangeBundle b3 = new ChangeBundle(c, messages(), patchSets(ps3),
-        approvals(), comments(), REVIEW_DB);
+        approvals(), comments(), reviewers(), REVIEW_DB);
     String msg = "createdOn differs for PatchSet.Id " + c.getId()
         + ",1 in NoteDb vs. ReviewDb:"
         + " {2009-09-30 17:00:02.0} != {2009-09-30 17:00:10.0}";
@@ -752,16 +900,16 @@
     ps2.setPushCertificate(ps2.getPushCertificate() + "\n\n");
 
     ChangeBundle b1 = new ChangeBundle(c, messages(), patchSets(ps1),
-        approvals(), comments(), NOTE_DB);
+        approvals(), comments(), reviewers(), NOTE_DB);
     ChangeBundle b2 = new ChangeBundle(c, messages(), patchSets(ps2),
-        approvals(), comments(), REVIEW_DB);
+        approvals(), comments(), reviewers(), REVIEW_DB);
     assertNoDiffs(b1, b2);
     assertNoDiffs(b2, b1);
 
     b1 = new ChangeBundle(c, messages(), patchSets(ps1), approvals(),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     b2 = new ChangeBundle(c, messages(), patchSets(ps2), approvals(),
-        comments(), NOTE_DB);
+        comments(), reviewers(), NOTE_DB);
     assertNoDiffs(b1, b2);
     assertNoDiffs(b2, b1);
   }
@@ -793,24 +941,24 @@
 
     // Both ReviewDb.
     ChangeBundle b1 = new ChangeBundle(c, messages(), patchSets(ps1),
-        approvals(a1), comments(), REVIEW_DB);
+        approvals(a1), comments(), reviewers(), REVIEW_DB);
     ChangeBundle b2 = new ChangeBundle(c, messages(), patchSets(ps1, ps2),
-        approvals(a1, a2), comments(), REVIEW_DB);
+        approvals(a1, a2), comments(), reviewers(), REVIEW_DB);
     assertNoDiffs(b1, b2);
 
     // One NoteDb.
     b1 = new ChangeBundle(c, messages(), patchSets(ps1), approvals(a1),
-        comments(), NOTE_DB);
+        comments(), reviewers(), NOTE_DB);
     b2 = new ChangeBundle(c, messages(), patchSets(ps1, ps2), approvals(a1, a2),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     assertNoDiffs(b1, b2);
     assertNoDiffs(b2, b1);
 
     // Both NoteDb.
     b1 = new ChangeBundle(c, messages(), patchSets(ps1), approvals(a1),
-        comments(), NOTE_DB);
+        comments(), reviewers(), NOTE_DB);
     b2 = new ChangeBundle(c, messages(), patchSets(ps1, ps2), approvals(a1, a2),
-        comments(), NOTE_DB);
+        comments(), reviewers(), NOTE_DB);
     assertNoDiffs(b1, b2);
   }
 
@@ -830,9 +978,9 @@
         TimeUtil.nowTs());
 
     ChangeBundle b1 = new ChangeBundle(c, messages(), latest(c), approvals(a1),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     ChangeBundle b2 = new ChangeBundle(c, messages(), latest(c), approvals(a2),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
 
     assertDiffs(b1, b2,
         "PatchSetApproval.Key sets differ:"
@@ -850,9 +998,9 @@
         TimeUtil.nowTs());
     PatchSetApproval a2 = clone(a1);
     ChangeBundle b1 = new ChangeBundle(c, messages(), latest(c), approvals(a1),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     ChangeBundle b2 = new ChangeBundle(c, messages(), latest(c), approvals(a2),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
 
     assertNoDiffs(b1, b2);
 
@@ -877,9 +1025,9 @@
 
     // Both are ReviewDb, exact timestamp match is required.
     ChangeBundle b1 = new ChangeBundle(c, messages(), latest(c), approvals(a1),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     ChangeBundle b2 = new ChangeBundle(c, messages(), latest(c), approvals(a2),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     assertDiffs(b1, b2,
         "granted differs for PatchSetApproval.Key "
             + c.getId() + "%2C1,100,Code-Review:"
@@ -887,9 +1035,9 @@
 
     // One NoteDb, slop is allowed.
     b1 = new ChangeBundle(c, messages(), latest(c), approvals(a1), comments(),
-        NOTE_DB);
+        reviewers(), NOTE_DB);
     b2 = new ChangeBundle(c, messages(), latest(c), approvals(a2), comments(),
-        REVIEW_DB);
+        reviewers(), REVIEW_DB);
     assertNoDiffs(b1, b2);
 
     // But not too much slop.
@@ -897,9 +1045,9 @@
     PatchSetApproval a3 = clone(a1);
     a3.setGranted(TimeUtil.nowTs());
     b1 = new ChangeBundle(c, messages(), latest(c), approvals(a1), comments(),
-        NOTE_DB);
+        reviewers(), NOTE_DB);
     ChangeBundle b3 = new ChangeBundle(c, messages(), latest(c), approvals(a3),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     String msg = "granted differs for PatchSetApproval.Key "
         + c.getId() + "%2C1,100,Code-Review in NoteDb vs. ReviewDb:"
         + " {2009-09-30 17:00:07.0} != {2009-09-30 17:00:15.0}";
@@ -923,9 +1071,9 @@
 
     // Both are ReviewDb, exact match is required.
     ChangeBundle b1 = new ChangeBundle(c, messages(), latest(c), approvals(a1),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     ChangeBundle b2 = new ChangeBundle(c, messages(), latest(c), approvals(a2),
-        comments(), REVIEW_DB);
+        comments(), reviewers(), REVIEW_DB);
     assertDiffs(b1, b2,
         "granted differs for PatchSetApproval.Key "
             + c.getId() + "%2C1,100,Code-Review:"
@@ -933,14 +1081,47 @@
 
     // Truncating NoteDb timestamp is allowed.
     b1 = new ChangeBundle(c, messages(), latest(c), approvals(a1), comments(),
-        NOTE_DB);
+        reviewers(), NOTE_DB);
     b2 = new ChangeBundle(c, messages(), latest(c), approvals(a2), comments(),
-        REVIEW_DB);
+        reviewers(), REVIEW_DB);
     assertNoDiffs(b1, b2);
     assertNoDiffs(b2, b1);
   }
 
   @Test
+  public void diffReviewers() throws Exception {
+    Change c = TestChanges.newChange(project, accountId);
+    Timestamp now = TimeUtil.nowTs();
+    ReviewerSet r1 = reviewers(REVIEWER, new Account.Id(1), now);
+    ReviewerSet r2 = reviewers(REVIEWER, new Account.Id(2), now);
+
+    ChangeBundle b1 = new ChangeBundle(c, messages(), latest(c), approvals(),
+        comments(), r1, REVIEW_DB);
+    ChangeBundle b2 = new ChangeBundle(c, messages(), latest(c), approvals(),
+        comments(), r2, REVIEW_DB);
+    assertNoDiffs(b1, b1);
+    assertNoDiffs(b2, b2);
+    assertDiffs(b1, b2,
+        "reviewer sets differ:"
+            + " [1] only in A;"
+            + " [2] only in B");
+  }
+
+  @Test
+  public void diffReviewersIgnoresStateAndTimestamp() throws Exception {
+    Change c = TestChanges.newChange(project, accountId);
+    ReviewerSet r1 = reviewers(REVIEWER, new Account.Id(1), TimeUtil.nowTs());
+    ReviewerSet r2 = reviewers(CC, new Account.Id(1), TimeUtil.nowTs());
+
+    ChangeBundle b1 = new ChangeBundle(c, messages(), latest(c), approvals(),
+        comments(), r1, REVIEW_DB);
+    ChangeBundle b2 = new ChangeBundle(c, messages(), latest(c), approvals(),
+        comments(), r2, REVIEW_DB);
+    assertNoDiffs(b1, b1);
+    assertNoDiffs(b2, b2);
+  }
+
+  @Test
   public void diffPatchLineCommentKeySets() throws Exception {
     Change c = TestChanges.newChange(project, accountId);
     int id = c.getId().get();
@@ -954,9 +1135,9 @@
         5, accountId, null, TimeUtil.nowTs());
 
     ChangeBundle b1 = new ChangeBundle(c, messages(), latest(c), approvals(),
-        comments(c1), REVIEW_DB);
+        comments(c1), reviewers(), REVIEW_DB);
     ChangeBundle b2 = new ChangeBundle(c, messages(), latest(c), approvals(),
-        comments(c2), REVIEW_DB);
+        comments(c2), reviewers(), REVIEW_DB);
 
     assertDiffs(b1, b2,
         "PatchLineComment.Key sets differ:"
@@ -973,9 +1154,9 @@
         5, accountId, null, TimeUtil.nowTs());
     PatchLineComment c2 = clone(c1);
     ChangeBundle b1 = new ChangeBundle(c, messages(), latest(c), approvals(),
-        comments(c1), REVIEW_DB);
+        comments(c1), reviewers(), REVIEW_DB);
     ChangeBundle b2 = new ChangeBundle(c, messages(), latest(c), approvals(),
-        comments(c2), REVIEW_DB);
+        comments(c2), reviewers(), REVIEW_DB);
 
     assertNoDiffs(b1, b2);
 
@@ -999,9 +1180,9 @@
 
     // Both are ReviewDb, exact timestamp match is required.
     ChangeBundle b1 = new ChangeBundle(c, messages(), latest(c), approvals(),
-        comments(c1), REVIEW_DB);
+        comments(c1), reviewers(), REVIEW_DB);
     ChangeBundle b2 = new ChangeBundle(c, messages(), latest(c), approvals(),
-        comments(c2), REVIEW_DB);
+        comments(c2), reviewers(), REVIEW_DB);
     assertDiffs(b1, b2,
         "writtenOn differs for PatchLineComment.Key "
             + c.getId() + ",1,filename,uuid:"
@@ -1009,9 +1190,9 @@
 
     // One NoteDb, slop is allowed.
     b1 = new ChangeBundle(c, messages(), latest(c), approvals(), comments(c1),
-        NOTE_DB);
+        reviewers(), NOTE_DB);
     b2 = new ChangeBundle(c, messages(), latest(c), approvals(), comments(c2),
-        REVIEW_DB);
+        reviewers(), REVIEW_DB);
     assertNoDiffs(b1, b2);
 
     // But not too much slop.
@@ -1019,9 +1200,9 @@
     PatchLineComment c3 = clone(c1);
     c3.setWrittenOn(TimeUtil.nowTs());
     b1 = new ChangeBundle(c, messages(), latest(c), approvals(), comments(c1),
-        NOTE_DB);
+        reviewers(), NOTE_DB);
     ChangeBundle b3 = new ChangeBundle(c, messages(), latest(c), approvals(),
-        comments(c3), REVIEW_DB);
+        comments(c3), reviewers(), REVIEW_DB);
     String msg = "writtenOn differs for PatchLineComment.Key " + c.getId()
         + ",1,filename,uuid in NoteDb vs. ReviewDb:"
         + " {2009-09-30 17:00:02.0} != {2009-09-30 17:00:10.0}";
@@ -1043,9 +1224,9 @@
         5, accountId, null, TimeUtil.nowTs());
 
     ChangeBundle b1 = new ChangeBundle(c, messages(), latest(c), approvals(),
-        comments(c1, c2), REVIEW_DB);
+        comments(c1, c2), reviewers(), REVIEW_DB);
     ChangeBundle b2 = new ChangeBundle(c, messages(), latest(c), approvals(),
-        comments(c1), REVIEW_DB);
+        comments(c1), reviewers(), REVIEW_DB);
     assertNoDiffs(b1, b2);
   }
 
@@ -1085,6 +1266,17 @@
     return Arrays.asList(ents);
   }
 
+  private static ReviewerSet reviewers(Object... ents) {
+    checkArgument(ents.length % 3 == 0);
+    Table<ReviewerStateInternal, Account.Id, Timestamp> t =
+        HashBasedTable.create();
+    for (int i = 0; i < ents.length; i += 3) {
+      t.put((ReviewerStateInternal) ents[i], (Account.Id) ents[i + 1],
+          (Timestamp) ents[i + 2]);
+    }
+    return ReviewerSet.fromTable(t);
+  }
+
   private static List<PatchLineComment> comments(PatchLineComment... ents) {
     return Arrays.asList(ents);
   }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
index 3f77498..1f54f55 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -28,7 +28,7 @@
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ImmutableMultimap;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.ImmutableSetMultimap;
+import com.google.common.collect.ImmutableTable;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
@@ -46,6 +46,7 @@
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
 import com.google.gerrit.testutil.TestChanges;
 import com.google.gwtorm.server.OrmException;
@@ -392,10 +393,12 @@
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    assertThat(notes.getReviewers()).isEqualTo(
-        ImmutableSetMultimap.of(
-          REVIEWER, new Account.Id(1),
-          REVIEWER, new Account.Id(2)));
+    Timestamp ts = new Timestamp(update.getWhen().getTime());
+    assertThat(notes.getReviewers()).isEqualTo(ReviewerSet.fromTable(
+        ImmutableTable.<ReviewerStateInternal, Account.Id, Timestamp>builder()
+            .put(REVIEWER, new Account.Id(1), ts)
+            .put(REVIEWER, new Account.Id(2), ts)
+            .build()));
   }
 
   @Test
@@ -407,10 +410,12 @@
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    assertThat(notes.getReviewers()).isEqualTo(
-        ImmutableSetMultimap.of(
-            REVIEWER, new Account.Id(1),
-            CC, new Account.Id(2)));
+    Timestamp ts = new Timestamp(update.getWhen().getTime());
+    assertThat(notes.getReviewers()).isEqualTo(ReviewerSet.fromTable(
+        ImmutableTable.<ReviewerStateInternal, Account.Id, Timestamp>builder()
+            .put(REVIEWER, new Account.Id(1), ts)
+            .put(CC, new Account.Id(2), ts)
+            .build()));
   }
 
   @Test
@@ -421,16 +426,18 @@
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    assertThat(notes.getReviewers()).isEqualTo(
-        ImmutableSetMultimap.of(REVIEWER, new Account.Id(2)));
+    Timestamp ts = new Timestamp(update.getWhen().getTime());
+    assertThat(notes.getReviewers()).isEqualTo(ReviewerSet.fromTable(
+        ImmutableTable.of(REVIEWER, new Account.Id(2), ts)));
 
     update = newUpdate(c, otherUser);
     update.putReviewer(otherUser.getAccount().getId(), CC);
     update.commit();
 
     notes = newNotes(c);
-    assertThat(notes.getReviewers()).isEqualTo(
-        ImmutableSetMultimap.of(CC, new Account.Id(2)));
+    ts = new Timestamp(update.getWhen().getTime());
+    assertThat(notes.getReviewers()).isEqualTo(ReviewerSet.fromTable(
+        ImmutableTable.of(CC, new Account.Id(2), ts)));
   }
 
   @Test
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/ProjectControlTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/ProjectControlTest.java
index f29a351..e3c382a 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/project/ProjectControlTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/ProjectControlTest.java
@@ -79,7 +79,7 @@
     schemaCreator.create(db);
     Account.Id userId = accountManager.authenticate(AuthRequest.forUser("user"))
         .getAccountId();
-    user = userFactory.create(Providers.of(db), userId);
+    user = userFactory.create(userId);
 
     Project.NameKey name = new Project.NameKey("project");
     InMemoryRepository inMemoryRepo = repoManager.createRepository(name);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
index e8c62e93..6f24a4b 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
@@ -41,7 +41,6 @@
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.errors.InvalidNameException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -76,7 +75,6 @@
 
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
@@ -914,10 +912,5 @@
     public Set<Change.Id> getStarredChanges() {
       return Collections.emptySet();
     }
-
-    @Override
-    public Collection<AccountProjectWatch> getNotificationFilters() {
-      return Collections.emptySet();
-    }
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index bcc9b38..dad134b 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -66,6 +66,7 @@
 import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.index.change.ChangeIndexCollection;
+import com.google.gerrit.server.index.change.ChangeIndexer;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.RefControl;
@@ -119,6 +120,7 @@
   @Inject protected GerritApi gApi;
   @Inject protected IdentifiedUser.GenericFactory userFactory;
   @Inject protected ChangeIndexCollection indexes;
+  @Inject protected ChangeIndexer indexer;
   @Inject protected InMemoryDatabase schemaFactory;
   @Inject protected InMemoryRepositoryManager repoManager;
   @Inject protected InternalChangeQuery internalChangeQuery;
@@ -154,13 +156,13 @@
     Account userAccount = db.accounts().get(userId);
     userAccount.setPreferredEmail("user@example.com");
     db.accounts().update(ImmutableList.of(userAccount));
-    user = userFactory.create(Providers.of(db), userId);
+    user = userFactory.create(userId);
     requestContext.setContext(newRequestContext(userAccount.getId()));
   }
 
   protected RequestContext newRequestContext(Account.Id requestUserId) {
     final CurrentUser requestUser =
-        userFactory.create(Providers.of(db), requestUserId);
+        userFactory.create(requestUserId);
     return new RequestContext() {
       @Override
       public CurrentUser getUser() {
@@ -569,7 +571,7 @@
         .reviewer(user.getAccountId().toString())
         .votes();
     assertThat(m).hasSize(1);
-    assertThat(m).containsEntry("Code-Review", new Short((short)1));
+    assertThat(m).containsEntry("Code-Review", Short.valueOf((short) 1));
 
     Map<Integer, Change> changes = new LinkedHashMap<>(5);
     changes.put(2, reviewPlus2Change);
@@ -1087,8 +1089,24 @@
 
   @Test
   public void byHashtagWithoutNoteDb() throws Exception {
-    assume().that(notesMigration.enabled()).isFalse();
-    setUpHashtagChanges();
+    assume().that(notesMigration.readChanges()).isFalse();
+
+    notesMigration.setWriteChanges(true);
+    notesMigration.setReadChanges(true);
+    db.close();
+    db = schemaFactory.open();
+    List<Change> changes;
+    try {
+      changes = setUpHashtagChanges();
+      notesMigration.setWriteChanges(false);
+      notesMigration.setReadChanges(false);
+    } finally {
+      db.close();
+    }
+    db = schemaFactory.open();
+    for (Change c : changes) {
+      indexer.index(db, c); // Reindex without hashtag field.
+    }
     assertQuery("hashtag:foo");
     assertQuery("hashtag:bar");
     assertQuery("hashtag:\" bar \"");
@@ -1415,7 +1433,7 @@
 
   @Test
   public void prepopulatedFields() throws Exception {
-    assume().that(notesMigration.enabled()).isFalse();
+    assume().that(notesMigration.readChanges()).isFalse();
     TestRepository<Repo> repo = createProject("repo");
     Change change = insert(repo, newChange(repo));
 
@@ -1444,7 +1462,7 @@
 
   @Test
   public void prepopulateOnlyRequestedFields() throws Exception {
-    assume().that(notesMigration.enabled()).isFalse();
+    assume().that(notesMigration.readChanges()).isFalse();
     TestRepository<Repo> repo = createProject("repo");
     Change change = insert(repo, newChange(repo));
 
@@ -1522,7 +1540,7 @@
     Project.NameKey project = new Project.NameKey(
         repo.getRepository().getDescription().getRepositoryName());
     Account.Id ownerId = owner != null ? owner : userId;
-    IdentifiedUser user = userFactory.create(Providers.of(db), ownerId);
+    IdentifiedUser user = userFactory.create(ownerId);
     try (BatchUpdate bu =
         updateFactory.create(db, project, user, TimeUtil.nowTs())) {
       bu.insertChange(ins);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java
deleted file mode 100644
index ba62cf7..0000000
--- a/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java
+++ /dev/null
@@ -1,290 +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.server.util;
-
-import static org.easymock.EasyMock.createNiceMock;
-import static org.easymock.EasyMock.createStrictMock;
-import static org.easymock.EasyMock.expect;
-import static org.easymock.EasyMock.replay;
-import static org.easymock.EasyMock.verify;
-import static org.junit.Assert.assertEquals;
-
-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.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectState;
-
-import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
-import org.eclipse.jgit.lib.BlobBasedConfig;
-import org.eclipse.jgit.lib.Constants;
-import org.junit.Before;
-import org.junit.Test;
-
-import java.net.URI;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.TreeMap;
-
-public class SubmoduleSectionParserTest extends LocalDiskRepositoryTestCase {
-  private static final String THIS_SERVER = "localhost";
-  private ProjectCache projectCache;
-  private BlobBasedConfig bbc;
-
-  @Override
-  @Before
-  public void setUp() throws Exception {
-    super.setUp();
-
-    projectCache = createStrictMock(ProjectCache.class);
-    bbc = createStrictMock(BlobBasedConfig.class);
-  }
-
-  private void doReplay() {
-    replay(projectCache, bbc);
-  }
-
-  private void doVerify() {
-    verify(projectCache, bbc);
-  }
-
-  @Test
-  public void testSubmodulesParseWithCorrectSections() throws Exception {
-    final Map<String, SubmoduleSection> sectionsToReturn = new TreeMap<>();
-    sectionsToReturn.put("a", new SubmoduleSection("ssh://localhost/a", "a",
-        "."));
-    sectionsToReturn.put("b", new SubmoduleSection("ssh://localhost/b", "b",
-        "."));
-    sectionsToReturn.put("c", new SubmoduleSection("ssh://localhost/test/c",
-        "c-path", "refs/heads/master"));
-    sectionsToReturn.put("d", new SubmoduleSection("ssh://localhost/d",
-        "d-parent/the-d-folder", "refs/heads/test"));
-    sectionsToReturn.put("e", new SubmoduleSection("ssh://localhost/e.git", "e",
-        "."));
-
-    Map<String, String> reposToBeFound = new HashMap<>();
-    reposToBeFound.put("a", "a");
-    reposToBeFound.put("b", "b");
-    reposToBeFound.put("c", "test/c");
-    reposToBeFound.put("d", "d");
-    reposToBeFound.put("e", "e");
-
-    final Branch.NameKey superBranchNameKey =
-        new Branch.NameKey(new Project.NameKey("super-project"),
-            "refs/heads/master");
-
-    Set<SubmoduleSubscription> expectedSubscriptions = new HashSet<>();
-    expectedSubscriptions
-        .add(new SubmoduleSubscription(superBranchNameKey, new Branch.NameKey(
-            new Project.NameKey("a"), "refs/heads/master"), "a"));
-    expectedSubscriptions
-        .add(new SubmoduleSubscription(superBranchNameKey, new Branch.NameKey(
-            new Project.NameKey("b"), "refs/heads/master"), "b"));
-    expectedSubscriptions.add(new SubmoduleSubscription(superBranchNameKey,
-        new Branch.NameKey(new Project.NameKey("test/c"), "refs/heads/master"),
-        "c-path"));
-    expectedSubscriptions.add(new SubmoduleSubscription(superBranchNameKey,
-        new Branch.NameKey(new Project.NameKey("d"), "refs/heads/test"),
-        "d-parent/the-d-folder"));
-    expectedSubscriptions
-        .add(new SubmoduleSubscription(superBranchNameKey, new Branch.NameKey(
-            new Project.NameKey("e"), "refs/heads/master"), "e"));
-
-    execute(superBranchNameKey, sectionsToReturn, reposToBeFound,
-        expectedSubscriptions);
-  }
-
-  @Test
-  public void testSubmodulesParseWithAnInvalidSection() throws Exception {
-    final Map<String, SubmoduleSection> sectionsToReturn = new TreeMap<>();
-    sectionsToReturn.put("a", new SubmoduleSection("ssh://localhost/a", "a",
-        "."));
-    // This one is invalid since "b" is not a recognized project
-    sectionsToReturn.put("b", new SubmoduleSection("ssh://localhost/b", "b",
-        "."));
-    sectionsToReturn.put("c", new SubmoduleSection("ssh://localhost/test/c",
-        "c-path", "refs/heads/master"));
-    sectionsToReturn.put("d", new SubmoduleSection("ssh://localhost/d",
-        "d-parent/the-d-folder", "refs/heads/test"));
-    sectionsToReturn.put("e", new SubmoduleSection("ssh://localhost/e.git", "e",
-        "."));
-
-    // "b" will not be in this list
-    Map<String, String> reposToBeFound = new HashMap<>();
-    reposToBeFound.put("a", "a");
-    reposToBeFound.put("c", "test/c");
-    reposToBeFound.put("d", "d");
-    reposToBeFound.put("e", "e");
-
-    final Branch.NameKey superBranchNameKey =
-        new Branch.NameKey(new Project.NameKey("super-project"),
-            "refs/heads/master");
-
-    Set<SubmoduleSubscription> expectedSubscriptions = new HashSet<>();
-    expectedSubscriptions
-        .add(new SubmoduleSubscription(superBranchNameKey, new Branch.NameKey(
-            new Project.NameKey("a"), "refs/heads/master"), "a"));
-    expectedSubscriptions.add(new SubmoduleSubscription(superBranchNameKey,
-        new Branch.NameKey(new Project.NameKey("test/c"), "refs/heads/master"),
-        "c-path"));
-    expectedSubscriptions.add(new SubmoduleSubscription(superBranchNameKey,
-        new Branch.NameKey(new Project.NameKey("d"), "refs/heads/test"),
-        "d-parent/the-d-folder"));
-    expectedSubscriptions
-        .add(new SubmoduleSubscription(superBranchNameKey, new Branch.NameKey(
-            new Project.NameKey("e"), "refs/heads/master"), "e"));
-
-    execute(superBranchNameKey, sectionsToReturn, reposToBeFound,
-        expectedSubscriptions);
-  }
-
-  @Test
-  public void testSubmoduleSectionToOtherServer() throws Exception {
-    Map<String, SubmoduleSection> sectionsToReturn = new HashMap<>();
-    // The url is not to this server.
-    sectionsToReturn.put("a", new SubmoduleSection("ssh://review.source.com/a",
-        "a", "."));
-
-    Set<SubmoduleSubscription> expectedSubscriptions = Collections.emptySet();
-    execute(new Branch.NameKey(new Project.NameKey("super-project"),
-        "refs/heads/master"), sectionsToReturn, new HashMap<String, String>(),
-        expectedSubscriptions);
-  }
-
-  @Test
-  public void testProjectNotFound() throws Exception {
-    Map<String, SubmoduleSection> sectionsToReturn = new HashMap<>();
-    sectionsToReturn.put("a", new SubmoduleSection("ssh://localhost/a", "a",
-        "."));
-
-    Set<SubmoduleSubscription> expectedSubscriptions = Collections.emptySet();
-    execute(new Branch.NameKey(new Project.NameKey("super-project"),
-        "refs/heads/master"), sectionsToReturn, new HashMap<String, String>(),
-        expectedSubscriptions);
-  }
-
-  @Test
-  public void testProjectWithSlashesNotFound() throws Exception {
-    Map<String, SubmoduleSection> sectionsToReturn = new HashMap<>();
-    sectionsToReturn.put("project", new SubmoduleSection(
-        "ssh://localhost/company/tools/project", "project", "."));
-
-    Set<SubmoduleSubscription> expectedSubscriptions = Collections.emptySet();
-    execute(new Branch.NameKey(new Project.NameKey("super-project"),
-        "refs/heads/master"), sectionsToReturn, new HashMap<String, String>(),
-        expectedSubscriptions);
-  }
-
-  @Test
-  public void testSubmodulesParseWithSubProjectFound() throws Exception {
-    Map<String, SubmoduleSection> sectionsToReturn = new TreeMap<>();
-    sectionsToReturn.put("a/b", new SubmoduleSection(
-        "ssh://localhost/a/b", "a/b", "."));
-
-    Map<String, String> reposToBeFound = new HashMap<>();
-    reposToBeFound.put("a/b", "a/b");
-    reposToBeFound.put("b", "b");
-
-    Branch.NameKey superBranchNameKey =
-        new Branch.NameKey(new Project.NameKey("super-project"),
-            "refs/heads/master");
-
-    Set<SubmoduleSubscription> expectedSubscriptions = new HashSet<>();
-    expectedSubscriptions
-        .add(new SubmoduleSubscription(superBranchNameKey, new Branch.NameKey(
-            new Project.NameKey("a/b"), "refs/heads/master"), "a/b"));
-    execute(superBranchNameKey, sectionsToReturn, reposToBeFound,
-        expectedSubscriptions);
-  }
-
-  private void execute(final Branch.NameKey superProjectBranch,
-      final Map<String, SubmoduleSection> sectionsToReturn,
-      final Map<String, String> reposToBeFound,
-      final Set<SubmoduleSubscription> expectedSubscriptions) throws Exception {
-    expect(bbc.getSubsections("submodule"))
-        .andReturn(sectionsToReturn.keySet());
-
-    for (Map.Entry<String, SubmoduleSection> entry : sectionsToReturn.entrySet()) {
-      String id = entry.getKey();
-      final SubmoduleSection section = entry.getValue();
-      expect(bbc.getString("submodule", id, "url")).andReturn(section.getUrl());
-      expect(bbc.getString("submodule", id, "path")).andReturn(
-          section.getPath());
-      expect(bbc.getString("submodule", id, "branch")).andReturn(
-          section.getBranch());
-
-      if (THIS_SERVER.equals(new URI(section.getUrl()).getHost())) {
-        String projectNameCandidate;
-        final String urlExtractedPath = new URI(section.getUrl()).getPath();
-        int fromIndex = urlExtractedPath.length() - 1;
-        while (fromIndex > 0) {
-          fromIndex = urlExtractedPath.lastIndexOf('/', fromIndex - 1);
-          projectNameCandidate = urlExtractedPath.substring(fromIndex + 1);
-          if (projectNameCandidate.endsWith(Constants.DOT_GIT_EXT)) {
-            projectNameCandidate = projectNameCandidate.substring(0, //
-                projectNameCandidate.length() - Constants.DOT_GIT_EXT.length());
-          }
-          if (reposToBeFound.containsValue(projectNameCandidate)) {
-            expect(projectCache.get(new Project.NameKey(projectNameCandidate)))
-                .andReturn(createNiceMock(ProjectState.class));
-          } else {
-            expect(projectCache.get(new Project.NameKey(projectNameCandidate)))
-                .andReturn(null);
-          }
-        }
-      }
-    }
-
-    doReplay();
-
-    final SubmoduleSectionParser ssp =
-        new SubmoduleSectionParser(projectCache, bbc, THIS_SERVER,
-            superProjectBranch);
-
-    Set<SubmoduleSubscription> returnedSubscriptions = ssp.parseAllSections();
-
-    doVerify();
-
-    assertEquals(expectedSubscriptions, returnedSubscriptions);
-  }
-
-  private static final class SubmoduleSection {
-    private final String url;
-    private final String path;
-    private final String branch;
-
-    SubmoduleSection(final String url, final String path,
-        final String branch) {
-      this.url = url;
-      this.path = path;
-      this.branch = branch;
-    }
-
-    public String getUrl() {
-      return url;
-    }
-
-    public String getPath() {
-      return path;
-    }
-
-    public String getBranch() {
-      return branch;
-    }
-  }
-}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java
index bbad2be..fde3a66 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java
@@ -63,19 +63,19 @@
     for (String name : chain(command)) {
       CommandProvider p = map.get(name);
       if (p == null) {
-        throw new UnloggedFailure(1, getName() + ": not found");
+        throw die(getName() + ": not found");
       }
 
       Command cmd = p.getProvider().get();
       if (!(cmd instanceof DispatchCommand)) {
-        throw new UnloggedFailure(1, getName() + ": not found");
+        throw die(getName() + ": not found");
       }
       map = ((DispatchCommand) cmd).getMap();
     }
 
     CommandProvider p = map.get(command.value());
     if (p == null) {
-      throw new UnloggedFailure(1, getName() + ": not found");
+      throw die(getName() + ": not found");
     }
 
     Command cmd = p.getProvider().get();
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
index f296ef3..2873c37 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
@@ -377,6 +377,14 @@
     return new UnloggedFailure(1, "fatal: " + why.getMessage(), why);
   }
 
+  protected void writeError(String type, String msg) {
+    try {
+      err.write((type + ": " + msg + "\n").getBytes(ENC));
+    } catch (IOException e) {
+      // Ignored
+    }
+  }
+
   public void checkExclusivity(final Object arg1, final String arg1name,
       final Object arg2, final String arg2name) throws UnloggedFailure {
     if (arg1 != null && arg2 != null) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/ChangeArgumentParser.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/ChangeArgumentParser.java
new file mode 100644
index 0000000..aa361af
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/ChangeArgumentParser.java
@@ -0,0 +1,85 @@
+// 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.sshd;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeFinder;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.ChangesCollection;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.sshd.BaseCommand.UnloggedFailure;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class ChangeArgumentParser {
+  private final CurrentUser currentUser;
+  private final ChangesCollection changesCollection;
+  private final ChangeFinder changeFinder;
+  private final ReviewDb db;
+
+  @Inject
+  ChangeArgumentParser(CurrentUser currentUser,
+      ChangesCollection changesCollection,
+      ChangeFinder changeFinder,
+      ReviewDb db) {
+    this.currentUser = currentUser;
+    this.changesCollection = changesCollection;
+    this.changeFinder = changeFinder;
+    this.db = db;
+  }
+
+  public void addChange(String id, Map<Change.Id, ChangeResource> changes)
+      throws UnloggedFailure, OrmException {
+    addChange(id, changes, null);
+  }
+
+  public void addChange(String id, Map<Change.Id, ChangeResource> changes,
+      ProjectControl projectControl) throws UnloggedFailure, OrmException {
+    List<ChangeControl> matched = changeFinder.find(id, currentUser);
+    List<ChangeControl> toAdd = new ArrayList<>(changes.size());
+    for (ChangeControl ctl : matched) {
+      if (!changes.containsKey(ctl.getId())
+          && inProject(projectControl, ctl.getProject())
+          && ctl.isVisible(db)) {
+        toAdd.add(ctl);
+      }
+    }
+
+    if (toAdd.isEmpty()) {
+      throw new UnloggedFailure(1, "\"" + id + "\" no such change");
+    } else if (toAdd.size() > 1) {
+      throw new UnloggedFailure(1, "\"" + id + "\" matches multiple changes");
+    }
+    ChangeControl ctl = toAdd.get(0);
+    changes.put(ctl.getId(), changesCollection.parse(ctl));
+  }
+
+  private boolean inProject(ProjectControl projectControl, Project project) {
+    if (projectControl != null) {
+      return projectControl.getProject().getNameKey().equals(project.getNameKey());
+    } else {
+      // No --project option, so they want every project.
+      return true;
+    }
+  }
+}
\ No newline at end of file
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
index 8873be9..f2911dc 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
@@ -73,7 +73,7 @@
       if (Strings.isNullOrEmpty(commandName)) {
         StringWriter msg = new StringWriter();
         msg.write(usage());
-        throw new UnloggedFailure(1, msg.toString());
+        throw die(msg.toString());
       }
 
       final CommandProvider p = commands.get(commandName);
@@ -81,7 +81,7 @@
         String msg =
             (getName().isEmpty() ? "Gerrit Code Review" : getName()) + ": "
                 + commandName + ": not found";
-        throw new UnloggedFailure(1, msg);
+        throw die(msg);
       }
 
       final Command cmd = p.getProvider().get();
@@ -96,7 +96,7 @@
         bc.setArguments(args.toArray(new String[args.size()]));
 
       } else if (!args.isEmpty()) {
-        throw new UnloggedFailure(1, commandName + " does not take arguments");
+        throw die(commandName + " does not take arguments");
       }
 
       provideStateTo(cmd);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
index 4308db9..24bd8c2 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
@@ -115,10 +115,9 @@
     if (caller instanceof PeerDaemonUser) {
       // OK.
     } else if (!enableRunAs) {
-      throw new UnloggedFailure(1,
-          "fatal: suexec disabled by auth.enableRunAs = false");
+      throw die("suexec disabled by auth.enableRunAs = false");
     } else if (!caller.getCapabilities().canRunAs()) {
-      throw new UnloggedFailure(1, "fatal: suexec not permitted");
+      throw die("suexec not permitted");
     }
   }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java
index 850026b..237d844 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java
@@ -47,7 +47,7 @@
     try {
       checkPermission();
     } catch (PermissionDeniedException err) {
-      throw new UnloggedFailure("fatal: " + err.getMessage());
+      throw die(err.getMessage());
     }
 
     QueryShell shell = factory.create(in, out);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
index b4594d4..eb0d7b2 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
@@ -84,12 +84,11 @@
   @Override
   protected void run() throws Failure {
     if (oldParent == null && children.isEmpty()) {
-      throw new UnloggedFailure(1, "fatal: child projects have to be specified as " +
-                                   "arguments or the --children-of option has to be set");
+      throw die("child projects have to be specified as " +
+          "arguments or the --children-of option has to be set");
     }
     if (oldParent == null && !excludedChildren.isEmpty()) {
-      throw new UnloggedFailure(1, "fatal: --exclude can only be used together " +
-                                   "with --children-of");
+      throw die("--exclude can only be used together with --children-of");
     }
 
     final StringBuilder err = new StringBuilder();
@@ -164,7 +163,7 @@
       while (err.charAt(err.length() - 1) == '\n') {
         err.setLength(err.length() - 1);
       }
-      throw new UnloggedFailure(1, err.toString());
+      throw die(err.toString());
     }
   }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AproposCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AproposCommand.java
index cc393ce..12f69ed 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AproposCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AproposCommand.java
@@ -48,7 +48,7 @@
             docResult.url));
       }
     } catch (DocQueryException dqe) {
-      throw new UnloggedFailure(1, "fatal: " + dqe.getMessage());
+      throw die(dqe);
     }
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BaseTestPrologCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BaseTestPrologCommand.java
index 301bc0e..9f31ddc 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BaseTestPrologCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BaseTestPrologCommand.java
@@ -76,7 +76,7 @@
       OutputFormat.JSON.newGson().toJson(result, stdout);
       stdout.print('\n');
     } catch (Exception e) {
-      throw new UnloggedFailure("Processing of prolog script failed: " + e);
+      throw die("Processing of prolog script failed: " + e);
     }
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateBranchCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateBranchCommand.java
index b1d09e9..d3ec69c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateBranchCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateBranchCommand.java
@@ -48,7 +48,7 @@
       gApi.projects().name(project.getProject().getNameKey().get())
           .branch(name).create(in);
     } catch (RestApiException e) {
-      throw new UnloggedFailure(1, "fatal: " + e.getMessage(), e);
+      throw die(e);
     }
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
index 3ad5156..4ebafb8 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
@@ -134,7 +134,7 @@
     try {
       if (!suggestParent) {
         if (projectName == null) {
-          throw new UnloggedFailure(1, "fatal: Project name is required.");
+          throw die("Project name is required.");
         }
 
         ProjectInput input = new ProjectInput();
@@ -176,7 +176,7 @@
         }
       }
     } catch (RestApiException | NoSuchProjectException err) {
-      throw new UnloggedFailure(1, "fatal: " + err.getMessage(), err);
+      throw die(err);
     }
   }
 
@@ -188,7 +188,7 @@
       String[] s = pluginConfigValue.split("=");
       String[] s2 = s[0].split("\\.");
       if (s.length != 2 || s2.length != 2) {
-        throw new UnloggedFailure(1, "Invalid plugin config value '"
+        throw die("Invalid plugin config value '"
             + pluginConfigValue
             + "', expected format '<plugin-name>.<parameter-name>=<value>'"
             + " or '<plugin-name>.<parameter-name>=<value1,value2,...>'");
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
index fcf365c..1f03225 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
@@ -60,14 +60,14 @@
     try {
       if (list) {
         if (all || caches.size() > 0) {
-          throw error("error: cannot use --list with --all or --cache");
+          throw die("cannot use --list with --all or --cache");
         }
         doList();
         return;
       }
 
       if (all && caches.size() > 0) {
-        throw error("error: cannot combine --all and --cache");
+        throw die("cannot combine --all and --cache");
       } else if (!all && caches.size() == 1 && caches.contains("all")) {
         caches.clear();
         all = true;
@@ -87,10 +87,6 @@
     }
   }
 
-  private static UnloggedFailure error(String msg) {
-    return new UnloggedFailure(1, msg);
-  }
-
   @SuppressWarnings("unchecked")
   private void doList() {
     for (String name : (List<String>) listCaches
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
index a3fbcb2..520d194 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
@@ -68,11 +68,10 @@
 
   private void verifyCommandLine() throws UnloggedFailure {
     if (!all && projects.isEmpty()) {
-      throw new UnloggedFailure(1,
-          "needs projects as command arguments or --all option");
+      throw die("needs projects as command arguments or --all option");
     }
     if (all && !projects.isEmpty()) {
-      throw new UnloggedFailure(1,
+      throw die(
           "either specify projects as command arguments or use --all option");
     }
   }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexActivateCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexActivateCommand.java
index c508b1d..4991700 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexActivateCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexActivateCommand.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.sshd.commands;
 
-import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER;
-
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.lucene.LuceneVersionManager;
@@ -28,8 +26,7 @@
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
 @CommandMetaData(name = "activate",
-  description = "Activate the latest index version available",
-  runsAt = MASTER)
+  description = "Activate the latest index version available")
 public class IndexActivateCommand extends SshCommand {
 
   @Argument(index = 0, required = true, metaVar = "INDEX",
@@ -48,8 +45,7 @@
         stdout.println("Not activating index, already using latest version");
       }
     } catch (ReindexerAlreadyRunningException e) {
-      throw new UnloggedFailure("Failed to activate latest index: "
-          + e.getMessage());
+      throw die("Failed to activate latest index: " + e.getMessage());
     }
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
new file mode 100644
index 0000000..f2c858e
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
@@ -0,0 +1,71 @@
+// 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.sshd.commands;
+
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.Index;
+import com.google.gerrit.sshd.ChangeArgumentParser;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import org.kohsuke.args4j.Argument;
+
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+@CommandMetaData(name = "changes", description = "Index changes")
+final class IndexChangesCommand extends SshCommand {
+  @Inject
+  private Index index;
+
+  @Inject
+  private ChangeArgumentParser changeArgumentParser;
+
+  @Argument(index = 0, required = true, multiValued = true, metaVar = "CHANGE",
+      usage = "changes to index")
+  void addChange(String token) {
+    try {
+      changeArgumentParser.addChange(token, changes);
+    } catch (UnloggedFailure e) {
+      throw new IllegalArgumentException(e.getMessage(), e);
+    } catch (OrmException e) {
+      throw new IllegalArgumentException("database is down", e);
+    }
+  }
+
+  private Map<Change.Id, ChangeResource> changes = new LinkedHashMap<>();
+
+  @Override
+  protected void run() throws UnloggedFailure {
+    boolean ok = true;
+    for (ChangeResource rsrc : changes.values()) {
+      try {
+        index.apply(rsrc, new Index.Input());
+      } catch (IOException | RestApiException e) {
+        ok = false;
+        writeError("error", String.format(
+            "failed to index change %s: %s", rsrc.getId(), e.getMessage()));
+      }
+    }
+    if (!ok) {
+      throw die("failed to index one or more changes");
+    }
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexCommandsModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexCommandsModule.java
index 3e7b293..633bca8 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexCommandsModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexCommandsModule.java
@@ -28,5 +28,6 @@
     command(index).toProvider(new DispatchCommandProvider(index));
     command(index, IndexActivateCommand.class);
     command(index, IndexStartCommand.class);
+    command(index, IndexChangesCommand.class);
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexStartCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexStartCommand.java
index c2c565f..73e9f33 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexStartCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexStartCommand.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.sshd.commands;
 
-import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER;
-
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.lucene.LuceneVersionManager;
@@ -27,8 +25,7 @@
 import org.kohsuke.args4j.Argument;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
-@CommandMetaData(name = "start", description = "Start the online reindexer",
-  runsAt = MASTER)
+@CommandMetaData(name = "start", description = "Start the online reindexer")
 public class IndexStartCommand extends SshCommand {
 
   @Argument(index = 0, required = true, metaVar = "INDEX",
@@ -47,7 +44,7 @@
         stdout.println("Nothing to reindex, index is already the latest version");
       }
     } catch (ReindexerAlreadyRunningException e) {
-      throw new UnloggedFailure("Failed to start reindexer: " + e.getMessage());
+      throw die("Failed to start reindexer: " + e.getMessage());
     }
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
index bd97286..2e11ef9 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
@@ -49,7 +49,7 @@
   @Override
   public void run() throws Exception {
     if (impl.getUser() != null && !impl.getProjects().isEmpty()) {
-      throw new UnloggedFailure(1, "fatal: --user and --project options are not compatible.");
+      throw die("--user and --project options are not compatible.");
     }
     impl.display(stdout);
   }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
index 134a719..d81c153 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
@@ -34,10 +34,10 @@
     if (!impl.getFormat().isJson()) {
       List<String> showBranch = impl.getShowBranch();
       if (impl.isShowTree() && (showBranch != null) && !showBranch.isEmpty()) {
-        throw new UnloggedFailure(1, "fatal: --tree and --show-branch options are not compatible.");
+        throw die("--tree and --show-branch options are not compatible.");
       }
       if (impl.isShowTree() && impl.isShowDescription()) {
-        throw new UnloggedFailure(1, "fatal: --tree and --description options are not compatible.");
+        throw die("--tree and --description options are not compatible.");
       }
     }
     impl.display(out);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
index a70d581..1ac347f 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
@@ -109,11 +109,10 @@
             + projectControl.getProject().getNameKey(), e);
       }
     } catch (RepositoryNotFoundException e) {
-      throw new UnloggedFailure("fatal: '"
-          + projectControl.getProject().getNameKey() + "': not a git archive");
+      throw die("'" + projectControl.getProject().getNameKey()
+          + "': not a git archive");
     } catch (IOException e) {
-      throw new UnloggedFailure("fatal: Error opening: '"
-          + projectControl.getProject().getNameKey());
+      throw die("Error opening: '" + projectControl.getProject().getNameKey());
     }
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
index f65a0c9..8bde743 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
@@ -102,7 +102,7 @@
     super.parseCommandLine();
     if (processor.getIncludeFiles() &&
         !(processor.getIncludePatchSets() || processor.getIncludeCurrentPatchSet())) {
-      throw new UnloggedFailure(1, "--files option needs --patch-sets or --current-patch-set");
+      throw die("--files option needs --patch-sets or --current-patch-set");
     }
   }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java
index 0b12aa6..011cb91 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java
@@ -83,7 +83,7 @@
 
     Capable r = receive.canUpload();
     if (r != Capable.OK) {
-      throw new UnloggedFailure(1, "\nfatal: " + r.getMessage());
+      throw die(r.getMessage());
     }
 
     verifyProjectVisible("reviewer", reviewerId);
@@ -165,7 +165,7 @@
     for (final Account.Id id : who) {
       final IdentifiedUser user = identifiedUserFactory.create(id);
       if (!projectControl.forUser(user).isVisible()) {
-        throw new UnloggedFailure(1, type + " "
+        throw die(type + " "
             + user.getAccount().getFullName() + " cannot access the project");
       }
     }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
index dad8672..3c5d5a3 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -153,68 +153,68 @@
   protected void run() throws UnloggedFailure {
     if (abandonChange) {
       if (restoreChange) {
-        throw error("abandon and restore actions are mutually exclusive");
+        throw die("abandon and restore actions are mutually exclusive");
       }
       if (submitChange) {
-        throw error("abandon and submit actions are mutually exclusive");
+        throw die("abandon and submit actions are mutually exclusive");
       }
       if (publishPatchSet) {
-        throw error("abandon and publish actions are mutually exclusive");
+        throw die("abandon and publish actions are mutually exclusive");
       }
       if (deleteDraftPatchSet) {
-        throw error("abandon and delete actions are mutually exclusive");
+        throw die("abandon and delete actions are mutually exclusive");
       }
       if (rebaseChange) {
-        throw error("abandon and rebase actions are mutually exclusive");
+        throw die("abandon and rebase actions are mutually exclusive");
       }
     }
     if (publishPatchSet) {
       if (restoreChange) {
-        throw error("publish and restore actions are mutually exclusive");
+        throw die("publish and restore actions are mutually exclusive");
       }
       if (submitChange) {
-        throw error("publish and submit actions are mutually exclusive");
+        throw die("publish and submit actions are mutually exclusive");
       }
       if (deleteDraftPatchSet) {
-        throw error("publish and delete actions are mutually exclusive");
+        throw die("publish and delete actions are mutually exclusive");
       }
     }
     if (json) {
       if (restoreChange) {
-        throw error("json and restore actions are mutually exclusive");
+        throw die("json and restore actions are mutually exclusive");
       }
       if (submitChange) {
-        throw error("json and submit actions are mutually exclusive");
+        throw die("json and submit actions are mutually exclusive");
       }
       if (deleteDraftPatchSet) {
-        throw error("json and delete actions are mutually exclusive");
+        throw die("json and delete actions are mutually exclusive");
       }
       if (publishPatchSet) {
-        throw error("json and publish actions are mutually exclusive");
+        throw die("json and publish actions are mutually exclusive");
       }
       if (abandonChange) {
-        throw error("json and abandon actions are mutually exclusive");
+        throw die("json and abandon actions are mutually exclusive");
       }
       if (changeComment != null) {
-        throw error("json and message are mutually exclusive");
+        throw die("json and message are mutually exclusive");
       }
       if (rebaseChange) {
-        throw error("json and rebase actions are mutually exclusive");
+        throw die("json and rebase actions are mutually exclusive");
       }
       if (changeTag != null) {
-        throw error("json and tag actions are mutually exclusive");
+        throw die("json and tag actions are mutually exclusive");
       }
     }
     if (rebaseChange) {
       if (deleteDraftPatchSet) {
-        throw error("rebase and delete actions are mutually exclusive");
+        throw die("rebase and delete actions are mutually exclusive");
       }
       if (submitChange) {
-        throw error("rebase and submit actions are mutually exclusive");
+        throw die("rebase and submit actions are mutually exclusive");
       }
     }
     if (deleteDraftPatchSet && submitChange) {
-      throw error("delete and submit actions are mutually exclusive");
+      throw die("delete and submit actions are mutually exclusive");
     }
 
     boolean ok = true;
@@ -232,20 +232,21 @@
         }
       } catch (RestApiException | UnloggedFailure e) {
         ok = false;
-        writeError("error: " + e.getMessage() + "\n");
+        writeError("error", e.getMessage() + "\n");
       } catch (NoSuchChangeException e) {
         ok = false;
-        writeError("no such change " + patchSet.getId().getParentKey().get());
+        writeError("error",
+            "no such change " + patchSet.getId().getParentKey().get());
       } catch (Exception e) {
         ok = false;
-        writeError("fatal: internal server error while reviewing "
+        writeError("fatal", "internal server error while reviewing "
             + patchSet.getId() + "\n");
         log.error("internal error while reviewing " + patchSet.getId(), e);
       }
     }
 
     if (!ok) {
-      throw error("one or more reviews failed; review output above");
+      throw die("one or more reviews failed; review output above");
     }
   }
 
@@ -262,8 +263,8 @@
       return OutputFormat.JSON.newGson().
           fromJson(CharStreams.toString(r), ReviewInput.class);
     } catch (IOException | JsonSyntaxException e) {
-      writeError(e.getMessage() + '\n');
-      throw error("internal error while reading review input");
+      writeError("error", e.getMessage() + '\n');
+      throw die("internal error while reading review input");
     }
   }
 
@@ -321,7 +322,7 @@
         revisionApi(patchSet).delete();
       }
     } catch (IllegalStateException | RestApiException e) {
-      throw error(e.getMessage());
+      throw die(e);
     }
   }
 
@@ -342,7 +343,7 @@
     try {
       allProjectsControl = projectControlFactory.controlFor(allProjects);
     } catch (NoSuchProjectException e) {
-      throw new UnloggedFailure("missing " + allProjects.get());
+      throw die("missing " + allProjects.get());
     }
 
     for (LabelType type : allProjectsControl.getLabelTypes().getLabelTypes()) {
@@ -360,16 +361,4 @@
 
     super.parseCommandLine();
   }
-
-  private void writeError(final String msg) {
-    try {
-      err.write(msg.getBytes(ENC));
-    } catch (IOException e) {
-      // Ignored
-    }
-  }
-
-  private static UnloggedFailure error(final String msg) {
-    return new UnloggedFailure(1, msg);
-  }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
index 6e03ac1..535f79a 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
@@ -145,16 +145,14 @@
 
   private void validate() throws UnloggedFailure {
     if (active && inactive) {
-      throw new UnloggedFailure(1,
-          "--active and --inactive options are mutually exclusive.");
+      throw die("--active and --inactive options are mutually exclusive.");
     }
     if (clearHttpPassword && !Strings.isNullOrEmpty(httpPassword)) {
-      throw new UnloggedFailure(1,
-          "--http-password and --clear-http-password options are mutually " +
-          "exclusive.");
+      throw die("--http-password and --clear-http-password options are "
+          + "mutually exclusive.");
     }
     if (addSshKeys.contains("-") && deleteSshKeys.contains("-")) {
-      throw new UnloggedFailure(1, "Only one option may use the stdin");
+      throw die("Only one option may use the stdin");
     }
     if (deleteSshKeys.contains("ALL")) {
       deleteSshKeys = Collections.singletonList("ALL");
@@ -163,8 +161,7 @@
       deleteEmails = Collections.singletonList("ALL");
     }
     if (deleteEmails.contains(preferredEmail)) {
-      throw new UnloggedFailure(1,
-          "--preferred-email and --delete-email options are mutually " +
+      throw die("--preferred-email and --delete-email options are mutually " +
           "exclusive for the same email address.");
     }
   }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetHeadCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetHeadCommand.java
index b1d1605..4fef018 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetHeadCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetHeadCommand.java
@@ -49,7 +49,7 @@
     try {
       setHead.apply(new ProjectResource(project), input);
     } catch (UnprocessableEntityException e) {
-      throw new UnloggedFailure("fatal: " + e.getMessage());
+      throw die(e);
     }
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
index 589fbf0..6328fb4 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
@@ -155,7 +155,7 @@
       md.setMessage("Project settings updated");
       config.commit(md);
     } catch (RepositoryNotFoundException notFound) {
-      err.append("error: Project ").append(name).append(" not found\n");
+      err.append("Project ").append(name).append(" not found\n");
     } catch (IOException | ConfigInvalidException e) {
       final String msg = "Cannot update project " + name;
       log.error(msg, e);
@@ -167,7 +167,7 @@
       while (err.charAt(err.length() - 1) == '\n') {
         err.setLength(err.length() - 1);
       }
-      throw new UnloggedFailure(1, err.toString());
+      throw die(err.toString());
     }
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
index 63cb54f..ac64803 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
@@ -18,17 +18,12 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ChangeFinder;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.ChangeResource;
-import com.google.gerrit.server.change.ChangesCollection;
 import com.google.gerrit.server.change.DeleteReviewer;
 import com.google.gerrit.server.change.PostReviewers;
 import com.google.gerrit.server.change.ReviewerResource;
-import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.sshd.ChangeArgumentParser;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gwtorm.server.OrmException;
@@ -39,7 +34,6 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
@@ -63,10 +57,10 @@
     toRemove.add(who);
   }
 
-  @Argument(index = 0, required = true, multiValued = true, metaVar = "COMMIT", usage = "changes to modify")
+  @Argument(index = 0, required = true, multiValued = true, metaVar = "CHANGE", usage = "changes to modify")
   void addChange(String token) {
     try {
-      addChangeImpl(token);
+      changeArgumentParser.addChange(token, changes, projectControl);
     } catch (UnloggedFailure e) {
       throw new IllegalArgumentException(e.getMessage(), e);
     } catch (OrmException e) {
@@ -75,9 +69,6 @@
   }
 
   @Inject
-  private ReviewDb db;
-
-  @Inject
   private ReviewerResource.Factory reviewerFactory;
 
   @Inject
@@ -87,13 +78,7 @@
   private DeleteReviewer deleteReviewer;
 
   @Inject
-  private CurrentUser currentUser;
-
-  @Inject
-  private ChangesCollection changesCollection;
-
-  @Inject
-  private ChangeFinder changeFinder;
+  private ChangeArgumentParser changeArgumentParser;
 
   private Set<Account.Id> toRemove = new HashSet<>();
 
@@ -113,7 +98,7 @@
     }
 
     if (!ok) {
-      throw error("fatal: one or more updates failed; review output above");
+      throw die("one or more updates failed; review output above");
     }
   }
 
@@ -159,48 +144,4 @@
 
     return ok;
   }
-
-  private void addChangeImpl(String id) throws UnloggedFailure, OrmException {
-    List<ChangeControl> matched = changeFinder.find(id, currentUser);
-    List<ChangeControl> toAdd = new ArrayList<>(changes.size());
-    for (ChangeControl ctl : matched) {
-      if (!changes.containsKey(ctl.getId()) && inProject(ctl.getProject())
-          && ctl.isVisible(db)) {
-        toAdd.add(ctl);
-      }
-    }
-    switch (toAdd.size()) {
-      case 0:
-        throw error("\"" + id + "\" no such change");
-
-      case 1:
-        ChangeControl ctl = toAdd.get(0);
-        changes.put(ctl.getId(), changesCollection.parse(ctl));
-        break;
-
-      default:
-        throw error("\"" + id + "\" matches multiple changes");
-    }
-  }
-
-  private boolean inProject(Project project) {
-    if (projectControl != null) {
-      return projectControl.getProject().getNameKey().equals(project.getNameKey());
-    } else {
-      // No --project option, so they want every project.
-      return true;
-    }
-  }
-
-  private void writeError(String type, String msg) {
-    try {
-      err.write((type + ": " + msg + "\n").getBytes(ENC));
-    } catch (IOException e) {
-      // Ignored
-    }
-  }
-
-  private static UnloggedFailure error(String msg) {
-    return new UnloggedFailure(1, msg);
-  }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
index 2da1b6d..9e6630a 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.sshd.commands;
 
-import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.Supplier;
@@ -48,8 +47,7 @@
 import java.util.concurrent.LinkedBlockingQueue;
 
 @RequiresCapability(GlobalCapability.STREAM_EVENTS)
-@CommandMetaData(name = "stream-events", description = "Monitor events occurring in real time",
-  runsAt = MASTER)
+@CommandMetaData(name = "stream-events", description = "Monitor events occurring in real time")
 final class StreamEvents extends BaseCommand {
   /** Maximum number of events that may be queued up for each connection. */
   private static final int MAX_EVENTS = 128;
diff --git a/lib/BUCK b/lib/BUCK
index 0c424ad..65bdd5e 100644
--- a/lib/BUCK
+++ b/lib/BUCK
@@ -257,8 +257,8 @@
 
 maven_jar(
   name = 'javassist',
-  id = 'org.javassist:javassist:3.18.1-GA',
-  sha1 = 'd9a09f7732226af26bf99f19e2cffe0ae219db5b',
+  id = 'org.javassist:javassist:3.20.0-GA',
+  sha1 = 'a9cbcdfb7e9f86fbc74d3afae65f2248bfbf82a0',
   license = 'DO_NOT_DISTRIBUTE',
 )
 
diff --git a/lib/auto/BUCK b/lib/auto/BUCK
index c688ee4..149f2d1 100644
--- a/lib/auto/BUCK
+++ b/lib/auto/BUCK
@@ -2,12 +2,8 @@
 
 maven_jar(
   name = 'auto-value',
-  id = 'com.google.auto.value:auto-value:1.1',
-  sha1 = 'f6951c141ea3e89c0f8b01da16834880a1ebf162',
-  # Exclude un-relocated dependencies and replace with our own versions; see
-  # https://github.com/google/auto/blob/auto-value-1.1/value/pom.xml#L151
-  exclude = ['org/apache/*'],
-  deps = ['//lib:velocity'],
+  id = 'com.google.auto.value:auto-value:1.2',
+  sha1 = '6873fed014fe1de1051aae2af68ba266d2934471',
   license = 'Apache2.0',
   visibility = ['PUBLIC'],
 )
diff --git a/lib/codemirror/BUCK b/lib/codemirror/BUCK
index 9b8a146..67779ab 100644
--- a/lib/codemirror/BUCK
+++ b/lib/codemirror/BUCK
@@ -1,14 +1,14 @@
 include_defs('//lib/maven.defs')
 include_defs('//lib/codemirror/cm.defs')
 
-VERSION = '5.14.2'
+VERSION = '5.15.2'
 TOP = 'META-INF/resources/webjars/codemirror/%s' % VERSION
 TOP_MINIFIED = 'META-INF/resources/webjars/codemirror-minified/%s' % VERSION
 
 maven_jar(
   name = 'codemirror-minified',
   id = 'org.webjars.npm:codemirror-minified:' + VERSION,
-  sha1 = 'c69056d2a0e07432326e67ea8fe2abb91a065030',
+  sha1 = '222152d6c4f9da6e812378499894e6f86688ac2a',
   attach_source = False,
   license = 'codemirror-minified',
   visibility = [],
@@ -17,7 +17,7 @@
 maven_jar(
   name = 'codemirror-original',
   id = 'org.webjars.npm:codemirror:' + VERSION,
-  sha1 = '1ed9697531be85c85edb70fcdf58f10045563f7b',
+  sha1 = '2f0c3e94bb133df1f07800ff0e361da8ac791442',
   attach_source = False,
   license = 'codemirror-original',
   visibility = [],
diff --git a/lib/easymock/BUCK b/lib/easymock/BUCK
index c0cb77b..93640a0 100644
--- a/lib/easymock/BUCK
+++ b/lib/easymock/BUCK
@@ -2,9 +2,9 @@
 
 maven_jar(
   name = 'easymock',
-  id = 'org.easymock:easymock:3.3.1', # When bumping the version
+  id = 'org.easymock:easymock:3.4', # When bumping the version
   # number, make sure to also move powermock to a compatible version
-  sha1 = 'a497d7f00c9af78b72b6d8f24762d9210309148a',
+  sha1 = '9fdeea183a399f25c2469497612cad131e920fa3',
   license = 'DO_NOT_DISTRIBUTE',
   deps = [
     ':cglib-2_2',
@@ -22,8 +22,8 @@
 
 maven_jar(
   name = 'objenesis',
-  id = 'org.objenesis:objenesis:2.1',
-  sha1 = '87c0ea803b69252868d09308b4618f766f135a96',
+  id = 'org.objenesis:objenesis:2.2',
+  sha1 = '3fb533efdaa50a768c394aa4624144cf8df17845',
   license = 'DO_NOT_DISTRIBUTE',
   visibility = ['//lib/powermock:powermock-reflect'],
   attach_source = False,
diff --git a/lib/js/BUCK b/lib/js/BUCK
index 36a3d19..275941c 100644
--- a/lib/js/BUCK
+++ b/lib/js/BUCK
@@ -17,8 +17,8 @@
 
 npm_binary(
   name = 'bower',
-  version = '1.6.5',
-  sha1 = '59d457122a161e42cc1625bbab8179c214b7ac11',
+  version = '1.7.9',
+  sha1 = 'b7296c2393e0d75edaa6ca39648132dd255812b0',
 )
 
 npm_binary(
@@ -102,17 +102,9 @@
 bower_component(
   name = 'fetch',
   package = 'fetch',
-  version = '0.11.0',
+  version = '1.0.0',
   license = 'fetch',
-  sha1 = 'a55d4e291821958d9d400bb3184c12bb367dc670',
-)
-
-bower_component(
-  name = 'font-roboto',
-  package = 'polymerelements/font-roboto',
-  version = '1.0.1',
-  license = 'polymer',
-  sha1 = '735676217f67221903d6be10cc2fb1b336bed13f',
+  sha1 = '1b05a2bb40c73232c2909dc196de7519fe4db7a9',
 )
 
 bower_component(
@@ -127,10 +119,10 @@
 bower_component(
   name = 'iron-a11y-keys-behavior',
   package = 'polymerelements/iron-a11y-keys-behavior',
-  version = '1.1.1',
+  version = '1.1.2',
   deps = [':polymer'],
   license = 'polymer',
-  sha1 = '6bb52b967a4fb242897520dad6c366135e3813ce',
+  sha1 = '57fd39ee153ce37ed719ba3f7a405afb987d54f9',
 )
 
 bower_component(
@@ -151,19 +143,19 @@
 bower_component(
   name = 'iron-behaviors',
   package = 'polymerelements/iron-behaviors',
-  version = '1.0.13',
+  version = '1.0.16',
   deps = [
     ':iron-a11y-keys-behavior',
     ':polymer',
   ],
   license = 'polymer',
-  sha1 = 'e9bcdac5414cb8282b5f75eeb51c9154380045af',
+  sha1 = 'bd70636a2c0a78c50d1a76f9b8ca1ffd815478a3',
 )
 
 bower_component(
   name = 'iron-dropdown',
   package = 'polymerelements/iron-dropdown',
-  version = '1.3.0',
+  version = '1.4.0',
   deps = [
     ':iron-a11y-keys-behavior',
     ':iron-behaviors',
@@ -173,16 +165,16 @@
     ':polymer',
   ],
   license = 'polymer',
-  sha1 = '08ae9c9fa2f2c19a8ab330dfe8240292c8d161cf',
+  sha1 = '63e3d669a09edaa31c4f05afc76b53b919ef0595',
 )
 
 bower_component(
   name = 'iron-fit-behavior',
   package = 'polymerelements/iron-fit-behavior',
-  version = '1.0.6',
+  version = '1.2.2',
   deps = [':polymer'],
   license = 'polymer',
-  sha1 = '28df0349d3cb20ac5e4aeb40651ef7d84de75fb0',
+  sha1 = 'bc53e9bab36b21f086ab8fac8c53cc7214aa1890',
 )
 
 bower_component(
@@ -206,14 +198,14 @@
 bower_component(
   name = 'iron-input',
   package = 'polymerelements/iron-input',
-  version = '1.0.9',
+  version = '1.0.10',
   deps = [
     ':iron-a11y-announcer',
     ':iron-validatable-behavior',
     ':polymer',
   ],
   license = 'polymer',
-  sha1 = '4e415c2511ec8ff6c8b17249ec8f02e8d8b1a0d9',
+  sha1 = '9bc0c8e81de2527125383cbcf74dd9f27e7fa9ac',
 )
 
 bower_component(
@@ -228,7 +220,7 @@
 bower_component(
   name = 'iron-overlay-behavior',
   package = 'polymerelements/iron-overlay-behavior',
-  version = '1.4.2',
+  version = '1.7.6',
   deps = [
     ':iron-a11y-keys-behavior',
     ':iron-fit-behavior',
@@ -236,7 +228,7 @@
     ':polymer',
   ],
   license = 'polymer',
-  sha1 = 'babdd95d7efd63bf3f2969a8f1036e8f324979a9',
+  sha1 = '83181085fda59446ce74fd0d5ca30c223f38ee4a',
 )
 
 bower_component(
@@ -251,31 +243,31 @@
 bower_component(
   name = 'iron-selector',
   package = 'polymerelements/iron-selector',
-  version = '1.2.5',
+  version = '1.5.2',
   deps = [':polymer'],
   license = 'polymer',
-  sha1 = '7728750bc9dfa858915dfd25397709bdbdaee2b1',
+  sha1 = 'c57235dfda7fbb987c20ad0e97aac70babf1a1bf',
 )
 
 bower_component(
   name = 'iron-test-helpers',
   package = 'polymerelements/iron-test-helpers',
-  version = '1.1.5',
+  version = '1.2.5',
   deps = [':polymer'],
   license = 'DO_NOT_DISTRIBUTE',
-  sha1 = '000e2256ae487e4d24edfb6d17dc98626bb8a8e2',
+  sha1 = '433b03b106f5ff32049b84150cd70938e18b67ac',
 )
 
 bower_component(
   name = 'iron-validatable-behavior',
   package = 'polymerelements/iron-validatable-behavior',
-  version = '1.0.5',
+  version = '1.1.1',
   deps = [
     ':iron-meta',
     ':polymer',
   ],
   license = 'polymer',
-  sha1 = '5a68250d6d9abcd576f116dc4fc7312426323883',
+  sha1 = '480423380be0536f948735d91bc472f6e7ced5b4',
 )
 
 bower_component(
@@ -289,23 +281,23 @@
 bower_component(
   name = 'mocha',
   package = 'mocha',
-  version = '2.4.5',
+  version = '2.5.1',
   license = 'DO_NOT_DISTRIBUTE',
-  sha1 = 'efbb1675710c0ba94a44eb7a4d27040229283197',
+  sha1 = 'cb29bdd1047cfd9304659ecf10ec263f9c888c99',
 )
 
 bower_component(
   name = 'moment',
   package = 'moment/moment',
-  version = '2.12.0',
+  version = '2.13.0',
   license = 'moment',
-  sha1 = '508d53de8f49ab87f03e209e5073e339107ed3e6',
+  sha1 = 'fc8ce2c799bab21f6ced7aff928244f4ca8880aa',
 )
 
 bower_component(
   name = 'neon-animation',
   package = 'polymerelements/neon-animation',
-  version = '1.1.1',
+  version = '1.2.3',
   deps = [
     ':iron-meta',
     ':iron-resizable-behavior',
@@ -314,7 +306,7 @@
     ':web-animations-js',
   ],
   license = 'polymer',
-  sha1 = 'd6e1b45e5a936d0ec0b66b3520e230e9d8605642',
+  sha1 = '71cc0d3e0afdf8b8563e87d2ff03a6fa19183bd9',
 )
 
 bower_component(
@@ -326,19 +318,6 @@
 )
 
 bower_component(
-  name = 'paper-styles',
-  package = 'polymerelements/paper-styles',
-  version = '1.1.4',
-  deps = [
-    ':font-roboto',
-    ':iron-flex-layout',
-    ':polymer',
-  ],
-  license = 'polymer',
-  sha1 = '89276c5ec18b8927a704dda2bf14ff35c310401a',
-)
-
-bower_component(
   name = 'polymer',
   package = 'polymer/polymer',
   version = '1.4.0',
@@ -383,17 +362,17 @@
 bower_component(
   name = 'test-fixture',
   package = 'polymerelements/test-fixture',
-  version = '1.1.0',
+  version = '1.1.1',
   license = 'DO_NOT_DISTRIBUTE',
-  sha1 = '4afc8998ae42b0421847906a7550b997c6fdc088',
+  sha1 = 'e373bd21c069163c3a754e234d52c07c77b20d3c',
 )
 
 bower_component(
   name = 'web-animations-js',
   package = 'web-animations/web-animations-js',
-  version = '2.1.4',
+  version = '2.2.1',
   license = 'Apache2.0',
-  sha1 = '92f06d8417a51f1f75c94b7a19616e19695cc6db',
+  sha1 = '0e73b263a86aa6764ad35c273eb12055f83d7eda',
 )
 
 bower_component(
@@ -418,7 +397,8 @@
 bower_component(
   name = 'webcomponentsjs',
   package = 'webcomponentsjs',
-  version = '0.7.21',
+  version = '0.7.22',
   license = 'polymer',
-  sha1 = 'ceb96b01c8a86b17831a25d6ab9eca95226c408e',
+  sha1 = '8ba97a4a279ec6973a19b171c462a7b5cf454fb9',
 )
+
diff --git a/lib/powermock/BUCK b/lib/powermock/BUCK
index 221a640..b642457 100644
--- a/lib/powermock/BUCK
+++ b/lib/powermock/BUCK
@@ -1,12 +1,12 @@
 include_defs('//lib/maven.defs')
 
-VERSION = '1.6.2' # When bumping VERSION, make sure to also move
+VERSION = '1.6.4' # When bumping VERSION, make sure to also move
 # easymock to a compatible version
 
 maven_jar(
   name = 'powermock-module-junit4',
   id = 'org.powermock:powermock-module-junit4:' + VERSION,
-  sha1 = 'dff58978da716e000463bc1b08013d6a7cf3d696',
+  sha1 = '8692eb1d9bb8eb1310ffe8a20c2da7ee6d1b5994',
   license = 'DO_NOT_DISTRIBUTE',
   deps = [
     ':powermock-module-junit4-common',
@@ -17,7 +17,7 @@
 maven_jar(
   name = 'powermock-module-junit4-common',
   id = 'org.powermock:powermock-module-junit4-common:' + VERSION,
-  sha1 = '48dd7406e11a14fe2ae4ab641e1f27510e896640',
+  sha1 = 'b0b578da443794ceb8224bd5f5f852aaf40f1b81',
   license = 'DO_NOT_DISTRIBUTE',
   deps = [
     ':powermock-reflect',
@@ -28,7 +28,7 @@
 maven_jar(
   name = 'powermock-reflect',
   id = 'org.powermock:powermock-reflect:' + VERSION,
-  sha1 = '1af1bbd1207c3ecdcf64973e6f9d57dcd17cc145',
+  sha1 = '5532f4e7c42db4bca4778bc9f1afcd4b0ee0b893',
   license = 'DO_NOT_DISTRIBUTE',
   deps = [
     '//lib:junit',
@@ -39,7 +39,7 @@
 maven_jar(
   name = 'powermock-api-easymock',
   id = 'org.powermock:powermock-api-easymock:' + VERSION,
-  sha1 = 'addd25742ac9fe3e0491cbd68e2515e3b06c77fd',
+  sha1 = '5c385a0d8c13f84b731b75c6e90319c532f80b45',
   license = 'DO_NOT_DISTRIBUTE',
   deps = [
     ':powermock-api-support',
@@ -50,7 +50,7 @@
 maven_jar(
   name = 'powermock-api-support',
   id = 'org.powermock:powermock-api-support:' + VERSION,
-  sha1 = '93b21413b4ee99b7bc0dd34e1416fdca96866aaf',
+  sha1 = '314daafb761541293595630e10a3699ebc07881d',
   license = 'DO_NOT_DISTRIBUTE',
   deps = [
     ':powermock-core',
@@ -62,7 +62,7 @@
 maven_jar(
   name = 'powermock-core',
   id = 'org.powermock:powermock-core:' + VERSION,
-  sha1 = 'ea04e79244e19dcf0c3ccf6863c5b028b4b58c9c',
+  sha1 = '85fb32e9ccba748d569fc36aef92e0b9e7f40b87',
   license = 'DO_NOT_DISTRIBUTE',
   deps = [
     ':powermock-reflect',
diff --git a/plugins/replication b/plugins/replication
index 4883981..b3ab82d 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 4883981228e871f9b58c01197c01d93ded6250d2
+Subproject commit b3ab82de95bedd46a60152e2ecffdab1f762e00d
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index c167df0..3f3d572 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit c167df08a8550d8c6c7ccf12b7df4fa6bfc6d432
+Subproject commit 3f3d572e9618f268b19cc54856deee4c96180e4c
diff --git a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html
new file mode 100644
index 0000000..6b35328
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html
@@ -0,0 +1,18 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<link rel="import" href="../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../elements/shared/gr-tooltip/gr-tooltip.html">
+<script src="gr-tooltip-behavior.js"></script>
diff --git a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
new file mode 100644
index 0000000..2da6846
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
@@ -0,0 +1,110 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function(window) {
+  'use strict';
+
+  var BOTTOM_OFFSET = 10;
+
+  var TooltipBehavior = {
+
+    properties: {
+      hasTooltip: Boolean,
+
+      _tooltip: Element,
+      _titleText: String,
+    },
+
+    attached: function() {
+      if (!this.hasTooltip) { return; }
+
+      this.addEventListener('mouseover', this._handleShowTooltip.bind(this));
+      this.addEventListener('mouseout', this._handleHideTooltip.bind(this));
+      this.addEventListener('focusin', this._handleShowTooltip.bind(this));
+      this.addEventListener('focusout', this._handleHideTooltip.bind(this));
+      this.listen(window, 'scroll', '_handleWindowScroll');
+    },
+
+    detached: function() {
+      this.unlisten(window, 'scroll', '_handleWindowScroll');
+    },
+
+    _handleShowTooltip: function(e) {
+      if (!this.hasAttribute('title') || this._tooltip) { return; }
+
+      // Store the title attribute text then set it to an empty string to
+      // prevent it from showing natively.
+      this._titleText = this.getAttribute('title');
+      this.setAttribute('title', '');
+
+      var tooltip = document.createElement('gr-tooltip');
+      tooltip.text = this._titleText;
+
+      // Set visibility to hidden before appending to the DOM so that
+      // calculations can be made based on the element’s size.
+      tooltip.style.visibility = 'hidden';
+      Polymer.dom(document.body).appendChild(tooltip);
+      this._positionTooltip(tooltip);
+      tooltip.style.visibility = null;
+
+      this._tooltip = tooltip;
+    },
+
+    _handleHideTooltip: function(e) {
+      if (!this.hasAttribute('title') ||
+          !this._titleText ||
+          this === document.activeElement) { return; }
+
+      this.setAttribute('title', this._titleText);
+      if (this._tooltip && this._tooltip.parentNode) {
+        this._tooltip.parentNode.removeChild(this._tooltip);
+      }
+      this._tooltip = null;
+    },
+
+    _handleWindowScroll: function(e) {
+      if (!this._tooltip) { return; }
+
+      this._positionTooltip(this._tooltip);
+    },
+
+    _positionTooltip: function(tooltip) {
+      var offset = this._getOffset(this);
+      var top = offset.top;
+      var left = offset.left;
+
+      top -= this.offsetHeight + BOTTOM_OFFSET;
+      left -= (tooltip.offsetWidth / 2) - (this.offsetWidth / 2);
+      left = Math.max(0, left);
+      top = Math.max(0, top);
+
+      tooltip.style.left = left + 'px';
+      tooltip.style.top = top + 'px';
+    },
+
+    _getOffset: function(el) {
+      var top = 0;
+      var left = 0;
+      for (var node = el; node; node = node.offsetParent) {
+        if (node.offsetTop) { top += node.offsetTop; }
+        if (node.offsetLeft) { left += node.offsetLeft; }
+      }
+      top += window.scrollY;
+      left += window.scrollX;
+      return {top: top, left: left};
+    },
+  };
+
+  window.Gerrit = window.Gerrit || {};
+  window.Gerrit.TooltipBehavior = TooltipBehavior;
+})(window);
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index dd63764..544ad4c 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -62,10 +62,7 @@
       },
       changeNum: String,
       patchNum: String,
-      commitInfo: {
-        type: Object,
-        readOnly: true,
-      },
+      commitInfo: Object,
       _loading: {
         type: Boolean,
         value: true,
@@ -237,11 +234,6 @@
 
     _handleRevertDialogConfirm: function() {
       var el = this.$.confirmRevertDialog;
-      if (!el.message) {
-        // TODO(viktard): Fix validation.
-        alert('The revert commit message can’t be empty.');
-        return;
-      }
       this.$.overlay.close();
       el.hidden = false;
       this._fireAction(
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index 098e097..b0f5a5c 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -221,12 +221,6 @@
         fireActionStub.restore();
       });
 
-      test('validation', function() {
-        element._handleRevertDialogConfirm();
-        assert.notOk(fireActionStub.called);
-        assert.ok(alertStub.called);
-      });
-
       test('works', function() {
         var revertButton = element.$$('gr-button[data-action-key="revert"]');
         MockInteractions.tap(revertButton);
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
index d416e14..5f76a79 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -104,6 +104,18 @@
       <span class="value">[[change.branch]]</span>
     </section>
     <section>
+      <span class="title">Commit</span>
+      <span class="value">
+        <template is="dom-if" if="[[_computeShowWebLink(commitInfo)]]">
+          <a target="_blank"
+              href$="[[_computeWebLink(commitInfo)]]">[[_computeShortHash(change)]]</a>
+        </template>
+        <template is="dom-if" if="[[!_computeShowWebLink(commitInfo)]]">
+          [[_computeShortHash(change)]]
+        </template>
+      </span>
+    </section>
+    <section>
       <span class="title">Topic</span>
       <span class="value">[[change.topic]]</span>
     </section>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index 4fa98ca..8e2e000 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -27,6 +27,7 @@
 
     properties: {
       change: Object,
+      commitInfo: Object,
       mutable: Boolean,
       serverConfig: Object,
     },
@@ -35,6 +36,18 @@
       Gerrit.RESTClientBehavior,
     ],
 
+    _computeShowWebLink: function(commitInfo) {
+      return commitInfo.web_links && commitInfo.web_links.length;
+    },
+
+    _computeWebLink: function(commitInfo) {
+      return '../../' + commitInfo.web_links[0].url;
+    },
+
+    _computeShortHash: function(change) {
+      return change.current_revision.slice(0, 6);
+    },
+
     _computeHideStrategy: function(change) {
       return !this.changeIsOpen(change.status);
     },
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index 7ab23a1..25eacc2 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -220,12 +220,12 @@
             <gr-change-star change="{{_change}}" hidden$="[[!_loggedIn]]"></gr-change-star>
             <a href$="[[_computeChangePermalink(_change._number)]]">[[_change._number]]</a><span>:</span>
             <span>[[_change.subject]]</span>
-            <span class="changeStatus">[[_computeChangeStatus(_change, _patchNum)]]</span>
+            <span class="changeStatus">[[_computeChangeStatus(_change, _patchRange.patchNum)]]</span>
           </span>
           <span class="header-actions">
             <gr-button hidden
                 class="reply"
-                primary$="[[_computeReplyButtonHighlighted(_diffDrafts)]]"
+                primary$="[[_computeReplyButtonHighlighted(_diffDrafts.*)]]"
                 hidden$="[[!_loggedIn]]"
                 on-tap="_handleReplyTap">[[_replyButtonLabel]]</gr-button>
             <gr-button class="download" on-tap="_handleDownloadTap">Download</gr-button>
@@ -233,7 +233,7 @@
               <label class="patchSelectLabel" for="patchSetSelect">Patch set</label>
               <select id="patchSetSelect" on-change="_handlePatchChange">
                 <template is="dom-repeat" items="{{_allPatchSets}}" as="patchNumber">
-                  <option value$="[[patchNumber]]" selected$="[[_computePatchIndexIsSelected(index, _patchNum)]]">
+                  <option value$="[[patchNumber]]" selected$="[[_computePatchIndexIsSelected(index, _patchRange.patchNum)]]">
                     <span>[[patchNumber]]</span>
                     /
                     <span>[[_computeLatestPatchNum(_change)]]</span>
@@ -248,12 +248,13 @@
         <div class="changeInfo-column changeMetadata">
           <gr-change-metadata
               change="[[_change]]"
+              commit-info="[[_commitInfo]]"
               server-config="[[serverConfig]]"
               mutable="[[_loggedIn]]"></gr-change-metadata>
           <gr-change-actions id="actions"
               actions="[[_change.actions]]"
               change-num="[[_changeNum]]"
-              patch-num="[[_patchNum]]"
+              patch-num="[[_patchRange.patchNum]]"
               commit-info="[[_commitInfo]]"
               on-reload-change="_handleReloadChange"></gr-change-actions>
         </div>
@@ -267,15 +268,18 @@
           <div class="relatedChanges">
             <gr-related-changes-list id="relatedChanges"
                 change="[[_change]]"
-                patch-num="[[_patchNum]]"></gr-related-changes-list>
+                patch-num="[[_patchRange.patchNum]]"></gr-related-changes-list>
           </div>
         </div>
       </section>
       <gr-file-list id="fileList"
+          change="[[_change]]"
           change-num="[[_changeNum]]"
-          patch-num="[[_patchNum]]"
+          patch-range="[[_patchRange]]"
           comments="[[_comments]]"
           drafts="[[_diffDrafts]]"
+          revisions="[[_change.revisions]]"
+          projectConfig="[[_projectConfig]]"
           selected-index="{{viewState.selectedFileIndex}}"></gr-file-list>
       <gr-messages-list id="messageList"
           change-num="[[_changeNum]]"
@@ -288,7 +292,7 @@
     <gr-overlay id="downloadOverlay" with-backdrop>
       <gr-download-dialog
           change="[[_change]]"
-          patch-num="[[_patchNum]]"
+          patch-num="[[_patchRange.patchNum]]"
           config="[[serverConfig.download]]"
           on-close="_handleDownloadDialogClose"></gr-download-dialog>
     </gr-overlay>
@@ -297,7 +301,8 @@
         with-backdrop>
       <gr-reply-dialog id="replyDialog"
           change-num="[[_changeNum]]"
-          patch-num="[[_patchNum]]"
+          patch-num="[[_patchRange.patchNum]]"
+          revisions="[[_change.revisions]]"
           labels="[[_change.labels]]"
           permitted-labels="[[_change.permitted_labels]]"
           diff-drafts="[[_diffDrafts]]"
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index c17c51c..831fdb8 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -55,8 +55,11 @@
       },
       _commitInfo: Object,
       _changeNum: String,
-      _diffDrafts: Object,
-      _patchNum: String,
+      _diffDrafts: {
+        type: Object,
+        value: function() { return {}; },
+      },
+      _patchRange: Object,
       _allPatchSets: {
         type: Array,
         computed: '_computeAllPatchSets(_change)',
@@ -69,14 +72,10 @@
       _headerContainerEl: Object,
       _headerEl: Object,
       _projectConfig: Object,
-      _boundScrollHandler: {
-        type: Function,
-        value: function() { return this._handleBodyScroll.bind(this); },
-      },
       _replyButtonLabel: {
         type: String,
         value: 'Reply',
-        computed: '_computeReplyButtonLabel(_diffDrafts)',
+        computed: '_computeReplyButtonLabel(_diffDrafts.*)',
       },
     },
 
@@ -94,11 +93,14 @@
         this._loggedIn = loggedIn;
       }.bind(this));
 
-      window.addEventListener('scroll', this._boundScrollHandler);
+      this.addEventListener('comment-save', this._handleCommentSave.bind(this));
+      this.addEventListener('comment-discard',
+          this._handleCommentDiscard.bind(this));
+      this.listen(window, 'scroll', '_handleBodyScroll');
     },
 
     detached: function() {
-      window.removeEventListener('scroll', this._boundScrollHandler);
+      this.unlisten(window, 'scroll', '_handleBodyScroll');
     },
 
     _handleBodyScroll: function(e) {
@@ -124,6 +126,67 @@
       el.classList.remove('pinned');
     },
 
+    _handleCommentSave: function(e) {
+      if (!e.target.comment.__draft) { return; }
+
+      var draft = e.target.comment;
+      draft.patch_set = draft.patch_set || this._patchRange.patchNum;
+
+      // The use of path-based notification helpers (set, push) can’t be used
+      // because the paths could contain dots in them. A new object must be
+      // created to satisfy Polymer’s dirty checking.
+      // https://github.com/Polymer/polymer/issues/3127
+      // TODO(andybons): Polyfill for Object.assign in IE.
+      var diffDrafts = Object.assign({}, this._diffDrafts);
+      if (!diffDrafts[draft.path]) {
+        diffDrafts[draft.path] = [draft];
+        this._diffDrafts = diffDrafts;
+        return;
+      }
+      for (var i = 0; i < this._diffDrafts[draft.path].length; i++) {
+        if (this._diffDrafts[draft.path][i].id === draft.id) {
+          diffDrafts[draft.path][i] = draft;
+          this._diffDrafts = diffDrafts;
+          return;
+        }
+      }
+      diffDrafts[draft.path].push(draft);
+      this._diffDrafts = diffDrafts;
+    },
+
+    _handleCommentDiscard: function(e) {
+      if (!e.target.comment.__draft) { return; }
+
+      var draft = e.target.comment;
+      if (!this._diffDrafts[draft.path]) {
+        return;
+      }
+      var index = -1;
+      for (var i = 0; i < this._diffDrafts[draft.path].length; i++) {
+        if (this._diffDrafts[draft.path][i].id === draft.id) {
+          index = i;
+          break;
+        }
+      }
+      if (index === -1) {
+        throw Error('Unable to find draft with id ' + draft.id);
+      }
+
+      draft.patch_set = draft.patch_set || this._patchRange.patchNum;
+
+      // The use of path-based notification helpers (set, push) can’t be used
+      // because the paths could contain dots in them. A new object must be
+      // created to satisfy Polymer’s dirty checking.
+      // https://github.com/Polymer/polymer/issues/3127
+      // TODO(andybons): Polyfill for Object.assign in IE.
+      var diffDrafts = Object.assign({}, this._diffDrafts);
+      diffDrafts[draft.path].splice(index, 1);
+      if (diffDrafts[draft.path].length === 0) {
+        delete diffDrafts[draft.path];
+      }
+      this._diffDrafts = diffDrafts;
+    },
+
     _handlePatchChange: function(e) {
       var patchNum = e.target.value;
       var currentPatchNum =
@@ -171,51 +234,74 @@
     },
 
     _paramsChanged: function(value) {
-      if (value.view != this.tagName.toLowerCase()) { return; }
+      if (value.view !== this.tagName.toLowerCase()) { return; }
 
       this._changeNum = value.changeNum;
-      this._patchNum = value.patchNum;
-      if (this.viewState.changeNum != this._changeNum ||
-          this.viewState.patchNum != this._patchNum) {
-        this.set('viewState.selectedFileIndex', 0);
-        this.set('viewState.changeNum', this._changeNum);
-        this.set('viewState.patchNum', this._patchNum);
+      this._patchRange = {
+        patchNum: value.patchNum,
+        basePatchNum: value.basePatchNum || 'PARENT',
+      };
+
+      // If the change number or patch range is different, then reset the
+      // selected file index.
+      var patchRangeState = this.viewState.patchRange;
+      if (this.viewState.changeNum !== this._changeNum ||
+          patchRangeState.basePatchNum !== this._patchRange.basePatchNum ||
+          patchRangeState.patchNum !== this._patchRange.patchNum) {
+        this._resetFileListViewState();
       }
-      if (!this._changeNum) {
-        return;
-      }
+
       this._reload().then(function() {
         this.$.messageList.topMargin = this._headerEl.offsetHeight;
+        this.$.fileList.topMargin = this._headerEl.offsetHeight;
 
         // Allow the message list to render before scrolling.
         this.async(function() {
-          var msgPrefix = '#message-';
-          var hash = window.location.hash;
-          if (hash.indexOf(msgPrefix) == 0) {
-            this.$.messageList.scrollToMessage(hash.substr(msgPrefix.length));
-          }
+          this._maybeScrollToMessage();
         }.bind(this), 1);
 
-        this._getLoggedIn().then(function(loggedIn) {
-          if (!loggedIn) { return; }
-
-          if (this.viewState.showReplyDialog) {
-            this.$.replyOverlay.open();
-            this.set('viewState.showReplyDialog', false);
-          }
-        }.bind(this));
+        this._maybeShowReplyDialog();
 
         this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.SHOW_CHANGE, {
           change: this._change,
-          patchNum: this._patchNum,
+          patchNum: this._patchRange.patchNum,
         });
       }.bind(this));
     },
 
+    _maybeScrollToMessage: function() {
+      var msgPrefix = '#message-';
+      var hash = window.location.hash;
+      if (hash.indexOf(msgPrefix) === 0) {
+        this.$.messageList.scrollToMessage(hash.substr(msgPrefix.length));
+      }
+    },
+
+    _maybeShowReplyDialog: function() {
+      this._getLoggedIn().then(function(loggedIn) {
+        if (!loggedIn) { return; }
+
+        if (this.viewState.showReplyDialog) {
+          this.$.replyOverlay.open();
+          this.async(function() { this.$.replyOverlay.center(); }, 1);
+          this.set('viewState.showReplyDialog', false);
+        }
+      }.bind(this));
+    },
+
+    _resetFileListViewState: function() {
+      this.set('viewState.selectedFileIndex', 0);
+      this.set('viewState.changeNum', this._changeNum);
+      this.set('viewState.patchRange', this._patchRange);
+    },
+
     _changeChanged: function(change) {
       if (!change) { return; }
-      this._patchNum = this._patchNum ||
-          change.revisions[change.current_revision]._number;
+      this.set('_patchRange.basePatchNum',
+          this._patchRange.basePatchNum || 'PARENT');
+      this.set('_patchRange.patchNum',
+          this._patchRange.patchNum ||
+              change.revisions[change.current_revision]._number);
 
       var title = change.subject + ' (' + change.change_id.substr(0, 9) + ')';
       this.fire('title-change', {title: title});
@@ -291,12 +377,13 @@
       return result;
     },
 
-    _computeReplyButtonHighlighted: function(drafts) {
-      return Object.keys(drafts || {}).length > 0;
+    _computeReplyButtonHighlighted: function(changeRecord) {
+      var drafts = (changeRecord && changeRecord.base) || {};
+      return Object.keys(drafts).length > 0;
     },
 
-    _computeReplyButtonLabel: function(drafts) {
-      drafts = drafts || {};
+    _computeReplyButtonLabel: function(changeRecord) {
+      var drafts = (changeRecord && changeRecord.base) || {};
       var draftCount = Object.keys(drafts).reduce(function(count, file) {
         return count + drafts[file].length;
       }, 0);
@@ -368,7 +455,7 @@
 
     _getCommitInfo: function() {
       return this.$.restAPI.getChangeCommitInfo(
-          this._changeNum, this._patchNum).then(
+          this._changeNum, this._patchRange.patchNum).then(
               function(commitInfo) {
                 this._commitInfo = commitInfo;
               }.bind(this));
@@ -415,7 +502,7 @@
 
       this._resetHeaderEl();
 
-      if (this._patchNum) {
+      if (this._patchRange.patchNum) {
         return reloadPatchNumDependentResources().then(function() {
           return detailCompletes;
         }).then(reloadDetailDependentResources);
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index ee08ba1..f4a0a52 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -81,9 +81,48 @@
       assert.equal(replyButton.textContent, 'Reply (3)');
     });
 
+    test('comment events properly update diff drafts', function() {
+      element._patchRange = {
+        basePatchNum: 'PARENT',
+        patchNum: 2,
+      };
+      var draft = {
+        __draft: true,
+        id: 'id1',
+        path: '/foo/bar.txt',
+        text: 'hello',
+      };
+      element._handleCommentSave({target: {comment: draft}});
+      draft.patch_set = 2;
+      assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft]});
+      draft.patch_set = null;
+      draft.text = 'hello, there';
+      element._handleCommentSave({target: {comment: draft}});
+      draft.patch_set = 2;
+      assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft]});
+      var draft2 = {
+        __draft: true,
+        id: 'id2',
+        path: '/foo/bar.txt',
+        text: 'hola',
+      };
+      element._handleCommentSave({target: {comment: draft2}});
+      draft2.patch_set = 2;
+      assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft, draft2]});
+      draft.patch_set = null;
+      element._handleCommentDiscard({target: {comment: draft}});
+      draft.patch_set = 2;
+      assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft2]});
+      element._handleCommentDiscard({target: {comment: draft2}});
+      assert.deepEqual(element._diffDrafts, {});
+    });
+
     test('patch num change', function(done) {
       element._changeNum = '42';
-      element._patchNum = 2;
+      element._patchRange = {
+        basePatchNum: 'PARENT',
+        patchNum: 2,
+      };
       element._change = {
         change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
         revisions: {
@@ -133,7 +172,10 @@
 
     test('change status new', function() {
       element._changeNum = '1';
-      element._patchNum = 1;
+      element._patchRange = {
+        basePatchNum: 'PARENT',
+        patchNum: 1,
+      };
       element._change = {
         change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
         revisions: {
@@ -149,7 +191,10 @@
 
     test('change status draft', function() {
       element._changeNum = '1';
-      element._patchNum = 1;
+      element._patchRange = {
+        basePatchNum: 'PARENT',
+        patchNum: 1,
+      };
       element._change = {
         change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
         revisions: {
@@ -165,7 +210,10 @@
 
     test('revision status draft', function() {
       element._changeNum = '1';
-      element._patchNum = 2;
+      element._patchRange = {
+        basePatchNum: 'PARENT',
+        patchNum: 1,
+      };
       element._change = {
         change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
         revisions: {
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
index 263fb28..da15406 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
@@ -42,12 +42,12 @@
         word-wrap: break-word;
       }
     </style>
-    <template is="dom-repeat" items="{{_files}}" as="file">
+    <template is="dom-repeat" items="[[_files]]" as="file">
       <div class="file">
         <a href$="[[_computeFileDiffURL(file, changeNum, patchNum)]]">[[file]]</a>:
       </div>
       <template is="dom-repeat"
-                items="[[_computeCommentsForFile(file)]]" as="comment">
+                items="[[_computeCommentsForFile(comments, file)]]" as="comment">
         <div class="container">
           <a class="lineNum"
              href$="[[_computeDiffLineURL(file, changeNum, comment.patch_set, comment)]]">
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
index b40c18e..c23c373 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
@@ -19,17 +19,17 @@
 
     properties: {
       changeNum: Number,
-      comments: {
-        type: Object,
-        observer: '_commentsChanged',
-      },
+      comments: Object,
       patchNum: Number,
 
-      _files: Array,
+      _files: {
+        type: Array,
+        computed: '_computeFiles(comments)',
+      },
     },
 
-    _commentsChanged: function(value) {
-      this._files = Object.keys(value || {}).sort();
+    _computeFiles: function(comments) {
+      return Object.keys(comments || {}).sort();
     },
 
     _computeFileDiffURL: function(file, changeNum, patchNum) {
@@ -45,8 +45,8 @@
       return diffURL;
     },
 
-    _computeCommentsForFile: function(file) {
-      return this.comments[file];
+    _computeCommentsForFile: function(comments, file) {
+      return comments[file];
     },
 
     _computePatchDisplayName: function(comment) {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html
index a3ab470..979a06a 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html
@@ -23,7 +23,6 @@
     <style>
       :host {
         display: block;
-        width: 30em;
       }
       :host([disabled]) {
         opacity: .5;
@@ -31,19 +30,18 @@
       }
       label {
         cursor: pointer;
+        display: block;
+        width: 100%;
       }
       iron-autogrow-textarea {
+        font-family: var(--monospace-font-family);
         padding: 0;
-      }
-      .main label,
-      .main input[type="text"] {
-        display: block;
-        font: inherit;
-        width: 100%;
-      }
-      .main .message {
-        border: groove;
-        width: 100%;
+        width: 73ch; /* Add a char to account for the border. */
+
+        --iron-autogrow-textarea {
+          border: 1px solid #ddd;
+          font-family: var(--monospace-font-family);
+        }
       }
     </style>
     <gr-confirm-dialog
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
index 5371ce5..ff0fc32 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
@@ -34,7 +34,6 @@
       message: String,
       commitInfo: {
         type: Object,
-        readOnly: true,
         observer: '_commitInfoChanged',
       },
     },
@@ -49,7 +48,8 @@
           new RegExp('\n{1,2}' + revertCommitText + '\\w+.\n*', 'gm');
       commitMessage = commitMessage.replace(previousRevertText, '');
       this.message = 'Revert "' + commitMessage + '"\n\n' +
-          revertCommitText + commitInfo.commit + '.';
+          revertCommitText + commitInfo.commit + '.\n\n' +
+          'Reason for revert: <INSERT REASONING HERE>\n\n';
     },
 
     _handleConfirmTap: function(e) {
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
index f42cdb6..f15c654 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -16,6 +16,9 @@
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html">
+<link rel="import" href="../../diff/gr-diff/gr-diff.html">
+<link rel="import" href="../../diff/gr-diff-cursor/gr-diff-cursor.html">
+<link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
 <dom-module id="gr-file-list">
@@ -28,10 +31,15 @@
         display: flex;
         padding: .1em .25em;
       }
-      .header {
+      header {
+        display: flex;
         font-weight: bold;
+        justify-content: space-between;
+        margin-bottom: .5em;
       }
-      .positionIndicator,
+      .rightControls {
+        font-weight: normal;
+      }
       .reviewed,
       .status {
         align-items: center;
@@ -43,36 +51,29 @@
         text-align: center;
         width: 1.5em;
       }
-      .positionIndicator {
-        justify-content: flex-start;
-        visibility: hidden;
-        width: 1.25em;
-      }
       .row:not(.header):hover {
         background-color: #f5fafd;
       }
       .row[selected] {
         background-color: #ebf5fb;
       }
-      .row[selected] .positionIndicator {
-        visibility: visible;
-      }
       .path {
         flex: 1;
-        overflow: hidden;
         padding-left: .35em;
         text-decoration: none;
-        text-overflow: ellipsis;
         white-space: nowrap;
       }
       .path:hover :first-child {
         text-decoration: underline;
       }
-      .oldPath {
-        color: #999;
+      .path,
+      .path div {
         overflow: hidden;
         text-overflow: ellipsis;
       }
+      .oldPath {
+        color: #999;
+      }
       .comments,
       .stats {
         text-align: right;
@@ -99,11 +100,15 @@
         color: #C62828;
         font-weight: bold;
       }
+      gr-diff {
+        box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
+        display: block;
+        margin: .25em 0 1em;
+      }
       @media screen and (max-width: 50em) {
         .row[selected] {
           background-color: transparent;
         }
-        .positionIndicator,
         .stats {
           display: none;
         }
@@ -116,17 +121,29 @@
         }
       }
     </style>
-    <div class="row header">
-      <div class="positionIndicator"></div>
-      <div class="reviewed" hidden$="[[!_loggedIn]]" hidden></div>
-      <div class="status"></div>
-      <div class="path">Path</div>
-      <div class="comments">Comments</div>
-      <div class="stats">Stats</div>
-    </div>
-    <template is="dom-repeat" items="{{_files}}" as="file">
+    <header>
+      <div>Files</div>
+      <div class="rightControls">
+        <gr-button link on-tap="_expandAllDiffs">Show diffs</gr-button>
+        /
+        <gr-button link on-tap="_collapseAllDiffs">Hide diffs</gr-button>
+        /
+        <label>
+          Diff against
+          <select on-change="_handlePatchChange">
+            <option value="PARENT">Base</option>
+            <template is="dom-repeat" items="[[_computePatchSets(revisions, patchRange.*)]]" as="patchNum">
+              <option
+                  value$="[[patchNum]]"
+                  selected$="[[_computePatchSetSelected(patchNum, patchRange.basePatchNum)]]"
+                  disabled$="[[_computePatchSetDisabled(patchNum, patchRange.patchNum)]]">[[patchNum]]</option>
+            </template>
+          </select>
+        </label>
+      </div>
+    </header>
+    <template is="dom-repeat" items="[[_files]]" as="file">
       <div class="row" selected$="[[_computeFileSelected(index, selectedIndex)]]">
-        <div class="positionIndicator">&#x25b6;</div>
         <div class="reviewed" hidden$="[[!_loggedIn]]" hidden>
           <input type="checkbox" checked$="[[_computeReviewed(file, _reviewed)]]"
               data-path$="[[file.__path]]" on-change="_handleReviewedChange">
@@ -134,7 +151,7 @@
         <div class$="[[_computeClass('status', file.__path)]]">
           [[_computeFileStatus(file.status)]]
         </div>
-        <a class="path" href$="[[_computeDiffURL(changeNum, patchNum, file.__path)]]">
+        <a class="path" href$="[[_computeDiffURL(changeNum, patchRange, file.__path)]]">
           <div title$="[[_computeFileDisplayName(file.__path)]]">
             [[_computeFileDisplayName(file.__path)]]
           </div>
@@ -144,16 +161,28 @@
           </div>
         </a>
         <div class="comments">
-          <span class="drafts">[[_computeDraftsString(drafts, patchNum, file.__path)]]</span>
-          [[_computeCommentsString(comments, patchNum, file.__path)]]
+          <span class="drafts">[[_computeDraftsString(drafts, patchRange.patchNum, file.__path)]]</span>
+          [[_computeCommentsString(comments, patchRange.patchNum, file.__path)]]
         </div>
         <div class$="[[_computeClass('stats', file.__path)]]">
           <span class="added">+[[file.lines_inserted]]</span>
           <span class="removed">-[[file.lines_deleted]]</span>
         </div>
       </div>
+      <gr-diff hidden
+          project="[[change.project]]"
+          commit="[[change.current_revision]]"
+          change-num="[[changeNum]]"
+          patch-range="[[patchRange]]"
+          path="[[file.__path]]"
+          prefs="[[_diffPrefs]]"
+          project-config="[[projectConfig]]"
+          view-mode="[[_userPrefs.diff_view]]"></gr-diff>
     </template>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+    <gr-diff-cursor
+        id="cursor"
+        fold-offset-top="[[topMargin]]"></gr-diff-cursor>
   </template>
   <script src="gr-file-list.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index de28184..82c47d6 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -20,10 +20,14 @@
     is: 'gr-file-list',
 
     properties: {
+      patchRange: Object,
       patchNum: String,
       changeNum: String,
       comments: Object,
       drafts: Object,
+      revisions: Object,
+      projectConfig: Object,
+      topMargin: Number,
       selectedIndex: {
         type: Number,
         notify: true,
@@ -32,8 +36,12 @@
         type: Object,
         value: function() { return document.body; },
       },
+      change: Object,
 
-      _files: Array,
+      _files: {
+        type: Array,
+        observer: '_filesChanged',
+      },
       _loggedIn: {
         type: Boolean,
         value: false,
@@ -42,6 +50,9 @@
         type: Array,
         value: function() { return []; },
       },
+      _diffPrefs: Object,
+      _userPrefs: Object,
+      _showInlineDiffs: Boolean,
     },
 
     behaviors: [
@@ -49,10 +60,12 @@
     ],
 
     reload: function() {
-      if (!this.changeNum || !this.patchNum) {
+      if (!this.changeNum || !this.patchRange.patchNum) {
         return Promise.resolve();
       }
 
+      this._collapseAllDiffs();
+
       var promises = [];
       var _this = this;
 
@@ -69,7 +82,72 @@
         });
       }));
 
-      return Promise.all(promises);
+      promises.push(this._getDiffPreferences().then(function(prefs) {
+        this._diffPrefs = prefs;
+      }.bind(this)));
+
+      promises.push(this._getPreferences().then(function(prefs) {
+        this._userPrefs = prefs;
+      }.bind(this)));
+    },
+
+    _getDiffPreferences: function() {
+      return this.$.restAPI.getDiffPreferences();
+    },
+
+    _getPreferences: function() {
+      return this.$.restAPI.getPreferences();
+    },
+
+    _computePatchSets: function(revisions) {
+      var patchNums = [];
+      for (var commit in revisions) {
+        patchNums.push(revisions[commit]._number);
+      }
+      return patchNums.sort(function(a, b) { return a - b; });
+    },
+
+    _computePatchSetDisabled: function(patchNum, currentPatchNum) {
+      return parseInt(patchNum, 10) >= parseInt(currentPatchNum, 10);
+    },
+
+    _computePatchSetSelected: function(patchNum, basePatchNum) {
+      return parseInt(patchNum, 10) === parseInt(basePatchNum, 10);
+    },
+
+    _handlePatchChange: function(e) {
+      this.set('patchRange.basePatchNum', Polymer.dom(e).rootTarget.value);
+      page.show('/c/' + encodeURIComponent(this.changeNum) + '/' +
+          encodeURIComponent(this._patchRangeStr(this.patchRange)));
+    },
+
+    _forEachDiff: function(fn) {
+      var diffs = Polymer.dom(this.root).querySelectorAll('gr-diff');
+      for (var i = 0; i < diffs.length; i++) {
+        fn(diffs[i]);
+      }
+    },
+
+    _expandAllDiffs: function(e) {
+      this._showInlineDiffs = true;
+      this._forEachDiff(function(diff) {
+        diff.hidden = false;
+        diff.reload();
+      });
+      if (e && e.target) {
+        e.target.blur();
+      }
+    },
+
+    _collapseAllDiffs: function(e) {
+      this._showInlineDiffs = false;
+      this._forEachDiff(function(diff) {
+        diff.hidden = true;
+      });
+      this.$.cursor.handleDiffUpdate();
+      if (e && e.target) {
+        e.target.blur();
+      }
     },
 
     _computeCommentsString: function(comments, patchNum, path) {
@@ -113,8 +191,8 @@
     },
 
     _saveReviewedState: function(path, reviewed) {
-      return this.$.restAPI.saveFileReviewed(this.changeNum, this.patchNum,
-          path, reviewed);
+      return this.$.restAPI.saveFileReviewed(this.changeNum,
+          this.patchRange.patchNum, path, reviewed);
     },
 
     _getLoggedIn: function() {
@@ -122,26 +200,62 @@
     },
 
     _getReviewedFiles: function() {
-      return this.$.restAPI.getReviewedFiles(this.changeNum, this.patchNum);
+      return this.$.restAPI.getReviewedFiles(this.changeNum,
+          this.patchRange.patchNum);
     },
 
     _getFiles: function() {
       return this.$.restAPI.getChangeFilesAsSpeciallySortedArray(
-          this.changeNum, this.patchNum);
+          this.changeNum, this.patchRange);
     },
 
     _handleKey: function(e) {
       if (this.shouldSupressKeyboardShortcut(e)) { return; }
 
       switch (e.keyCode) {
+        case 37: // left
+          if (e.shiftKey && this._showInlineDiffs) {
+            e.preventDefault();
+            this.$.cursor.moveLeft();
+          }
+          break;
+        case 39: // right
+          if (e.shiftKey && this._showInlineDiffs) {
+            e.preventDefault();
+            this.$.cursor.moveRight();
+          }
+          break;
+        case 73:  // 'i'
+          if (!e.shiftKey) { return; }
+          e.preventDefault();
+          this._toggleInlineDiffs();
+          break;
+        case 40:  // down
         case 74:  // 'j'
           e.preventDefault();
-          this.selectedIndex =
-              Math.min(this._files.length - 1, this.selectedIndex + 1);
+          if (this._showInlineDiffs) {
+            this.$.cursor.moveDown();
+          } else {
+            this.selectedIndex =
+                Math.min(this._files.length - 1, this.selectedIndex + 1);
+            this._scrollToSelectedFile();
+          }
           break;
+        case 38:  // up
         case 75:  // 'k'
           e.preventDefault();
-          this.selectedIndex = Math.max(0, this.selectedIndex - 1);
+          if (this._showInlineDiffs) {
+            this.$.cursor.moveUp();
+          } else {
+            this.selectedIndex = Math.max(0, this.selectedIndex - 1);
+            this._scrollToSelectedFile();
+          }
+          break;
+        case 67: // 'c'
+          if (this._showInlineDiffs) {
+            e.preventDefault();
+            this._addDraftAtTarget();
+          }
           break;
         case 219:  // '['
           e.preventDefault();
@@ -154,19 +268,81 @@
         case 13:  // <enter>
         case 79:  // 'o'
           e.preventDefault();
-          this._openSelectedFile();
+          if (this._showInlineDiffs) {
+            this._openCursorFile();
+          } else {
+            this._openSelectedFile();
+          }
+          break;
+        case 78:  // 'n'
+          if (this._showInlineDiffs) {
+            e.preventDefault();
+            if (e.shiftKey) {
+              this.$.cursor.moveToNextCommentThread();
+            } else {
+              this.$.cursor.moveToNextChunk();
+            }
+          }
+          break;
+        case 80:  // 'p'
+          if (this._showInlineDiffs) {
+            e.preventDefault();
+            if (e.shiftKey) {
+              this.$.cursor.moveToPreviousCommentThread();
+            } else {
+              this.$.cursor.moveToPreviousChunk();
+            }
+          }
           break;
       }
     },
 
+    _toggleInlineDiffs: function() {
+      if (this._showInlineDiffs) {
+        this._collapseAllDiffs();
+      } else {
+        this._expandAllDiffs();
+      }
+    },
+
+    _openCursorFile: function() {
+      var diff = this.$.cursor.getTargetDiffElement();
+      page.show(this._computeDiffURL(diff.changeNum, diff.patchRange,
+          diff.path));
+    },
+
     _openSelectedFile: function(opt_index) {
       if (opt_index != null) {
         this.selectedIndex = opt_index;
       }
-      page.show(this._computeDiffURL(this.changeNum, this.patchNum,
+      page.show(this._computeDiffURL(this.changeNum, this.patchRange,
           this._files[this.selectedIndex].__path));
     },
 
+    _addDraftAtTarget: function() {
+      var diff = this.$.cursor.getTargetDiffElement();
+      var target = this.$.cursor.getTargetLineElement();
+      if (diff && target) {
+        diff.addDraftAtLine(target);
+      }
+    },
+
+    _scrollToSelectedFile: function() {
+      var el = this.$$('.row[selected]');
+      var top = 0;
+      for (var node = el; node; node = node.offsetParent) {
+        top += node.offsetTop;
+      }
+
+      // Don't scroll if it's already in view.
+      if (top > window.pageYOffset + this.topMargin &&
+          top < window.pageYOffset + window.innerHeight - el.clientHeight) {
+        return;
+      }
+
+      window.scrollTo(0, top - document.body.clientHeight / 2);
+    },
+
     _computeFileSelected: function(index, selectedIndex) {
       return index === selectedIndex;
     },
@@ -175,8 +351,19 @@
       return status || 'M';
     },
 
-    _computeDiffURL: function(changeNum, patchNum, path) {
-      return '/c/' + changeNum + '/' + patchNum + '/' + path;
+    _computeDiffURL: function(changeNum, patchRange, path) {
+      return '/c/' +
+          encodeURIComponent(changeNum) +
+          '/' +
+          encodeURIComponent(this._patchRangeStr(patchRange)) +
+          '/' +
+          path;
+    },
+
+    _patchRangeStr: function(patchRange) {
+      return patchRange.basePatchNum !== 'PARENT' ?
+          patchRange.basePatchNum + '..' + patchRange.patchNum :
+          patchRange.patchNum + '';
     },
 
     _computeFileDisplayName: function(path) {
@@ -190,5 +377,15 @@
       }
       return classes.join(' ');
     },
+
+    _filesChanged: function() {
+      this.async(function() {
+        var diffElements = Polymer.dom(this.root).querySelectorAll('gr-diff');
+
+        // Overwrite the cursor's list of diffs:
+        this.$.cursor.splice.apply(this.$.cursor,
+            ['diffs', 0, this.$.cursor.diffs.length].concat(diffElements));
+      }.bind(this), 1);
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index 64ef91f..055f961 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -35,17 +35,12 @@
 <script>
   suite('gr-file-list tests', function() {
     var element;
-    var getLoggedInStub;
 
     setup(function() {
-      element = fixture('basic');
-      getLoggedInStub = sinon.stub(element, '_getLoggedIn', function() {
-        return Promise.resolve(true);
+      stub('gr-rest-api-interface', {
+        getLoggedIn: function() { return Promise.resolve(true); },
       });
-    });
-
-    teardown(function() {
-      getLoggedInStub.restore();
+      element = fixture('basic');
     });
 
     test('get file list', function(done) {
@@ -83,13 +78,21 @@
     });
 
     test('keyboard shortcuts', function() {
+      var toggleInlineDiffsStub = sinon.stub(element, '_toggleInlineDiffs');
+      MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift');  // 'I'
+      assert.isTrue(toggleInlineDiffsStub.calledOnce);
+      toggleInlineDiffsStub.restore();
+
       element._files = [
         {__path: '/COMMIT_MSG'},
         {__path: 'file_added_in_rev2.txt'},
         {__path: 'myfile.txt'},
       ];
       element.changeNum = '42';
-      element.patchNum = '2';
+      element.patchRange = {
+        basePatchNum: 'PARENT',
+        patchNum: '2',
+      };
       element.selectedIndex = 0;
 
       flushAsynchronousOperations();
@@ -181,7 +184,10 @@
       ];
       element._reviewed = ['/COMMIT_MSG', 'myfile.txt'];
       element.changeNum = '42';
-      element.patchNum = '2';
+      element.patchRange = {
+        basePatchNum: 'PARENT',
+        patchNum: '2',
+      };
       element.selectedIndex = 0;
 
       flushAsynchronousOperations();
@@ -205,5 +211,52 @@
 
       saveStub.restore();
     });
+
+    test('patch set from revisions', function() {
+      var patchNums = element._computePatchSets({
+        rev3: {_number: 3},
+        rev1: {_number: 1},
+        rev4: {_number: 4},
+        rev2: {_number: 2},
+      });
+      assert.deepEqual(patchNums, [1, 2, 3, 4]);
+    });
+
+    test('patch range string', function() {
+      assert.equal(
+          element._patchRangeStr({basePatchNum: 'PARENT', patchNum: '1'}),
+          '1');
+      assert.equal(
+          element._patchRangeStr({basePatchNum: '1', patchNum: '3'}),
+          '1..3');
+    });
+
+    test('diff against dropdown', function(done) {
+      var showStub = sinon.stub(page, 'show');
+      element.changeNum = '42';
+      element.patchRange = {
+        basePatchNum: 'PARENT',
+        patchNum: '3',
+      };
+      element.revisions = {
+        rev1: {_number: 1},
+        rev2: {_number: 2},
+        rev3: {_number: 3},
+      };
+      flush(function() {
+        var selectEl = element.$$('select');
+        assert.equal(selectEl.value, 'PARENT');
+        assert.isTrue(element.$$('option[value="3"]').hasAttribute('disabled'));
+        selectEl.addEventListener('change', function() {
+          assert.equal(selectEl.value, '2');
+          assert(showStub.lastCall.calledWithExactly('/c/42/2..3'),
+              'Should navigate to /c/42/2..3');
+          showStub.restore();
+          done();
+        });
+        selectEl.value = '2';
+        element.fire('change', {}, {node: selectEl});
+      });
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
index 71fee37..7fb998b 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -59,6 +59,9 @@
         border: none;
         width: 100%;
       }
+      .labelsNotShown {
+        color: #666;
+      }
       .labelContainer:not(:first-of-type) {
         margin-top: .5em;
       }
@@ -112,19 +115,28 @@
             bind-value="{{draft}}"></iron-autogrow-textarea>
       </section>
       <section class="labelsContainer">
-        <template is="dom-repeat"
-            items="[[_computeLabelArray(permittedLabels)]]" as="label">
-          <div class="labelContainer">
-            <span class="labelName">[[label]]</span>
-            <iron-selector data-label$="[[label]]"
-                selected="[[_computeIndexOfLabelValue(labels, permittedLabels, label, _account)]]">
-              <template is="dom-repeat"
-                  items="[[_computePermittedLabelValues(permittedLabels, label)]]"
-                  as="value">
-                <gr-button data-value$="[[value]]">[[value]]</gr-button>
-              </template>
-            </iron-selector>
-          </div>
+        <template is="dom-if" if="[[_computeShowLabels(patchNum, revisions)]]">
+          <template is="dom-repeat"
+              items="[[_computeLabelArray(permittedLabels)]]" as="label">
+            <div class="labelContainer">
+              <span class="labelName">[[label]]</span>
+              <iron-selector data-label$="[[label]]"
+                  selected="[[_computeIndexOfLabelValue(labels, permittedLabels, label, _account)]]">
+                <template is="dom-repeat"
+                    items="[[_computePermittedLabelValues(permittedLabels, label)]]"
+                    as="value">
+                  <gr-button has-tooltip data-value$="[[value]]"
+                      title$="[[_computeLabelValueTitle(labels, label, value)]]">[[value]]</gr-button>
+                </template>
+              </iron-selector>
+            </div>
+          </template>
+        </template>
+        <template is="dom-if" if="[[!_computeShowLabels(patchNum, revisions)]]">
+          <span class="labelsNotShown">
+            Labels are not shown because this is not the most recent patch set.
+            <a href$="/c/[[changeNum]]">Go to the latest patch set.</a>
+          </span>
         </template>
       </section>
       <section class="draftsContainer" hidden$="[[_computeHideDraftList(diffDrafts)]]">
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index be536da..c89cb93 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -32,6 +32,7 @@
     properties: {
       changeNum: String,
       patchNum: String,
+      revisions: Object,
       disabled: {
         type: Boolean,
         value: false,
@@ -64,6 +65,16 @@
       }.bind(this));
     },
 
+    _computeShowLabels: function(patchNum, revisions) {
+      var num = parseInt(patchNum, 10);
+      for (var rev in revisions) {
+        if (revisions[rev]._number > num) {
+          return false;
+        }
+      }
+      return true;
+    },
+
     _computeHideDraftList: function(drafts) {
       return Object.keys(drafts || {}).length == 0;
     },
@@ -78,6 +89,10 @@
       if (total > 1) { return total + ' Drafts'; }
     },
 
+    _computeLabelValueTitle: function(labels, label, value) {
+      return labels[label] && labels[label].values[value];
+    },
+
     _computeLabelArray: function(labelsObj) {
       return Object.keys(labelsObj).sort();
     },
@@ -131,6 +146,10 @@
       };
       for (var label in this.permittedLabels) {
         var selectorEl = this.$$('iron-selector[data-label="' + label + '"]');
+
+        // The selector may not be present if it’s not at the latest patch set.
+        if (!selectorEl) { continue; }
+
         var selectedVal = selectorEl.selectedItem.getAttribute('data-value');
         selectedVal = parseInt(selectedVal, 10);
         obj.labels[label] = selectedVal;
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
index bc850e3..cbef78d 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -84,7 +84,21 @@
       MockInteractions.tap(element.$$('.cancel'));
     });
 
+    test('show/hide labels', function() {
+      var revisions = {
+        rev1: {_number: 1},
+        rev2: {_number: 2},
+      };
+      assert.isFalse(element._computeShowLabels('1', revisions));
+      assert.isTrue(element._computeShowLabels('2', revisions));
+    });
+
     test('label picker', function(done) {
+      var showLabelsStub = sinon.stub(element, '_computeShowLabels',
+          function() { return true; });
+      element.revisions = {};
+      element.patchNum = '';
+
       // Async tick is needed because iron-selector content is distributed and
       // distributed content requires an observer to be set up.
       flush(function() {
@@ -118,6 +132,7 @@
               'Element should be enabled when done sending reply.');
           assert.equal(element.draft.length, 0);
           saveReviewStub.restore();
+          showLabelsStub.restore();
           done();
         });
 
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
index 2c7550f..6880a39 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
@@ -82,8 +82,8 @@
               <div>[[account.email]]</div>
             </div>
           </li>
-          <li><a href="/switch-account">Switch account</a></li>
-          <li><a href="/logout">Sign out</a></li>
+          <li><a href$="[[_computeRelativeURL('/switch-account')]]">Switch account</a></li>
+          <li><a href$="[[_computeRelativeURL('/logout')]]">Sign out</a></li>
         </ul>
       </div>
     </iron-dropdown>
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
index 62212a3..31edb1a 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
@@ -31,5 +31,9 @@
     _showDropdownTapHandler: function(e) {
       this.$.dropdown.open();
     },
+
+    _computeRelativeURL: function(path) {
+      return '//' + window.location.host + path;
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
index 9a75fa9..5b70769 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
@@ -98,6 +98,13 @@
             <td><span class="key">u</span></td>
             <td>Up to change list</td>
           </tr>
+          <tr>
+            <td>
+              <span class="key modifier">Shift</span>
+              <span class="key">i</span>
+            </td>
+            <td>Show/hide inline diffs</td>
+          </tr>
         </tbody>
         <!-- Diff View -->
         <tbody hidden$="[[!_computeInView(view, 'gr-diff-view')]]" hidden>
@@ -134,7 +141,10 @@
             <td>Show previous change</td>
           </tr>
           <tr>
-            <td><span class="key">Enter</span> or <span class="key">o</span></td>
+            <td>
+              <span class="key">Enter</span> or
+              <span class="key">o</span>
+            </td>
             <td>Show selected change</td>
           </tr>
         </tbody>
@@ -151,17 +161,70 @@
             <td></td><td class="header">File list</td>
           </tr>
           <tr>
-            <td><span class="key">j</span></td>
+            <td><span class="key">j</span> or <span class="key">↓</span></td>
             <td>Select next file</td>
           </tr>
           <tr>
-            <td><span class="key">k</span></td>
+            <td><span class="key">k</span> or <span class="key">↑</span></td>
             <td>Select previous file</td>
           </tr>
           <tr>
             <td><span class="key">Enter</span> or <span class="key">o</span></td>
             <td>Show selected file</td>
           </tr>
+          <tr>
+            <td></td><td class="header">Diffs</td>
+          </tr>
+          <tr>
+            <td><span class="key">j</span> or <span class="key">↓</span></td>
+            <td>Go to next line</td>
+          </tr>
+          <tr>
+            <td><span class="key">k</span> or <span class="key">↑</span></td>
+            <td>Go to previous line</td>
+          </tr>
+          <tr>
+            <td><span class="key">n</span></td>
+            <td>Go to next diff chunk</td>
+          </tr>
+          <tr>
+            <td><span class="key">p</span></td>
+            <td>Go to previous diff chunk</td>
+          </tr>
+          <tr>
+            <td>
+              <span class="key modifier">Shift</span>
+              <span class="key">n</span>
+            </td>
+            <td>Go to next comment thread</td>
+          </tr>
+          <tr>
+            <td>
+              <span class="key modifier">Shift</span>
+              <span class="key">p</span>
+            </td>
+            <td>Go to previous comment thread</td>
+          </tr>
+          <tr>
+            <td>
+              <span class="key modifier">Shift</span>
+              <span class="key">←</span>
+            </td>
+            <td>Select left pane</td>
+          </tr>
+          <tr>
+            <td>
+              <span class="key modifier">Shift</span>
+              <span class="key">→</span>
+            </td>
+            <td>Select right pane</td>
+          </tr>
+          <tr>
+            <td>
+              <span class="key">c</span>
+            </td>
+            <td>Draft new comment</td>
+          </tr>
         </tbody>
         <!-- Diff View -->
         <tbody hidden$="[[!_computeInView(view, 'gr-diff-view')]]" hidden>
@@ -169,6 +232,14 @@
             <td></td><td class="header">Actions</td>
           </tr>
           <tr>
+            <td><span class="key">j</span> or <span class="key">↓</span></td>
+            <td>Show next line</td>
+          </tr>
+          <tr>
+            <td><span class="key">k</span> or <span class="key">↑</span></td>
+            <td>Show previous line</td>
+          </tr>
+          <tr>
             <td><span class="key">n</span></td>
             <td>Show next diff chunk</td>
           </tr>
@@ -191,6 +262,26 @@
             <td>Show previous comment thread</td>
           </tr>
           <tr>
+            <td>
+              <span class="key modifier">Shift</span>
+              <span class="key">←</span>
+            </td>
+            <td>Select left pane</td>
+          </tr>
+          <tr>
+            <td>
+              <span class="key modifier">Shift</span>
+              <span class="key">→</span>
+            </td>
+            <td>Select right pane</td>
+          </tr>
+          <tr>
+            <td>
+              <span class="key">c</span>
+            </td>
+            <td>Draft new comment</td>
+          </tr>
+          <tr>
             <td><span class="key">a</span></td>
             <td>Review and publish comments</td>
           </tr>
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
index 20405b9..930c8cf 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
@@ -130,7 +130,7 @@
       }
     </style>
     <nav>
-      <a href="/" class="bigTitle">PolyGerrit</a>
+      <a href$="[[_computeRelativeURL('/')]]" class="bigTitle">PolyGerrit</a>
       <ul class="links">
         <template is="dom-repeat" items="[[_links]]" as="linkGroup">
           <li>
@@ -139,7 +139,7 @@
             </span>
             <ul>
               <template is="dom-repeat" items="[[linkGroup.links]]" as="link">
-                <li><a href="[[link.url]]">[[link.name]]</a></li>
+                <li><a href$="[[link.url]]">[[link.name]]</a></li>
               </template>
             </ul>
           </li>
@@ -148,7 +148,7 @@
       <div class="rightItems">
         <gr-search-bar value="{{searchQuery}}" role="search"></gr-search-bar>
         <div class="accountContainer" id="accountContainer">
-          <a class="loginButton" href="/login" on-tap="_loginTapHandler">Sign in</a>
+          <a class="loginButton" href$="[[_loginURL]]" on-tap="_loginTapHandler">Sign in</a>
           <gr-account-dropdown account="[[_account]]"></gr-account-dropdown>
         </div>
       </div>
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
index 173b88e..6fc3cc1 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
@@ -56,6 +56,10 @@
         type: Array,
         computed: '_computeLinks(_defaultLinks, _userLinks)',
       },
+      _loginURL: {
+        type: String,
+        value: '/login',
+      },
       _userLinks: {
         type: Array,
         value: function() { return []; },
@@ -68,6 +72,22 @@
 
     attached: function() {
       this._loadAccount();
+      this.listen(window, 'location-change', '_handleLocationChange');
+    },
+
+    detached: function() {
+      this.unlisten(window, 'location-change', '_handleLocationChange');
+    },
+
+    _handleLocationChange: function(e) {
+      this._loginURL = '/login/' + encodeURIComponent(
+          window.location.pathname +
+          window.location.search +
+          window.location.hash);
+    },
+
+    _computeRelativeURL: function(path) {
+      return '//' + window.location.host + path;
     },
 
     _computeLinks: function(defaultLinks, userLinks) {
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
index abd2a2c..dea0d1d 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -23,6 +23,12 @@
     // Middleware
     page(function(ctx, next) {
       document.body.scrollTop = 0;
+
+      // Fire asynchronously so that the URL is changed by the time the event
+      // is processed.
+      app.async(function() {
+        app.fire('location-change');
+      }, 1);
       next();
     });
 
@@ -68,15 +74,36 @@
     page('/q/:query', queryHandler);
 
     page(/^\/(\d+)\/?/, function(ctx) {
-      page.redirect('/c/' + ctx.params[0]);
+      page.redirect('/c/' + encodeURIComponent(ctx.params[0]));
     });
 
-    page('/c/:changeNum/:patchNum?', function(data) {
-      data.params.view = 'gr-change-view';
-      app.params = data.params;
+    // Matches /c/<changeNum>/[<basePatchNum>..][<patchNum>].
+    page(/^\/c\/(\d+)\/?(((\d+)(\.\.(\d+))?))?$/, function(ctx) {
+      // Parameter order is based on the regex group number matched.
+      var params = {
+        changeNum: ctx.params[0],
+        basePatchNum: ctx.params[3],
+        patchNum: ctx.params[5],
+        view: 'gr-change-view',
+      };
+
+      // Don't allow diffing the same patch number against itself.
+      if (params.basePatchNum != null &&
+          params.basePatchNum === params.patchNum) {
+        page.redirect('/c/' +
+            encodeURIComponent(params.changeNum) +
+            '/' +
+            encodeURIComponent(params.patchNum) +
+            '/');
+        return;
+      }
+      normalizePatchRangeParams(params);
+      app.params = params;
     });
 
+    // Matches /c/<changeNum>/[<basePatchNum>..]<patchNum>/<path>.
     page(/^\/c\/(\d+)\/((\d+)(\.\.(\d+))?)\/(.+)/, function(ctx) {
+      // Parameter order is based on the regex group number matched.
       var params = {
         changeNum: ctx.params[0],
         basePatchNum: ctx.params[2],
@@ -84,19 +111,27 @@
         path: ctx.params[5],
         view: 'gr-diff-view',
       };
-      // Don't allow diffing the same patch number against itself because WHY?
-      if (params.basePatchNum == params.patchNum) {
-        page.redirect('/c/' + params.changeNum + '/' + params.patchNum + '/' +
-            params.path);
+      // Don't allow diffing the same patch number against itself.
+      if (params.basePatchNum === params.patchNum) {
+        page.redirect('/c/' +
+            encodeURIComponent(params.changeNum) +
+            '/' +
+            encodeURIComponent(params.patchNum) +
+            '/' +
+            encodeURIComponent(params.path));
         return;
       }
-      if (!params.patchNum) {
-        params.patchNum = params.basePatchNum;
-        delete(params.basePatchNum);
-      }
+      normalizePatchRangeParams(params);
       app.params = params;
     });
 
+    function normalizePatchRangeParams(params) {
+      if (params.basePatchNum && !params.patchNum) {
+        params.patchNum = params.basePatchNum;
+        params.basePatchNum = null;
+      }
+    }
+
     page.start();
   });
 })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
index b827d26..7304ac7 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
@@ -41,10 +41,6 @@
       _orderedComments: Array,
     },
 
-    get naturalHeight() {
-      return this.$.container.offsetHeight;
-    },
-
     observers: [
       '_commentsChanged(comments.splices)',
     ],
@@ -64,7 +60,9 @@
         return;
       }
 
-      this.push('comments', this._newDraft(opt_lineNum));
+      var draft = this._newDraft(opt_lineNum);
+      draft.__editing = true;
+      this.push('comments', draft);
     },
 
     _getLoggedIn: function() {
@@ -121,13 +119,8 @@
             function(line) { return ' > ' + line; }).join('\n') + '\n\n';
       }
       var reply = this._newReply(comment.id, comment.line, quoteStr);
+      reply.__editing = true;
       this.push('comments', reply);
-
-      // Allow the reply to render in the dom-repeat.
-      this.async(function() {
-        var commentEl = this._commentElWithDraftID(reply.__draftID);
-        commentEl.editing = true;
-      }.bind(this), 1);
     },
 
     _handleCommentDone: function(e) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
index fd35ad7..6e7a68a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
@@ -19,6 +19,7 @@
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
 <link rel="import" href="../../shared/gr-linked-text/gr-linked-text.html">
+<link rel="import" href="../../shared/gr-storage/gr-storage.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
 <dom-module id="gr-diff-comment">
@@ -128,7 +129,7 @@
           class="editMessage"
           disabled="{{disabled}}"
           rows="4"
-          bind-value="{{_editDraft}}"
+          bind-value="{{_messageText}}"
           on-keydown="_handleTextareaKeydown"></iron-autogrow-textarea>
       <gr-linked-text class="message"
           pre
@@ -140,7 +141,7 @@
         <gr-button class="action done" on-tap="_handleDone">Done</gr-button>
         <gr-button class="action edit" on-tap="_handleEdit">Edit</gr-button>
         <gr-button class="action save" on-tap="_handleSave"
-            disabled$="[[_computeSaveDisabled(_editDraft)]]">Save</gr-button>
+            disabled$="[[_computeSaveDisabled(_messageText)]]">Save</gr-button>
         <gr-button class="action cancel" on-tap="_handleCancel" hidden>Cancel</gr-button>
         <div class="danger">
           <gr-button class="action discard" on-tap="_handleDiscard">Discard</gr-button>
@@ -148,6 +149,7 @@
       </div>
     </div>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+    <gr-storage id="storage"></gr-storage>
   </template>
   <script src="gr-diff-comment.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
index ded5108..7a15754 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
@@ -14,6 +14,9 @@
 (function() {
   'use strict';
 
+  var STORAGE_DEBOUNCE_INTERVAL = 400;
+  var UPDATE_DEBOUNCE_INTERVAL = 500;
+
   Polymer({
     is: 'gr-diff-comment',
 
@@ -35,11 +38,24 @@
      * @event comment-discard
      */
 
+    /**
+     * Fired when this comment is saved.
+     *
+     * @event comment-save
+     */
+
+    /**
+     * Fired when this comment is updated.
+     *
+     * @event comment-update
+     */
+
     properties: {
       changeNum: String,
       comment: {
         type: Object,
         notify: true,
+        observer: '_commentChanged',
       },
       disabled: {
         type: Boolean,
@@ -61,17 +77,33 @@
       projectConfig: Object,
 
       _xhrPromise: Object,  // Used for testing.
-      _editDraft: String,
+      _messageText: {
+        type: String,
+        value: '',
+        observer: '_messageTextChanged',
+      },
     },
 
-    ready: function() {
-      this._editDraft = (this.comment && this.comment.message) || '';
-      this.editing = this._editDraft.length == 0;
+    observers: [
+      '_commentMessageChanged(comment.message)',
+      '_loadLocalDraft(changeNum, patchNum, comment)',
+    ],
+
+    detached: function() {
+      this.flushDebouncer('fire-update');
     },
 
     save: function() {
-      this.comment.message = this._editDraft;
+      this.comment.message = this._messageText;
       this.disabled = true;
+
+      this.$.storage.eraseDraftComment({
+        changeNum: this.changeNum,
+        patchNum: this.patchNum,
+        path: this.comment.path,
+        line: this.comment.line,
+      });
+
       this._xhrPromise = this._saveDraft(this.comment).then(function(response) {
         this.disabled = false;
         if (!response.ok) { return response; }
@@ -86,7 +118,7 @@
           }
           this.comment = comment;
           this.editing = false;
-
+          this.fire('comment-save', {comment: this.comment});
           return obj;
         }.bind(this));
       }.bind(this)).catch(function(err) {
@@ -95,6 +127,16 @@
       }.bind(this));
     },
 
+    _commentChanged: function(comment) {
+      this.editing = !!comment.__editing;
+    },
+
+    _fireUpdate: function() {
+      this.debounce('fire-update', function() {
+        this.fire('comment-update', {comment: this.comment});
+      }, UPDATE_DEBOUNCE_INTERVAL);
+    },
+
     _draftChanged: function(draft) {
       this.$.container.classList.toggle('draft', draft);
     },
@@ -113,6 +155,10 @@
       if (this.comment && this.comment.id) {
         this.$$('.cancel').hidden = !editing;
       }
+      if (this.comment) {
+        this.comment.__editing = this.editing;
+      }
+      this._fireUpdate();
     },
 
     _computeLinkToComment: function(comment) {
@@ -129,6 +175,34 @@
       }
     },
 
+    _commentMessageChanged: function(message) {
+      this._messageText = message || '';
+    },
+
+    _messageTextChanged: function(newValue, oldValue) {
+      if (!this.comment || (this.comment && this.comment.id)) { return; }
+
+      this.debounce('store', function() {
+        var message = this._messageText;
+
+        var commentLocation = {
+          changeNum: this.changeNum,
+          patchNum: this.patchNum,
+          path: this.comment.path,
+          line: this.comment.line,
+        };
+
+        if ((!this._messageText || !this._messageText.length) && oldValue) {
+          // If the draft has been modified to be empty, then erase the storage
+          // entry.
+          this.$.storage.eraseDraftComment(commentLocation);
+        } else {
+          this.$.storage.setDraftComment(commentLocation, message);
+        }
+        this._fireUpdate();
+      }, STORAGE_DEBOUNCE_INTERVAL);
+    },
+
     _handleLinkTap: function(e) {
       e.preventDefault();
       var hash = this._computeLinkToComment(this.comment);
@@ -158,7 +232,7 @@
 
     _handleEdit: function(e) {
       this._preventDefaultAndBlur(e);
-      this._editDraft = this.comment.message;
+      this._messageText = this.comment.message;
       this.editing = true;
     },
 
@@ -170,10 +244,10 @@
     _handleCancel: function(e) {
       this._preventDefaultAndBlur(e);
       if (this.comment.message == null || this.comment.message.length == 0) {
-        this.fire('comment-discard');
+        this.fire('comment-discard', {comment: this.comment});
         return;
       }
-      this._editDraft = this.comment.message;
+      this._messageText = this.comment.message;
       this.editing = false;
     },
 
@@ -182,22 +256,24 @@
       if (!this.comment.__draft) {
         throw Error('Cannot discard a non-draft comment.');
       }
+      this.editing = false;
       this.disabled = true;
       if (!this.comment.id) {
-        this.fire('comment-discard');
+        this.disabled = false;
+        this.fire('comment-discard', {comment: this.comment});
         return;
       }
 
-      this._xhrPromise =
-          this._deleteDraft(this.comment).then(function(response) {
-        this.disabled = false;
-        if (!response.ok) { return response; }
+      this._xhrPromise = this._deleteDraft(this.comment).then(
+          function(response) {
+            this.disabled = false;
+            if (!response.ok) { return response; }
 
-        this.fire('comment-discard');
-      }.bind(this)).catch(function(err) {
-        this.disabled = false;
-        throw err;
-      }.bind(this));;
+            this.fire('comment-discard', {comment: this.comment});
+          }.bind(this)).catch(function(err) {
+            this.disabled = false;
+            throw err;
+          }.bind(this));
     },
 
     _preventDefaultAndBlur: function(e) {
@@ -213,5 +289,24 @@
       return this.$.restAPI.deleteDiffDraft(this.changeNum, this.patchNum,
           draft);
     },
+
+    _loadLocalDraft: function(changeNum, patchNum, comment) {
+      // Only apply local drafts to comments that haven't been saved
+      // remotely, and haven't been given a default message already.
+      if (!comment || comment.id || comment.message) {
+        return;
+      }
+
+      var draft = this.$.storage.getDraftComment({
+        changeNum: changeNum,
+        patchNum: patchNum,
+        path: comment.path,
+        line: comment.line,
+      });
+
+      if (draft) {
+        this.set('comment.message', draft.message);
+      }
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
index a333e14..6c27a36 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
@@ -183,11 +183,11 @@
       MockInteractions.tap(element.$$('.edit'));
       assert.isTrue(element.editing);
 
-      element._editDraft = '';
+      element._messageText = '';
       // Save should be disabled on an empty message.
       var disabled = element.$$('.save').hasAttribute('disabled');
       assert.isTrue(disabled, 'save button should be disabled.');
-      element._editDraft = '     ';
+      element._messageText = '     ';
       disabled = element.$$('.save').hasAttribute('disabled');
       assert.isTrue(disabled, 'save button should be disabled.');
 
@@ -204,21 +204,55 @@
     });
 
     test('draft saving/editing', function(done) {
+      var fireStub = sinon.stub(element, 'fire');
+
       element.draft = true;
       MockInteractions.tap(element.$$('.edit'));
-      element._editDraft = 'good news, everyone!';
+      element._messageText = 'good news, everyone!';
+      element.flushDebouncer('fire-update');
+      element.flushDebouncer('store');
+      assert(fireStub.calledWith('comment-update'),
+             'comment-update should be sent');
+      assert.deepEqual(fireStub.lastCall.args, [
+        'comment-update', {
+          comment: {
+            __draft: true,
+            __draftID: 'temp_draft_id',
+            __editing: true,
+            line: 5,
+            path: '/path/to/file',
+          },
+        },
+      ]);
       MockInteractions.tap(element.$$('.save'));
+
       assert.isTrue(element.disabled,
           'Element should be disabled when creating draft.');
 
       element._xhrPromise.then(function(draft) {
+        assert(fireStub.calledWith('comment-save'),
+               'comment-save should be sent');
+        assert.deepEqual(fireStub.lastCall.args, [
+          'comment-save', {
+            comment: {
+              __draft: true,
+              __draftID: 'temp_draft_id',
+              __editing: false,
+              id: 'baf0414d_40572e03',
+              line: 5,
+              message: 'saved!',
+              path: '/path/to/file',
+              updated: '2015-12-08 21:52:36.177000000',
+            },
+          },
+        ]);
         assert.isFalse(element.disabled,
-            'Element should be enabled when done creating draft.');
+                       'Element should be enabled when done creating draft.');
         assert.equal(draft.message, 'saved!');
         assert.isFalse(element.editing);
       }).then(function() {
         MockInteractions.tap(element.$$('.edit'));
-        element._editDraft = 'You’ll be delivering a package to Chapek 9, a ' +
+        element._messageText = 'You’ll be delivering a package to Chapek 9, a ' +
             'world where humans are killed on sight.';
         MockInteractions.tap(element.$$('.save'));
         assert.isTrue(element.disabled,
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html
new file mode 100644
index 0000000..5a41709
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html
@@ -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.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
+
+<dom-module id="gr-diff-cursor">
+  <template>
+    <gr-cursor-manager
+        id="cursorManager"
+        scroll="keep-visible"
+        cursor-target-class="target-row"
+        fold-offset-top="[[foldOffsetTop]]"
+        target="{{diffRow}}"></gr-cursor-manager>
+  </template>
+  <script src="gr-diff-cursor.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
new file mode 100644
index 0000000..3de3ae4
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
@@ -0,0 +1,288 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  var DiffSides = {
+    LEFT: 'left',
+    RIGHT: 'right',
+  };
+
+  var DiffViewMode = {
+    SIDE_BY_SIDE: 'SIDE_BY_SIDE',
+    UNIFIED: 'UNIFIED_DIFF',
+  };
+
+  var LEFT_SIDE_CLASS = 'target-side-left';
+  var RIGHT_SIDE_CLASS = 'target-side-right';
+
+  Polymer({
+    is: 'gr-diff-cursor',
+
+    properties: {
+      /**
+       * Either DiffSides.LEFT or DiffSides.RIGHT.
+       */
+      side: {
+        type: String,
+        value: DiffSides.RIGHT,
+      },
+      diffRow: {
+        type: Object,
+        notify: true,
+        observer: '_rowChanged',
+      },
+
+      /**
+       * The diff views to cursor through and listen to.
+       */
+      diffs: {
+        type: Array,
+        value: function() {
+          return [];
+        },
+      },
+
+      foldOffsetTop: {
+        type: Number,
+        value: 0,
+      },
+    },
+
+    observers: [
+      '_updateSideClass(side)',
+      '_diffsChanged(diffs.splices)',
+    ],
+
+    moveLeft: function() {
+      this.side = DiffSides.LEFT;
+      if (this._isTargetBlank()) {
+        this.moveUp()
+      }
+    },
+
+    moveRight: function() {
+      this.side = DiffSides.RIGHT;
+      if (this._isTargetBlank()) {
+        this.moveUp()
+      }
+    },
+
+    moveDown: function() {
+      if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) {
+        this.$.cursorManager.next(this._rowHasSide.bind(this));
+      } else {
+        this.$.cursorManager.next();
+      }
+    },
+
+    moveUp: function() {
+      if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) {
+        this.$.cursorManager.previous(this._rowHasSide.bind(this));
+      } else {
+        this.$.cursorManager.previous();
+      }
+    },
+
+    moveToNextChunk: function() {
+      this.$.cursorManager.next(this._isFirstRowOfChunk.bind(this));
+      this._fixSide();
+    },
+
+    moveToPreviousChunk: function() {
+      this.$.cursorManager.previous(this._isFirstRowOfChunk.bind(this));
+      this._fixSide();
+    },
+
+    moveToNextCommentThread: function() {
+      this.$.cursorManager.next(this._rowHasThread.bind(this));
+      this._fixSide();
+    },
+
+    moveToPreviousCommentThread: function() {
+      this.$.cursorManager.previous(this._rowHasThread.bind(this));
+      this._fixSide();
+    },
+
+    /**
+     * Get the line number element targeted by the cursor row and side.
+     * @return {DOMElement}
+     */
+    getTargetLineElement: function() {
+      var lineElSelector = '.lineNum';
+
+      if (!this.diffRow) {
+        return;
+      }
+
+      if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) {
+        lineElSelector += this.side === DiffSides.LEFT ? '.left' : '.right';
+      }
+
+      return this.diffRow.querySelector(lineElSelector);
+    },
+
+    getTargetDiffElement: function() {
+      // Find the parent diff element of the cursor row.
+      for (var diff = this.diffRow; diff; diff = diff.parentElement) {
+        if (diff.tagName === 'GR-DIFF') { return diff; }
+      }
+      return null;
+    },
+
+    moveToFirstChunk: function() {
+      this.$.cursorManager.moveToStart();
+      this.moveToNextChunk();
+    },
+
+    reInitCursor: function() {
+      this._updateStops();
+      this.moveToFirstChunk();
+    },
+
+    handleDiffUpdate: function() {
+      this._updateStops();
+
+      if (!this.diffRow) {
+        this.reInitCursor();
+      }
+    },
+
+    _getViewMode: function() {
+      if (!this.diffRow) {
+        return null;
+      }
+
+      if (this.diffRow.classList.contains('side-by-side')) {
+        return DiffViewMode.SIDE_BY_SIDE;
+      } else {
+        return DiffViewMode.UNIFIED;
+      }
+    },
+
+    _rowHasSide: function(row) {
+      var selector = (this.side === DiffSides.LEFT ? '.left' : '.right') +
+          ' + .content';
+      return !!row.querySelector(selector);
+    },
+
+    _isFirstRowOfChunk: function(row) {
+      var parentClassList = row.parentNode.classList;
+      return parentClassList.contains('section') &&
+          parentClassList.contains('delta') &&
+          !row.previousSibling;
+    },
+
+    _rowHasThread: function(row) {
+      return row.querySelector('gr-diff-comment-thread');
+    },
+
+    /**
+     * If we jumped to a row where there is no content on the current side then
+     * switch to the alternate side.
+     */
+    _fixSide: function() {
+      if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE &&
+          this._isTargetBlank()) {
+        this.side = this.side === DiffSides.LEFT ?
+            DiffSides.RIGHT : DiffSides.LEFT;
+      }
+    },
+
+    _isTargetBlank: function() {
+      if (!this.diffRow) {
+        return false;
+      }
+
+      var actions = this._getActionsForRow();
+      return (this.side === DiffSides.LEFT && !actions.left) ||
+          (this.side === DiffSides.RIGHT && !actions.right);
+    },
+
+    _rowChanged: function(newRow, oldRow) {
+      if (oldRow) {
+        oldRow.classList.remove(LEFT_SIDE_CLASS, RIGHT_SIDE_CLASS);
+      }
+      this._updateSideClass();
+    },
+
+    _updateSideClass: function() {
+      if (!this.diffRow) {
+        return;
+      }
+      this.toggleClass(LEFT_SIDE_CLASS, this.side === DiffSides.LEFT,
+          this.diffRow);
+      this.toggleClass(RIGHT_SIDE_CLASS, this.side === DiffSides.RIGHT,
+          this.diffRow);
+    },
+
+    _isActionType: function(type) {
+      return type !== 'blank' && type !== 'contextControl';
+    },
+
+    _getActionsForRow: function() {
+      var actions = {left: false, right: false};
+      if (this.diffRow) {
+        actions.left = this._isActionType(
+            this.diffRow.getAttribute('left-type'));
+        actions.right = this._isActionType(
+            this.diffRow.getAttribute('right-type'));
+      }
+      return actions;
+    },
+
+    _getStops: function() {
+      return this.diffs.reduce(
+          function(stops, diff) {
+            return stops.concat(diff.getCursorStops());
+          }, []);
+    },
+
+    _updateStops: function() {
+      this.$.cursorManager.stops = this._getStops();
+    },
+
+    /**
+     * Setup and tear down on-render listeners for any diffs that are added or
+     * removed from the cursor.
+     * @private
+     */
+    _diffsChanged: function(changeRecord) {
+      if (!changeRecord) { return; }
+
+      this._updateStops();
+
+      var splice;
+      var i;
+      for (var spliceIdx = 0;
+        changeRecord.indexSplices &&
+            spliceIdx < changeRecord.indexSplices.length;
+        spliceIdx++) {
+        splice = changeRecord.indexSplices[spliceIdx];
+
+        for (i = splice.index;
+            i < splice.index + splice.addedCount;
+            i++) {
+          this.listen(this.diffs[i], 'render', 'handleDiffUpdate');
+        }
+
+        for (i = 0;
+            i < splice.removed && splice.removed.length;
+            i++) {
+          this.unlisten(splice.removed[i], 'render', 'handleDiffUpdate');
+        }
+      }
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
new file mode 100644
index 0000000..f3c9f95
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
@@ -0,0 +1,185 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-diff-cursor</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/util.js"></script>
+
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="../gr-diff/gr-diff.html">
+<link rel="import" href="./gr-diff-cursor.html">
+<link rel="import" href="./mock-diff-response_test.html">
+
+<test-fixture id="basic">
+  <template>
+    <mock-diff-response></mock-diff-response>
+    <gr-diff></gr-diff>
+    <gr-diff-cursor></gr-diff-cursor>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-diff-cursor tests', function() {
+    var cursorElement;
+    var diffElement;
+    var mockDiffResponse;
+
+    setup(function(done) {
+      stub('gr-rest-api-interface', {
+        getLoggedIn: function() { return Promise.resolve(false); },
+      });
+
+      var fixtureElems = fixture('basic');
+      mockDiffResponse = fixtureElems[0];
+      diffElement = fixtureElems[1];
+      cursorElement = fixtureElems[2];
+
+      // Register the diff with the cursor.
+      cursorElement.push('diffs', diffElement);
+
+      diffElement.$.restAPI.getDiffPreferences().then(function(prefs) {
+        diffElement.prefs = prefs;
+      });
+
+      sinon.stub(diffElement, '_getDiff', function() {
+        return Promise.resolve(mockDiffResponse.diffResponse);
+      });
+
+      sinon.stub(diffElement, '_getDiffComments', function() {
+        return Promise.resolve({baseComments: [], comments: []});
+      });
+
+      sinon.stub(diffElement, '_getDiffDrafts', function() {
+        return Promise.resolve({baseComments: [], comments: []});
+      });
+
+      var setupDone = function() {
+        cursorElement.moveToFirstChunk();
+        done();
+        diffElement.removeEventListener('render', setupDone);
+      };
+      diffElement.addEventListener('render', setupDone);
+
+      diffElement.reload();
+    });
+
+    test('diff cursor functionality (side-by-side)', function() {
+      // The cursor has been initialized to the first delta.
+      assert.isOk(cursorElement.diffRow);
+
+      var firstDeltaRow = diffElement.$$('.section.delta .diff-row');
+      assert.equal(cursorElement.diffRow, firstDeltaRow);
+
+      cursorElement.moveDown()
+
+      assert.notEqual(cursorElement.diffRow, firstDeltaRow);
+      assert.equal(cursorElement.diffRow, firstDeltaRow.nextSibling);
+
+      cursorElement.moveUp();
+
+      assert.notEqual(cursorElement.diffRow, firstDeltaRow.nextSibling);
+      assert.equal(cursorElement.diffRow, firstDeltaRow);
+    });
+
+    test('diff cursor functionality (unified)', function() {
+      diffElement.viewMode = 'UNIFIED_DIFF';
+      cursorElement.reInitCursor();
+
+      // The cursor has been initialized to the first delta.
+      assert.isOk(cursorElement.diffRow);
+
+      var firstDeltaRow = diffElement.$$('.section.delta .diff-row');
+      assert.equal(cursorElement.diffRow, firstDeltaRow);
+
+      firstDeltaRow = diffElement.$$('.section.delta .diff-row');
+      assert.equal(cursorElement.diffRow, firstDeltaRow);
+
+      cursorElement.moveDown();
+
+      assert.notEqual(cursorElement.diffRow, firstDeltaRow);
+      assert.equal(cursorElement.diffRow, firstDeltaRow.nextSibling);
+
+      cursorElement.moveUp();
+
+      assert.notEqual(cursorElement.diffRow, firstDeltaRow.nextSibling);
+      assert.equal(cursorElement.diffRow, firstDeltaRow);
+    });
+
+    test('cursor side functionality', function() {
+      // The side only applies to side-by-side mode, which should be the default
+      // mode.
+      assert.equal(diffElement.viewMode, 'SIDE_BY_SIDE');
+
+      var firstDeltaSection = diffElement.$$('.section.delta');
+      var firstDeltaRow = firstDeltaSection.querySelector('.diff-row');
+
+      // Because the first delta in this diff is on the right, it should be set
+      // to the right side.
+      assert.equal(cursorElement.side, 'right');
+      assert.equal(cursorElement.diffRow, firstDeltaRow);
+      var firstIndex = cursorElement.$.cursorManager.index;
+
+      // Move the side to the left. Because this delta only has a right side, we
+      // should be moved up to the previous line where there is content on the
+      // right. The previous row is part of the previous section.
+      cursorElement.moveLeft();
+
+      assert.equal(cursorElement.side, 'left');
+      assert.notEqual(cursorElement.diffRow, firstDeltaRow);
+      assert.equal(cursorElement.$.cursorManager.index, firstIndex - 1);
+      assert.equal(cursorElement.diffRow.parentElement,
+          firstDeltaSection.previousSibling);
+
+      // If we move down, we should skip everything in the first delta because
+      // we are on the left side and the first delta has no content on the left.
+      cursorElement.moveDown();
+
+      assert.equal(cursorElement.side, 'left');
+      assert.notEqual(cursorElement.diffRow, firstDeltaRow);
+      assert.isTrue(cursorElement.$.cursorManager.index > firstIndex);
+      assert.equal(cursorElement.diffRow.parentElement,
+          firstDeltaSection.nextSibling);
+    });
+
+    test('chunk skip functionality', function() {
+      var chunks = Polymer.dom(diffElement.root).querySelectorAll(
+          '.section.delta');
+      var indexOfChunk = function(chunk) {
+        return Array.prototype.indexOf.call(chunks, chunk);
+      };
+
+      // We should be initialized to the first chunk. Since this chunk only has
+      // content on the right side, our side should be right.
+      var currentIndex = indexOfChunk(cursorElement.diffRow.parentElement);
+      assert.equal(currentIndex, 0);
+      assert.equal(cursorElement.side, 'right');
+
+      // Move to the next chunk.
+      cursorElement.moveToNextChunk();
+
+      // Since this chunk only has content on the left side. we should have been
+      // automatically mvoed over.
+      var previousIndex = currentIndex;
+      currentIndex = indexOfChunk(cursorElement.diffRow.parentElement);
+      assert.equal(currentIndex, previousIndex + 1);
+      assert.equal(cursorElement.side, 'left');
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/mock-diff-response_test.html b/polygerrit-ui/app/elements/diff/gr-diff-cursor/mock-diff-response_test.html
new file mode 100644
index 0000000..ee4bd51
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/mock-diff-response_test.html
@@ -0,0 +1,163 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+
+<dom-module id="mock-diff-response">
+  <template></template>
+  <script>
+    (function() {
+      'use strict';
+
+      var RESPONSE = {
+        "meta_a": {
+          "name": "lorem-ipsum.txt",
+          "content_type": "text/plain",
+          "lines": 45,
+        },
+        "meta_b": {
+          "name": "lorem-ipsum.txt",
+          "content_type": "text/plain",
+          "lines": 48,
+        },
+        "intraline_status": "OK",
+        "change_type": "MODIFIED",
+        "diff_header": [
+          "diff --git a/lorem-ipsum.txt b/lorem-ipsum.txt",
+          "index b2adcf4..554ae49 100644",
+          "--- a/lorem-ipsum.txt",
+          "+++ b/lorem-ipsum.txt",
+        ],
+        "content": [
+          {
+            "ab": [
+              "Lorem ipsum dolor sit amet, suspendisse inceptos vehicula, nulla phasellus.",
+              "Mattis lectus.",
+              "Sodales duis.",
+              "Orci a faucibus.",
+            ]
+          },
+          {
+            "b": [
+              "Nullam neque, ligula ac, id blandit.",
+              "Sagittis tincidunt torquent, tempor nunc amet.",
+              "At rhoncus id.",
+            ],
+          },
+          {
+            "ab": [
+              "Sem nascetur, erat ut, non in.",
+              "A donec, venenatis pellentesque dis.",
+              "Mauris mauris.",
+              "Quisque nisl duis, facilisis viverra.",
+              "Justo purus, semper eget et.",
+            ],
+          },
+          { "a": [
+              "Est amet, vestibulum pellentesque.",
+              "Erat ligula.",
+              "Justo eros.",
+              "Fringilla quisque.",
+            ],
+          },
+          {
+            "ab": [
+              "Arcu eget, rhoncus amet cursus, ipsum elementum.",
+              "Eros suspendisse.",
+            ],
+          },
+          {
+            "a": [
+              "Rhoncus tempor, ultricies aliquam ipsum.",
+            ],
+            "b": [
+              "Rhoncus tempor, ultricies praesent ipsum.",
+            ],
+            "edit_a": [
+              [
+                26,
+                7,
+              ],
+            ],
+            "edit_b": [
+              [
+                26,
+                8,
+              ],
+            ],
+          },
+          {
+            "ab": [
+              "Sollicitudin duis.",
+              "Blandit blandit, ante nisl fusce.",
+              "Felis ac at, tellus consectetuer.",
+              "Sociis ligula sapien, egestas leo.",
+              "Cum pulvinar, sed mauris, cursus neque velit.",
+              "Augue porta lobortis.",
+              "Nibh lorem, amet fermentum turpis, vel pulvinar diam.",
+              "Id quam ipsum, id urna et, massa suspendisse.",
+              "Ac nec, nibh praesent.",
+              "Rutrum vestibulum.",
+              "Est tellus, bibendum habitasse.",
+              "Justo facilisis, vel nulla.",
+              "Donec eu, vulputate neque aliquam, nulla dui.",
+              "Risus adipiscing in.",
+              "Lacus arcu arcu.",
+              "Urna velit.",
+              "Urna a dolor.",
+              "Lectus magna augue, convallis mattis tortor, sed tellus consequat.",
+              "Etiam dui, blandit wisi.",
+              "Mi nec.",
+              "Vitae eget vestibulum.",
+              "Ullamcorper nunc ante, nec imperdiet felis, consectetur in.",
+              "Ac eget.",
+              "Vel fringilla, interdum pellentesque placerat, proin ante.",
+            ],
+          },
+          {
+            "b": [
+              "Eu congue risus.",
+              "Enim ac, quis elementum.",
+              "Non et elit.",
+              "Etiam aliquam, diam vel nunc.",
+            ],
+          },
+          {
+            "ab": [
+              "Nec at.",
+              "Arcu mauris, venenatis lacus fermentum, praesent duis.",
+              "Pellentesque amet et, tellus duis.",
+              "Ipsum arcu vitae, justo elit, sed libero tellus.",
+              "Metus rutrum euismod, vivamus sodales, vel arcu nisl.",
+            ],
+          },
+        ],
+      };
+
+      Polymer({
+        is: 'mock-diff-response',
+        properties: {
+          diffResponse: {
+            type: Object,
+            value: function() {
+              return RESPONSE;
+            },
+          },
+        },
+      });
+    })();
+  </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
index 4480319..903eabf 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
@@ -21,6 +21,7 @@
 <link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../gr-diff/gr-diff.html">
+<link rel="import" href="../gr-diff-cursor/gr-diff-cursor.html">
 <link rel="import" href="../gr-diff-preferences/gr-diff-preferences.html">
 <link rel="import" href="../gr-patch-range-select/gr-patch-range-select.html">
 
@@ -96,6 +97,7 @@
         color: #666;
       }
       .header {
+        align-items: center;
         display: flex;
         justify-content: space-between;
         margin: 0 var(--default-horizontal-margin) .75em;
@@ -123,7 +125,7 @@
       }
     </style>
     <h3>
-      <a href$="[[_computeChangePath(_changeNum, _patchRange.patchNum, _change.revisions)]]">
+      <a href$="[[_computeChangePath(_changeNum, _patchRange.*, _change.revisions)]]">
         [[_changeNum]]</a><span>:</span>
       <span>[[_change.subject]]</span>
       <span class="dash">—</span>
@@ -140,7 +142,7 @@
         <iron-dropdown id="dropdown" vertical-align="top" vertical-offset="25">
           <div class="dropdown-content">
             <template is="dom-repeat" items="[[_fileList]]" as="path">
-              <a href$="[[_computeDiffURL(_changeNum, _patchRange, path)]]"
+              <a href$="[[_computeDiffURL(_changeNum, _patchRange.*, path)]]"
                  selected$="[[_computeFileSelected(path, _path)]]"
                  data-key-nav$="[[_computeKeyNav(path, _path, _fileList)]]"
                  on-tap="_handleFileTap">
@@ -172,15 +174,20 @@
             available-patches="[[_computeAvailablePatches(_change.revisions)]]">
         </gr-patch-range-select>
         <div>
-          <gr-button link
-              class="prefsButton"
-              on-tap="_handlePrefsTap"
-              hidden$="[[_computePrefsButtonHidden(_prefs, _loggedIn)]]"
-              hidden>Diff View Preferences</gr-button>
-          <select id="modeSelect" on-change="_handleModeChange">
+          <select
+              id="modeSelect"
+              on-change="_handleModeChange"
+              hidden$="[[_computeModeSelectHidden(_isImageDiff)]]">
             <option value="SIDE_BY_SIDE">Side By Side</option>
             <option value="UNIFIED_DIFF">Unified</option>
           </select>
+          <span hidden$="[[_computePrefsButtonHidden(_prefs, _loggedIn)]]">
+            <span
+                hidden$="[[_computeModeSelectHidden(_isImageDiff)]]">/</span>
+            <gr-button link
+                class="prefsButton"
+                on-tap="_handlePrefsTap">Preferences</gr-button>
+          </span>
         </div>
       </div>
       <gr-overlay id="prefsOverlay" with-backdrop>
@@ -190,6 +197,9 @@
             on-cancel="_handlePrefsCancel"></gr-diff-preferences>
       </gr-overlay>
       <gr-diff id="diff"
+          project="[[_change.project]]"
+          commit="[[_change.current_revision]]"
+          is-image-diff="{{_isImageDiff}}"
           change-num="[[_changeNum]]"
           patch-range="[[_patchRange]]"
           path="[[_path]]"
@@ -200,6 +210,7 @@
       </gr-diff>
     </div>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+    <gr-diff-cursor id="cursor"></gr-diff-cursor>
   </template>
   <script src="gr-diff-view.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index 6932d10..85cf380 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -69,11 +69,13 @@
         value: true,
       },
       _prefs: Object,
+      _projectConfig: Object,
       _userPrefs: Object,
       _diffMode: {
         type: String,
         computed: '_getDiffViewMode(changeViewState.diffMode, _userPrefs)'
       },
+      _isImageDiff: Boolean,
     },
 
     behaviors: [
@@ -83,7 +85,7 @@
     observers: [
       '_getChangeDetail(_changeNum)',
       '_getProjectConfig(_change.project)',
-      '_getFiles(_changeNum, _patchRange.patchNum)',
+      '_getFiles(_changeNum, _patchRange.*)',
       '_updateModeSelect(_diffMode)',
     ],
 
@@ -99,14 +101,13 @@
         this.fire('title-change',
             {title: this._computeFileDisplayName(this._path)});
       }
-      window.addEventListener('resize', this._boundWindowResizeHandler);
+
+      this.$.cursor.push('diffs', this.$.diff);
     },
 
     detached: function() {
       // Reset the diff mode to null so that it reverts to the user preference.
       this.changeViewState.diffMode = null;
-
-      window.removeEventListener('resize', this._boundWindowResizeHandler);
     },
 
     _getLoggedIn: function() {
@@ -127,9 +128,10 @@
           }.bind(this));
     },
 
-    _getFiles: function(changeNum, patchNum) {
+    _getFiles: function(changeNum, patchRangeRecord) {
+      var patchRange = patchRangeRecord.base;
       return this.$.restAPI.getChangeFilePathsAsSpeciallySortedArray(
-          changeNum, patchNum).then(function(files) {
+          changeNum, patchRange).then(function(files) {
             this._fileList = files;
           }.bind(this));
     },
@@ -164,6 +166,35 @@
       if (this.shouldSupressKeyboardShortcut(e)) { return; }
 
       switch (e.keyCode) {
+        case 37: // left
+          if (e.shiftKey) {
+            e.preventDefault();
+            this.$.cursor.moveLeft();
+          }
+          break;
+        case 39: // right
+          if (e.shiftKey) {
+            e.preventDefault();
+            this.$.cursor.moveRight();
+          }
+          break;
+        case 40: // down
+        case 74: // 'j'
+          e.preventDefault();
+          this.$.cursor.moveDown();
+          break;
+        case 38: // up
+        case 75: // 'k'
+          e.preventDefault();
+          this.$.cursor.moveUp();
+          break;
+        case 67: // 'c'
+          e.preventDefault();
+          var line = this.$.cursor.getTargetLineElement();
+          if (line) {
+            this.$.diff.addDraftAtLine(line);
+          }
+          break;
         case 219:  // '['
           e.preventDefault();
           this._navToFile(this._fileList, -1);
@@ -175,17 +206,17 @@
         case 78:  // 'n'
           e.preventDefault();
           if (e.shiftKey) {
-            this.$.diff.scrollToNextCommentThread();
+            this.$.cursor.moveToNextCommentThread();
           } else {
-            this.$.diff.scrollToNextDiffChunk();
+            this.$.cursor.moveToNextChunk();
           }
           break;
         case 80:  // 'p'
           e.preventDefault();
           if (e.shiftKey) {
-            this.$.diff.scrollToPreviousCommentThread();
+            this.$.cursor.moveToPreviousCommentThread();
           } else {
-            this.$.diff.scrollToPreviousDiffChunk();
+            this.$.cursor.moveToPreviousChunk();
           }
           break;
         case 65:  // 'a'
@@ -196,15 +227,15 @@
         case 85:  // 'u'
           if (this._changeNum && this._patchRange.patchNum) {
             e.preventDefault();
-            page.show(this._computeChangePath(
+            page.show(this._getChangePath(
                 this._changeNum,
-                this._patchRange.patchNum,
+                this._patchRange,
                 this._change && this._change.revisions));
           }
           break;
         case 188:  // ','
           e.preventDefault();
-          this.$.diff.showDiffPreferences();
+          this.$.prefsOverlay.open();
           break;
       }
     },
@@ -221,15 +252,15 @@
 
       var idx = fileList.indexOf(this._path) + direction;
       if (idx < 0 || idx > fileList.length - 1) {
-        page.show(this._computeChangePath(
+        page.show(this._getChangePath(
             this._changeNum,
-            this._patchRange.patchNum,
+            this._patchRange,
             this._change && this._change.revisions));
         return;
       }
-      page.show(this._computeDiffURL(this._changeNum,
-                                     this._patchRange,
-                                     fileList[idx]));
+      page.show(this._getDiffURL(this._changeNum,
+                                 this._patchRange,
+                                 fileList[idx]));
     },
 
     _paramsChanged: function(value) {
@@ -264,11 +295,11 @@
         this._userPrefs = prefs;
       }.bind(this)));
 
-      promises.push(this.$.diff.reload());
+      promises.push(this._getChangeDetail(this._changeNum));
 
-      Promise.all(promises).then(function() {
-        this._loading = false;
-      }.bind(this));
+      Promise.all(promises)
+          .then(function() { return this.$.diff.reload(); }.bind(this))
+          .then(function() { this._loading = false; }.bind(this));
     },
 
     _pathChanged: function(path) {
@@ -282,13 +313,22 @@
       }
     },
 
-    _computeDiffURL: function(changeNum, patchRange, path) {
+    _getDiffURL: function(changeNum, patchRange, path) {
+      return '/c/' + changeNum + '/' + this._patchRangeStr(patchRange) + '/' +
+          path;
+    },
+
+    _computeDiffURL: function(changeNum, patchRangeRecord, path) {
+      return this._getDiffURL(changeNum, patchRangeRecord.base, path);
+    },
+
+    _patchRangeStr: function(patchRange) {
       var patchStr = patchRange.patchNum;
       if (patchRange.basePatchNum != null &&
           patchRange.basePatchNum != 'PARENT') {
         patchStr = patchRange.basePatchNum + '..' + patchRange.patchNum;
       }
-      return '/c/' + changeNum + '/' + patchStr + '/' + path;
+      return patchStr;
     },
 
     _computeAvailablePatches: function(revisions) {
@@ -299,25 +339,30 @@
       return patchNums.sort(function(a, b) { return a - b; });
     },
 
-    _computeChangePath: function(changeNum, patchNum, revisions) {
+    _getChangePath: function(changeNum, patchRange, revisions) {
       var base = '/c/' + changeNum + '/';
 
       // The change may not have loaded yet, making revisions unavailable.
       if (!revisions) {
-        return base + patchNum;
+        return base + this._patchRangeStr(patchRange);
       }
 
       var latestPatchNum = -1;
       for (var rev in revisions) {
         latestPatchNum = Math.max(latestPatchNum, revisions[rev]._number);
       }
-      if (parseInt(patchNum, 10) != latestPatchNum) {
-        return base + patchNum;
+      if (patchRange.basePatchNum !== 'PARENT' ||
+          parseInt(patchRange.patchNum, 10) !== latestPatchNum) {
+        return base + this._patchRangeStr(patchRange);
       }
 
       return base;
     },
 
+    _computeChangePath: function(changeNum, patchRangeRecord, revisions) {
+      return this._getChangePath(changeNum, patchRangeRecord.base, revisions);
+    },
+
     _computeFileDisplayName: function(path) {
       return path == COMMIT_MESSAGE_PATH ? 'Commit message' : path;
     },
@@ -347,8 +392,7 @@
 
     _handleMobileSelectChange: function(e) {
       var path = Polymer.dom(e).rootTarget.value;
-      page.show(
-          this._computeDiffURL(this._changeNum, this._patchRange, path));
+      page.show(this._getDiffURL(this._changeNum, this._patchRange, path));
     },
 
     _showDropdownTapHandler: function(e) {
@@ -420,5 +464,9 @@
         this.$.modeSelect.value = mode;
       }
     },
+
+    _computeModeSelectHidden: function() {
+      return this._isImageDiff;
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
index 4167424..5fb3126 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
@@ -50,6 +50,7 @@
     test('keyboard shortcuts', function() {
       element._changeNum = '42';
       element._patchRange = {
+        basePatchNum: 'PARENT',
         patchNum: '10',
       };
       element._change = {
@@ -89,26 +90,26 @@
           'Should navigate to /c/42/');
       assert.equal(element.changeViewState.selectedFileIndex, 0);
 
-      var showPrefsStub = sinon.stub(element.$.diff, 'showDiffPreferences');
+      var showPrefsStub = sinon.stub(element.$.prefsOverlay, 'open');
       MockInteractions.pressAndReleaseKeyOn(element, 188);  // ','
       assert(showPrefsStub.calledOnce);
 
-      var scrollStub = sinon.stub(element.$.diff, 'scrollToNextDiffChunk');
+      var scrollStub = sinon.stub(element.$.cursor, 'moveToNextChunk');
       MockInteractions.pressAndReleaseKeyOn(element, 78);  // 'n'
       assert(scrollStub.calledOnce);
       scrollStub.restore();
 
-      scrollStub = sinon.stub(element.$.diff, 'scrollToPreviousDiffChunk');
+      scrollStub = sinon.stub(element.$.cursor, 'moveToPreviousChunk');
       MockInteractions.pressAndReleaseKeyOn(element, 80);  // 'p'
       assert(scrollStub.calledOnce);
       scrollStub.restore();
 
-      scrollStub = sinon.stub(element.$.diff, 'scrollToNextCommentThread');
+      scrollStub = sinon.stub(element.$.cursor, 'moveToNextCommentThread');
       MockInteractions.pressAndReleaseKeyOn(element, 78, ['shift']);  // 'N'
       assert(scrollStub.calledOnce);
       scrollStub.restore();
 
-      scrollStub = sinon.stub(element.$.diff, 'scrollToPreviousCommentThread');
+      scrollStub = sinon.stub(element.$.cursor, 'moveToPreviousCommentThread');
       MockInteractions.pressAndReleaseKeyOn(element, 80, ['shift']);  // 'P'
       assert(scrollStub.calledOnce);
       scrollStub.restore();
@@ -143,12 +144,12 @@
       MockInteractions.pressAndReleaseKeyOn(element, 65);  // 'a'
       assert.isTrue(element.changeViewState.showReplyDialog);
 
-      assert(showStub.lastCall.calledWithExactly('/c/42/'),
-          'Should navigate to /c/42/');
+      assert(showStub.lastCall.calledWithExactly('/c/42/5..10'),
+          'Should navigate to /c/42/5..10');
 
       MockInteractions.pressAndReleaseKeyOn(element, 85);  // 'u'
-      assert(showStub.lastCall.calledWithExactly('/c/42/'),
-          'Should navigate to /c/42/');
+      assert(showStub.lastCall.calledWithExactly('/c/42/5..10'),
+          'Should navigate to /c/42/5..10');
 
       MockInteractions.pressAndReleaseKeyOn(element, 221);  // ']'
       assert(showStub.lastCall.calledWithExactly('/c/42/5..10/wheatley.md'),
@@ -166,8 +167,8 @@
       element._path = 'chell.go';
 
       MockInteractions.pressAndReleaseKeyOn(element, 219);  // '['
-      assert(showStub.lastCall.calledWithExactly('/c/42/'),
-          'Should navigate to /c/42/');
+      assert(showStub.lastCall.calledWithExactly('/c/42/5..10'),
+          'Should navigate to /c/42/5..10');
 
       showStub.restore();
     });
@@ -175,6 +176,7 @@
     test('keyboard shortcuts with old patch number', function() {
       element._changeNum = '42';
       element._patchRange = {
+        basePatchNum: 'PARENT',
         patchNum: '1',
       };
       element._change = {
@@ -230,6 +232,7 @@
     test('go up to change via kb without change loaded', function() {
       element._changeNum = '42';
       element._patchRange = {
+        basePatchNum: 'PARENT',
         patchNum: '1',
       };
 
@@ -280,6 +283,7 @@
     test('jump to file dropdown', function() {
       element._changeNum = '42';
       element._patchRange = {
+        basePatchNum: 'PARENT',
         patchNum: '10',
       };
       element._fileList = ['chell.go', 'glados.txt', 'wheatley.md'];
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-image.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-image.js
new file mode 100644
index 0000000..b897708
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-image.js
@@ -0,0 +1,100 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the 'License');
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function(window, GrDiffBuilderSideBySide) {
+  'use strict';
+
+  function GrDiffBuilderImage(diff, comments, prefs, outputEl, baseImage,
+      revisionImage) {
+    GrDiffBuilderSideBySide.call(this, diff, comments, prefs, outputEl);
+    this._baseImage = baseImage;
+    this._revisionImage = revisionImage;
+  }
+
+  GrDiffBuilderImage.prototype = Object.create(
+      GrDiffBuilderSideBySide.prototype);
+  GrDiffBuilderImage.prototype.constructor = GrDiffBuilderImage;
+
+  GrDiffBuilderImage.prototype.emitDiff = function() {
+    this.emitGroup(this._groups[0]);
+
+    var section = this._createElement('tbody', 'image-diff');
+
+    this._emitImagePair(section);
+    this._emitImageLabels(section);
+
+    this._outputEl.appendChild(section);
+  };
+
+  GrDiffBuilderImage.prototype._emitImagePair = function(section) {
+    var tr = this._createElement('tr');
+
+    tr.appendChild(this._createElement('td'));
+    tr.appendChild(this._createImageCell(this._baseImage, 'left'));
+
+    tr.appendChild(this._createElement('td'));
+    tr.appendChild(this._createImageCell(this._revisionImage, 'right'));
+
+    section.appendChild(tr);
+  };
+
+  GrDiffBuilderImage.prototype._createImageCell = function(image, className) {
+    var td = this._createElement('td', className);
+    if (image) {
+      var imageEl = this._createElement('img');
+      imageEl.src = 'data:' + image.type + ';base64, ' + image.body;
+      image._height = imageEl.naturalHeight;
+      image._width = imageEl.naturalWidth;
+      imageEl.addEventListener('error', function(e) {
+        imageEl.remove();
+        td.textContent = '[Image failed to load]';
+      });
+      td.appendChild(imageEl);
+    }
+    return td;
+  };
+
+  GrDiffBuilderImage.prototype._emitImageLabels = function(section) {
+    var tr = this._createElement('tr');
+
+    tr.appendChild(this._createElement('td'));
+    var td = this._createElement('td', 'left');
+    var label = this._createElement('label');
+    label.textContent = this._getImageLabel(this._baseImage);
+    td.appendChild(label);
+    tr.appendChild(td);
+
+    tr.appendChild(this._createElement('td'));
+    td = this._createElement('td', 'right');
+    label = this._createElement('label');
+    label.textContent = this._getImageLabel(this._revisionImage);
+    td.appendChild(label);
+    tr.appendChild(td);
+
+    section.appendChild(tr);
+  };
+
+  GrDiffBuilderImage.prototype._getImageLabel = function(image) {
+    if (image) {
+      var type = image.type || image._expectedType;
+      if (image._width && image._height) {
+        return image._width + '⨉' + image._height + ' ' + type;
+      } else {
+        return type;
+      }
+    }
+    return 'No image';
+  };
+
+  window.GrDiffBuilderImage = GrDiffBuilderImage;
+})(window, GrDiffBuilderSideBySide);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-side-by-side.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-side-by-side.js
index 061ed4f..7e8779f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-side-by-side.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-side-by-side.js
@@ -20,7 +20,7 @@
   GrDiffBuilderSideBySide.prototype = Object.create(GrDiffBuilder.prototype);
   GrDiffBuilderSideBySide.prototype.constructor = GrDiffBuilderSideBySide;
 
-  GrDiffBuilderSideBySide.prototype.emitGroup = function(group,
+  GrDiffBuilderSideBySide.prototype.buildSectionElement = function(group,
       opt_beforeSection) {
     var sectionEl = this._createElement('tbody', 'section');
     sectionEl.classList.add(group.type);
@@ -29,12 +29,16 @@
       sectionEl.appendChild(this._createRow(sectionEl, pairs[i].left,
           pairs[i].right));
     }
-    this._outputEl.insertBefore(sectionEl, opt_beforeSection);
+    return sectionEl;
   };
 
   GrDiffBuilderSideBySide.prototype._createRow = function(section, leftLine,
       rightLine) {
     var row = this._createElement('tr');
+    row.classList.add('diff-row', 'side-by-side');
+    row.setAttribute('left-type', leftLine.type);
+    row.setAttribute('right-type', rightLine.type);
+
     this._appendPair(section, row, leftLine, leftLine.beforeNumber,
         GrDiffBuilder.Side.LEFT);
     this._appendPair(section, row, rightLine, rightLine.afterNumber,
@@ -44,13 +48,14 @@
 
   GrDiffBuilderSideBySide.prototype._appendPair = function(section, row, line,
       lineNumber, side) {
-    row.appendChild(this._createLineEl(line, lineNumber, line.type));
+    var lineEl = this._createLineEl(line, lineNumber, line.type, side);
+    lineEl.classList.add(side);
+    row.appendChild(lineEl);
     var action = this._createContextControl(section, line);
     if (action) {
       row.appendChild(action);
     } else {
       var textEl = this._createTextEl(line);
-      textEl.classList.add(side);
       var threadEl = this._commentThreadForLine(line, side);
       if (threadEl) {
         textEl.appendChild(threadEl);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-unified.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-unified.js
index d9517d3..2f1aac6 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-unified.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-unified.js
@@ -20,22 +20,27 @@
   GrDiffBuilderUnified.prototype = Object.create(GrDiffBuilder.prototype);
   GrDiffBuilderUnified.prototype.constructor = GrDiffBuilderUnified;
 
-  GrDiffBuilderUnified.prototype.emitGroup = function(group,
-      opt_beforeSection) {
+  GrDiffBuilderUnified.prototype.buildSectionElement = function(group) {
     var sectionEl = this._createElement('tbody', 'section');
+    sectionEl.classList.add(group.type);
 
     for (var i = 0; i < group.lines.length; ++i) {
       sectionEl.appendChild(this._createRow(sectionEl, group.lines[i]));
     }
-    this._outputEl.insertBefore(sectionEl, opt_beforeSection);
+    return sectionEl;
   };
 
   GrDiffBuilderUnified.prototype._createRow = function(section, line) {
     var row = this._createElement('tr', line.type);
-    row.appendChild(this._createLineEl(line, line.beforeNumber,
-        GrDiffLine.Type.REMOVE));
-    row.appendChild(this._createLineEl(line, line.afterNumber,
-        GrDiffLine.Type.ADD));
+    var lineEl = this._createLineEl(line, line.beforeNumber,
+        GrDiffLine.Type.REMOVE);
+    lineEl.classList.add('left');
+    row.appendChild(lineEl);
+    lineEl = this._createLineEl(line, line.afterNumber,
+        GrDiffLine.Type.ADD);
+    lineEl.classList.add('right');
+    row.appendChild(lineEl);
+    row.classList.add('diff-row', 'unified');
 
     var action = this._createContextControl(section, line);
     if (action) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder.js
index ce4515c..d2a7c25 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder.js
@@ -15,6 +15,7 @@
   'use strict';
 
   function GrDiffBuilder(diff, comments, prefs, outputEl) {
+    this._diff = diff;
     this._comments = comments;
     this._prefs = prefs;
     this._outputEl = outputEl;
@@ -56,8 +57,51 @@
     }
   };
 
+  GrDiffBuilder.prototype.buildSectionElement = function(
+      group, opt_beforeSection) {
+    throw Error('Subclasses must implement buildGroupElement');
+  };
+
   GrDiffBuilder.prototype.emitGroup = function(group, opt_beforeSection) {
-    throw Error('Subclasses must implement emitGroup');
+    var element = this.buildSectionElement(group);
+    this._outputEl.insertBefore(element, opt_beforeSection);
+    group.element = element;
+  };
+
+  GrDiffBuilder.prototype.renderSection = function(element) {
+    for (var i = 0; i < this._groups.length; i++) {
+      var group = this._groups[i];
+      if (group.element === element) {
+        var newElement = this.buildSectionElement(group);
+        group.element.parentElement.replaceChild(newElement, group.element);
+        group.element = newElement;
+        break;
+      }
+    }
+  };
+
+  GrDiffBuilder.prototype.getSectionsByLineRange = function(
+      startLine, endLine, opt_side) {
+    var sections = [];
+    for (var i = 0; i < this._groups.length; i++) {
+      var group = this._groups[i];
+      if (group.lines.length === 0) {
+        continue;
+      }
+      var groupStartLine;
+      var groupEndLine;
+      if (opt_side === GrDiffBuilder.Side.LEFT) {
+        groupStartLine = group.lines[0].beforeNumber;
+        groupEndLine = group.lines[group.lines.length - 1].beforeNumber;
+      } else if (opt_side === GrDiffBuilder.Side.RIGHT) {
+        groupStartLine = group.lines[0].afterNumber;
+        groupEndLine = group.lines[group.lines.length - 1].afterNumber;
+      }
+      if (startLine <= groupEndLine && endLine >= groupStartLine) {
+        sections.push(group.element);
+      }
+    }
+    return sections;
   };
 
   GrDiffBuilder.prototype._processContent = function(content, groups, context) {
@@ -182,6 +226,7 @@
           currentChunk.ab.push(chunk[j]);
         }
       }
+      // != instead of !== because we want to cover both undefined and null.
       if (currentChunk.ab != null && currentChunk.ab.length > 0) {
         result.push(currentChunk);
       }
@@ -260,7 +305,8 @@
     }
 
     var ctxLine = new GrDiffLine(GrDiffLine.Type.CONTEXT_CONTROL);
-    ctxLine.contextLines = hiddenLines;
+    ctxLine.contextGroup =
+        new GrDiffGroup(GrDiffGroup.Type.BOTH, hiddenLines);
     groups.push(new GrDiffGroup(GrDiffGroup.Type.CONTEXT_CONTROL,
         [ctxLine]));
 
@@ -310,13 +356,14 @@
   };
 
   GrDiffBuilder.prototype._createContextControl = function(section, line) {
-    if (!line.contextLines.length) {
+    if (!line.contextGroup || !line.contextGroup.lines.length) {
       return null;
     }
+    var contextLines = line.contextGroup.lines;
     var td = this._createElement('td');
     var button = this._createElement('gr-button', 'showContext');
     button.setAttribute('link', true);
-    var commonLines = line.contextLines.length;
+    var commonLines = contextLines.length;
     var text = 'Show ' + commonLines + ' common line';
     if (commonLines > 1) {
       text += 's';
@@ -325,7 +372,7 @@
     button.textContent = text;
     button.addEventListener('tap', function(e) {
       e.detail = {
-        group: new GrDiffGroup(GrDiffGroup.Type.BOTH, line.contextLines),
+        group: line.contextGroup,
         section: section,
       };
       // Let it bubble up the DOM tree.
@@ -382,7 +429,7 @@
     }
 
     var patchNum = this._comments.meta.patchRange.patchNum;
-    var side = 'REVISION';
+    var side = comments[0].side || 'REVISION';
     if (line.type === GrDiffLine.Type.REMOVE ||
         opt_side === GrDiffBuilder.Side.LEFT) {
       if (this._comments.meta.patchRange.basePatchNum === 'PARENT') {
@@ -401,14 +448,18 @@
     return threadEl;
   };
 
-  GrDiffBuilder.prototype._createLineEl = function(line, number, type) {
+  GrDiffBuilder.prototype._createLineEl = function(line, number, type,
+      opt_class) {
     var td = this._createElement('td');
+    if (opt_class) {
+      td.classList.add(opt_class);
+    }
     if (line.type === GrDiffLine.Type.BLANK) {
       return td;
     } else if (line.type === GrDiffLine.Type.CONTEXT_CONTROL) {
       td.classList.add('contextLineNum');
       td.setAttribute('data-value', '@@');
-    } else if (line.type === GrDiffLine.Type.BOTH || line.type == type) {
+    } else if (line.type === GrDiffLine.Type.BOTH || line.type === type) {
       td.classList.add('lineNum');
       td.setAttribute('data-value', number);
     }
@@ -471,18 +522,18 @@
 
     // Tags don't count as characters
     while (index < html.length &&
-           html.charCodeAt(index) == GrDiffBuilder.LESS_THAN_CODE) {
+           html.charCodeAt(index) === GrDiffBuilder.LESS_THAN_CODE) {
       while (index < html.length &&
-             html.charCodeAt(index) != GrDiffBuilder.GREATER_THAN_CODE) {
+             html.charCodeAt(index) !== GrDiffBuilder.GREATER_THAN_CODE) {
         index++;
       }
       index++;  // skip the ">" itself
     }
     // An HTML entity (e.g., &lt;) counts as one character.
     if (index < html.length &&
-        html.charCodeAt(index) == GrDiffBuilder.AMPERSAND_CODE) {
+        html.charCodeAt(index) === GrDiffBuilder.AMPERSAND_CODE) {
       while (index < html.length &&
-             html.charCodeAt(index) != GrDiffBuilder.SEMICOLON_CODE) {
+             html.charCodeAt(index) !== GrDiffBuilder.SEMICOLON_CODE) {
         index++;
       }
     }
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder_test.html
index 22b9072..4392de0 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder_test.html
@@ -144,8 +144,9 @@
       assert.equal(groups[0].lines[0].afterNumber, GrDiffLine.FILE);
 
       assert.equal(groups[1].type, GrDiffGroup.Type.CONTEXT_CONTROL);
-      assert.equal(groups[1].lines[0].contextLines.length, 90);
-      groups[1].lines[0].contextLines.forEach(function(l) {
+      assert.instanceOf(groups[1].lines[0].contextGroup, GrDiffGroup);
+      assert.equal(groups[1].lines[0].contextGroup.lines.length, 90);
+      groups[1].lines[0].contextGroup.lines.forEach(function(l) {
         assert.equal(l.text, content[0].ab[0]);
       });
 
@@ -179,8 +180,9 @@
       });
 
       assert.equal(groups[7].type, GrDiffGroup.Type.CONTEXT_CONTROL);
-      assert.equal(groups[7].lines[0].contextLines.length, 90);
-      groups[7].lines[0].contextLines.forEach(function(l) {
+      assert.instanceOf(groups[7].lines[0].contextGroup, GrDiffGroup);
+      assert.equal(groups[7].lines[0].contextGroup.lines.length, 90);
+      groups[7].lines[0].contextGroup.lines.forEach(function(l) {
         assert.equal(l.text, content[4].ab[0]);
       });
 
@@ -215,8 +217,9 @@
       });
 
       assert.equal(groups[3].type, GrDiffGroup.Type.CONTEXT_CONTROL);
-      assert.equal(groups[3].lines[0].contextLines.length, 30);
-      groups[3].lines[0].contextLines.forEach(function(l) {
+      assert.instanceOf(groups[3].lines[0].contextGroup, GrDiffGroup);
+      assert.equal(groups[3].lines[0].contextGroup.lines.length, 30);
+      groups[3].lines[0].contextGroup.lines.forEach(function(l) {
         assert.equal(l.text, content[1].ab[0]);
       });
 
@@ -517,5 +520,54 @@
         }
       ]);
     });
+
+    suite('rendering', function() {
+      var content;
+      var outputEl;
+
+      setup(function() {
+        var prefs = {
+          line_length: 10,
+          show_tabs: true,
+          tab_size: 4,
+          context: -1
+        };
+        content = [
+          {ab: []},
+          {a: ['all work and no play make andybons a dull boy']},
+          {ab: []},
+          {b: ['elgoog elgoog elgoog']},
+          {ab: []},
+        ];
+        outputEl = document.createElement('out');
+        builder =
+            new GrDiffBuilder(
+                {content: content}, {left: [], right: []}, prefs, outputEl);
+        builder.buildSectionElement = function(group) {
+          var section = document.createElement('stub');
+          section.textContent = group.lines.reduce(function(acc, line) {
+            return acc + line.text;
+          }, '');
+          return section;
+        };
+        builder.emitDiff();
+      });
+
+      test('renderSection', function() {
+        var section = outputEl.querySelector('stub:nth-of-type(2)');
+        var prevInnerHTML = section.innerHTML;
+        section.innerHTML = 'wiped';
+        builder.renderSection(section);
+        section = outputEl.querySelector('stub:nth-of-type(2)');
+        assert.equal(section.innerHTML, prevInnerHTML);
+      });
+
+      test('getSectionsByLineRange', function() {
+        var section = outputEl.querySelector('stub:nth-of-type(2)');
+        var sections = builder.getSectionsByLineRange(1, 1, 'left');
+        assert.equal(sections.length, 1);
+        assert.strictEqual(sections[0], section);
+      });
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js
index 750f7da..7c7c508 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js
@@ -25,6 +25,8 @@
     }
   }
 
+  GrDiffGroup.prototype.element = null;
+
   GrDiffGroup.Type = {
     BOTH: 'both',
     CONTEXT_CONTROL: 'contextControl',
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js
index ea00a3d..4acde0c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js
@@ -16,13 +16,14 @@
 
   function GrDiffLine(type) {
     this.type = type;
-    this.contextLines = [];
     this.highlights = [];
   }
 
+  GrDiffLine.prototype.afterNumber = 0;
+
   GrDiffLine.prototype.beforeNumber = 0;
 
-  GrDiffLine.prototype.afterNumber = 0;
+  GrDiffLine.prototype.contextGroup = null;
 
   GrDiffLine.prototype.text = '';
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
index 7b1ce00..826a21e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -43,6 +43,27 @@
       .section {
         background-color: #eee;
       }
+      .image-diff .gr-diff {
+        text-align: center;
+      }
+      .image-diff img {
+        max-width: 50em;
+        outline: 1px solid #ccc;
+      }
+      .image-diff label {
+        font-family: var(--font-family);
+        font-style: italic;
+      }
+      .diff-row.target-row.target-side-left .lineNum.left,
+      .diff-row.target-row.target-side-right .lineNum.right,
+      .diff-row.target-row.unified .lineNum {
+        background-color: #BBDEFB;
+      }
+      .diff-row.target-row.target-side-left .lineNum.left:before,
+      .diff-row.target-row.target-side-right .lineNum.right:before,
+      .diff-row.target-row.unified .lineNum:before {
+        color: #000;
+      }
       .blank,
       .content {
         background-color: #fff;
@@ -64,9 +85,6 @@
       .canComment .lineNum[data-value] {
         cursor: pointer;
       }
-      .canComment .lineNum[data-value]:before {
-        text-decoration: underline;
-      }
       .canComment .lineNum[data-value]:hover:before {
         background-color: #ccc;
       }
@@ -144,5 +162,6 @@
   <script src="gr-diff-builder.js"></script>
   <script src="gr-diff-builder-side-by-side.js"></script>
   <script src="gr-diff-builder-unified.js"></script>
+  <script src="gr-diff-builder-image.js"></script>
   <script src="gr-diff.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index 8fef29c..d813933 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -42,6 +42,13 @@
         type: Object,
         observer: '_projectConfigChanged',
       },
+      project: String,
+      commit: String,
+      isImageDiff: {
+        type: Boolean,
+        computed: '_computeIsImageDiff(_diff)',
+        notify: true,
+      },
 
       _loggedIn: {
         type: Boolean,
@@ -58,29 +65,23 @@
         observer: '_selectionSideChanged',
       },
       _comments: Object,
-      _focusedSection: {
-        type: Number,
-        value: -1,
-      },
-      _focusedThread: {
-        type: Number,
-        value: -1,
-      },
     },
 
     observers: [
       '_prefsChanged(prefs.*, viewMode)',
     ],
 
+    listeners: {
+      'thread-discard': '_handleThreadDiscard',
+      'comment-discard': '_handleCommentDiscard',
+      'comment-update': '_handleCommentUpdate',
+      'comment-save': '_handleCommentSave',
+    },
+
     attached: function() {
       this._getLoggedIn().then(function(loggedIn) {
         this._loggedIn = loggedIn;
       }.bind(this));
-
-      this.addEventListener('thread-discard',
-          this._handleThreadDiscard.bind(this));
-      this.addEventListener('comment-discard',
-          this._handleCommentDiscard.bind(this));
     },
 
     reload: function() {
@@ -90,6 +91,7 @@
 
       promises.push(this._getDiff().then(function(diff) {
         this._diff = diff;
+        return this._loadDiffAssets();
       }.bind(this)));
 
       promises.push(this._getDiffCommentsAndDrafts().then(function(comments) {
@@ -103,10 +105,6 @@
       }.bind(this));
     },
 
-    showDiffPreferences: function() {
-      this.$.prefsOverlay.open();
-    },
-
     scrollToLine: function(lineNum) {
       if (isNaN(lineNum) || lineNum < 1) { return; }
 
@@ -118,24 +116,29 @@
       this._scrollToElement(el);
     },
 
-    scrollToNextDiffChunk: function() {
-      this._focusedSection = this._advanceElementWithinNodeList(
-          this._getDeltaSections(), this._focusedSection, 1);
+    getCursorStops: function() {
+      if (this.hidden) {
+        return [];
+      }
+
+      return Polymer.dom(this.root).querySelectorAll('.diff-row');
     },
 
-    scrollToPreviousDiffChunk: function() {
-      this._focusedSection = this._advanceElementWithinNodeList(
-          this._getDeltaSections(), this._focusedSection, -1);
-    },
+    addDraftAtLine: function(el) {
+      this._getLoggedIn().then(function(loggedIn) {
+        if (!loggedIn) { return; }
 
-    scrollToNextCommentThread: function() {
-      this._focusedThread = this._advanceElementWithinNodeList(
-          this._getCommentThreads(), this._focusedThread, 1);
-    },
-
-    scrollToPreviousCommentThread: function() {
-      this._focusedThread = this._advanceElementWithinNodeList(
-          this._getCommentThreads(), this._focusedThread, -1);
+        var value = el.getAttribute('data-value');
+        if (value === GrDiffLine.FILE) {
+          this._addDraft(el);
+          return;
+        }
+        var lineNum = parseInt(value, 10);
+        if (isNaN(lineNum)) {
+          throw Error('Invalid line number: ' + value);
+        }
+        this._addDraft(el, lineNum);
+      }.bind(this));
     },
 
     _advanceElementWithinNodeList: function(els, curIndex, direction) {
@@ -151,10 +154,6 @@
       return Polymer.dom(this.root).querySelectorAll('gr-diff-comment-thread');
     },
 
-    _getDeltaSections: function() {
-      return Polymer.dom(this.root).querySelectorAll('.section.delta');
-    },
-
     _scrollToElement: function(el) {
       if (!el) { return; }
 
@@ -198,27 +197,10 @@
       if (el.classList.contains('showContext')) {
         this._showContext(e.detail.group, e.detail.section);
       } else if (el.classList.contains('lineNum')) {
-        this._handleLineTap(el);
+        this.addDraftAtLine(el);
       }
     },
 
-    _handleLineTap: function(el) {
-      this._getLoggedIn().then(function(loggedIn) {
-        if (!loggedIn) { return; }
-
-        var value = el.getAttribute('data-value');
-        if (value === GrDiffLine.FILE) {
-          this._addDraft(el);
-          return;
-        }
-        var lineNum = parseInt(value, 10);
-        if (isNaN(lineNum)) {
-          throw Error('Invalid line number: ' + value);
-        }
-        this._addDraft(el, lineNum);
-      }.bind(this));
-    },
-
     _addDraft: function(lineEl, opt_lineNum) {
       var threadEl;
 
@@ -233,7 +215,7 @@
       } else {
         var patchNum = this.patchRange.patchNum;
         var side = 'REVISION';
-        if (contentEl.classList.contains(DiffSide.LEFT) ||
+        if (lineEl.classList.contains(DiffSide.LEFT) ||
             contentEl.classList.contains('remove')) {
           if (this.patchRange.basePatchNum === 'PARENT') {
             side = 'PARENT';
@@ -254,29 +236,71 @@
     },
 
     _handleCommentDiscard: function(e) {
-      var comment = Polymer.dom(e).rootTarget.comment;
-      this._removeComment(comment);
+      var comment = e.detail.comment;
+      this._removeComment(comment, e.target.patchNum);
     },
 
-    _removeComment: function(comment) {
-      if (!comment.id) { return; }
-      this._removeCommentFromSide(comment, DiffSide.LEFT) ||
-          this._removeCommentFromSide(comment, DiffSide.RIGHT);
+    _removeComment: function(comment, opt_patchNum) {
+      var side = this._findCommentSide(comment, opt_patchNum);
+      this._removeCommentFromSide(comment, side);
+    },
+
+    _findCommentSide: function(comment, opt_patchNum) {
+      if (comment.side === 'PARENT') {
+        return DiffSide.LEFT;
+      } else {
+        return this._comments.meta.patchRange.basePatchNum === opt_patchNum ?
+            DiffSide.LEFT : DiffSide.RIGHT;
+      }
+    },
+
+    _handleCommentSave: function(e) {
+      var comment = e.detail.comment;
+      var side = this._findCommentSide(comment, e.target.patchNum);
+      var idx = this._findDraftIndex(comment, side);
+      this.set(['_comments', side, idx], comment);
+    },
+
+    _handleCommentUpdate: function(e) {
+      var comment = e.detail.comment;
+      var side = this._findCommentSide(comment, e.target.patchNum);
+      var idx = this._findCommentIndex(comment, side);
+      if (idx === -1) {
+        idx = this._findDraftIndex(comment, side);
+      }
+      if (idx !== -1) { // Update draft or comment.
+        this.set(['_comments', side, idx], comment);
+      } else { // Create new draft.
+        this.push(['_comments', side], comment);
+      }
     },
 
     _removeCommentFromSide: function(comment, side) {
-      var idx = -1;
-      for (var i = 0; i < this._comments[side].length; i++) {
-        if (this._comments[side][i].id === comment.id) {
-          idx = i;
-          break;
-        }
+      var idx = this._findCommentIndex(comment, side);
+      if (idx === -1) {
+        idx = this._findDraftIndex(comment, side);
       }
       if (idx !== -1) {
         this.splice('_comments.' + side, idx, 1);
-        return true;
       }
-      return false;
+    },
+
+    _findCommentIndex: function(comment, side) {
+      if (!comment.id || !this._comments[side]) {
+        return -1;
+      }
+      return this._comments[side].findIndex(function(item) {
+        return item.id === comment.id;
+      });
+    },
+
+    _findDraftIndex: function(comment, side) {
+      if (!comment.__draftID || !this._comments[side]) {
+        return -1;
+      }
+      return this._comments[side].findIndex(function(item) {
+        return item.__draftID === comment.__draftID;
+      });
     },
 
     _handleMouseDown: function(e) {
@@ -340,8 +364,19 @@
     },
 
     _showContext: function(group, sectionEl) {
+      var groups = this._builder._groups;
+      // TODO(viktard): Polyfill findIndex for IE10.
+      var contextIndex = groups.findIndex(function(group) {
+        return group.element == sectionEl;
+      });
+      groups[contextIndex] = group;
+
       this._builder.emitGroup(group, sectionEl);
       sectionEl.parentNode.removeChild(sectionEl);
+
+      this.async(function() {
+        this.fire('render', null, {bubbles: false});
+      }.bind(this), 1);
     },
 
     _prefsChanged: function(prefsChangeRecord) {
@@ -355,14 +390,17 @@
     },
 
     _render: function() {
-      this._clearDiffContent();
-      this._builder = this._getDiffBuilder(this._diff, this._comments,
-          this.prefs);
-      this._builder.emitDiff(this._diff.content);
+      this._builder =
+          this._getDiffBuilder(this._diff, this._comments, this.prefs);
+      this._renderDiff();
+    },
 
+    _renderDiff: function() {
+      this._clearDiffContent();
+      this._builder.emitDiff();
       this.async(function() {
         this.fire('render', null, {bubbles: false});
-      }.bind(this), 1);
+      }, 1);
     },
 
     _clearDiffContent: function() {
@@ -438,8 +476,40 @@
       return this.$.restAPI.getLoggedIn();
     },
 
+    _computeIsImageDiff: function() {
+      if (!this._diff) { return false; }
+
+      var isA = this._diff.meta_a &&
+          this._diff.meta_a.content_type.indexOf('image/') === 0;
+      var isB = this._diff.meta_b &&
+          this._diff.meta_b.content_type.indexOf('image/') === 0;
+
+      return this._diff.binary && (isA || isB);
+    },
+
+    _loadDiffAssets: function() {
+      if (this.isImageDiff) {
+        return this._getImages().then(function(images) {
+          this._baseImage = images.baseImage;
+          this._revisionImage = images.revisionImage;
+        }.bind(this));
+      } else {
+        this._baseImage = null;
+        this._revisionImage = null;
+        return Promise.resolve();
+      }
+    },
+
+    _getImages: function() {
+      return this.$.restAPI.getImagesForDiff(this.project, this.commit,
+          this.changeNum, this._diff, this.patchRange);
+    },
+
     _getDiffBuilder: function(diff, comments, prefs) {
-      if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) {
+      if (this.isImageDiff) {
+        return new GrDiffBuilderImage(diff, comments, prefs, this.$.diffTable,
+            this._baseImage, this._revisionImage);
+      } else if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) {
         return new GrDiffBuilderSideBySide(diff, comments, prefs,
             this.$.diffTable);
       } else if (this.viewMode === DiffViewMode.UNIFIED) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
index f02b861..aec32b6 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -20,6 +20,7 @@
 
 <script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
 <script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/util.js"></script>
 
 <link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
 <link rel="import" href="gr-diff.html">
@@ -34,85 +35,29 @@
   suite('gr-diff tests', function() {
     var element;
 
-    setup(function() {
-      stub('gr-rest-api-interface', {
-        getLoggedIn: function() { return Promise.resolve(false); },
+    suite('not logged in', function() {
+
+      setup(function() {
+        stub('gr-rest-api-interface', {
+          getLoggedIn: function() { return Promise.resolve(false); },
+        });
+        element = fixture('basic');
       });
-      element = fixture('basic');
-    });
 
-    test('get drafts logged out', function(done) {
-      element.patchRange = {basePatchNum: 0, patchNum: 0};
+      test('get drafts', function(done) {
+        element.patchRange = {basePatchNum: 0, patchNum: 0};
 
-      var getDraftsStub = sinon.stub(element.$.restAPI, 'getDiffDrafts');
-      var loggedInStub = sinon.stub(element, '_getLoggedIn',
-          function() { return Promise.resolve(false); });
-      element._getDiffDrafts().then(function(result) {
-        assert.deepEqual(result, {baseComments: [], comments: []});
-        sinon.assert.notCalled(getDraftsStub);
-        loggedInStub.restore();
-        getDraftsStub.restore();
-        done();
+        var getDraftsStub = sinon.stub(element.$.restAPI, 'getDiffDrafts');
+        element._getDiffDrafts().then(function(result) {
+          assert.deepEqual(result, {baseComments: [], comments: []});
+          sinon.assert.notCalled(getDraftsStub);
+          getDraftsStub.restore();
+          done();
+        });
       });
-    });
 
-    test('get drafts logged in', function(done) {
-      element.patchRange = {basePatchNum: 0, patchNum: 0};
-      var draftsResponse = {
-        baseComments: [{id: 'foo'}],
-        comments: [{id: 'bar'}],
-      };
-      var getDraftsStub = sinon.stub(element.$.restAPI, 'getDiffDrafts',
-          function() { return Promise.resolve(draftsResponse); });
-      var loggedInStub = sinon.stub(element, '_getLoggedIn',
-          function() { return Promise.resolve(true); });
-      element._getDiffDrafts().then(function(result) {
-        assert.deepEqual(result, draftsResponse);
-        loggedInStub.restore();
-        getDraftsStub.restore();
-        done();
-      });
-    });
-
-    test('get comments and drafts', function(done) {
-      var loggedInStub = sinon.stub(element, '_getLoggedIn',
-          function() { return Promise.resolve(true); });
-      var comments = {
-        baseComments: [
-          {id: 'bc1'},
-          {id: 'bc2'},
-        ],
-        comments: [
-          {id: 'c1'},
-          {id: 'c2'},
-        ],
-      };
-      var diffCommentsStub = sinon.stub(element, '_getDiffComments',
-          function() { return Promise.resolve(comments); });
-
-      var drafts = {
-        baseComments: [
-          {id: 'bd1'},
-          {id: 'bd2'},
-        ],
-        comments: [
-          {id: 'd1'},
-          {id: 'd2'},
-        ],
-      };
-      var diffDraftsStub = sinon.stub(element, '_getDiffDrafts',
-          function() { return Promise.resolve(drafts); });
-
-      element.changeNum = '42';
-      element.patchRange = {
-        basePatchNum: 'PARENT',
-        patchNum: 3,
-      };
-      element.path = '/path/to/foo';
-      element.projectConfig = {foo: 'bar'};
-
-      element._getDiffCommentsAndDrafts().then(function(result) {
-        assert.deepEqual(result, {
+      test('remove comment', function() {
+        element._comments = {
           meta: {
             changeNum: '42',
             patchRange: {
@@ -123,10 +68,10 @@
             projectConfig: {foo: 'bar'},
           },
           left: [
-            {id: 'bc1'},
-            {id: 'bc2'},
-            {id: 'bd1', __draft: true},
-            {id: 'bd2', __draft: true},
+            {id: 'bc1', side: 'PARENT'},
+            {id: 'bc2', side: 'PARENT'},
+            {id: 'bd1', __draft: true, side: 'PARENT'},
+            {id: 'bd2', __draft: true, side: 'PARENT'},
           ],
           right: [
             {id: 'c1'},
@@ -134,113 +79,348 @@
             {id: 'd1', __draft: true},
             {id: 'd2', __draft: true},
           ],
-        });
+        };
 
-        diffCommentsStub.restore();
-        diffDraftsStub.restore();
-        loggedInStub.restore();
-        done();
+        element._removeComment({});
+        // Using JSON.stringify because Safari 9.1 (11601.5.17.1) doesn’t seem
+        // to believe that one object deepEquals another even when they do :-/.
+        assert.equal(JSON.stringify(element._comments), JSON.stringify({
+          meta: {
+            changeNum: '42',
+            patchRange: {
+              basePatchNum: 'PARENT',
+              patchNum: 3,
+            },
+            path: '/path/to/foo',
+            projectConfig: {foo: 'bar'},
+          },
+          left: [
+            {id: 'bc1', side: 'PARENT'},
+            {id: 'bc2', side: 'PARENT'},
+            {id: 'bd1', __draft: true, side: 'PARENT'},
+            {id: 'bd2', __draft: true, side: 'PARENT'},
+          ],
+          right: [
+            {id: 'c1'},
+            {id: 'c2'},
+            {id: 'd1', __draft: true},
+            {id: 'd2', __draft: true},
+          ],
+        }));
+
+        element._removeComment({id: 'bc2', side: 'PARENT'});
+        assert.equal(JSON.stringify(element._comments), JSON.stringify({
+          meta: {
+            changeNum: '42',
+            patchRange: {
+              basePatchNum: 'PARENT',
+              patchNum: 3,
+            },
+            path: '/path/to/foo',
+            projectConfig: {foo: 'bar'},
+          },
+          left: [
+            {id: 'bc1', side: 'PARENT'},
+            {id: 'bd1', __draft: true, side: 'PARENT'},
+            {id: 'bd2', __draft: true, side: 'PARENT'},
+          ],
+          right: [
+            {id: 'c1'},
+            {id: 'c2'},
+            {id: 'd1', __draft: true},
+            {id: 'd2', __draft: true},
+          ],
+        }));
+
+        element._removeComment({id: 'd2'});
+        assert.deepEqual(JSON.stringify(element._comments), JSON.stringify({
+          meta: {
+            changeNum: '42',
+            patchRange: {
+              basePatchNum: 'PARENT',
+              patchNum: 3,
+            },
+            path: '/path/to/foo',
+            projectConfig: {foo: 'bar'},
+          },
+          left: [
+            {id: 'bc1', side: 'PARENT'},
+            {id: 'bd1', __draft: true, side: 'PARENT'},
+            {id: 'bd2', __draft: true, side: 'PARENT'},
+          ],
+          right: [
+            {id: 'c1'},
+            {id: 'c2'},
+            {id: 'd1', __draft: true},
+          ],
+        }));
+      });
+
+      test('renders image diffs', function(done) {
+        var mockDiff = {
+          meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
+          meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 560},
+          intraline_status: 'OK',
+          change_type: 'MODIFIED',
+          diff_header: [
+            'diff --git a/carrot.jpg b/carrot.jpg',
+            'index 2adc47d..f9c2f2c 100644',
+            '--- a/carrot.jpg',
+            '+++ b/carrot.jpg',
+            'Binary files differ',
+          ],
+          content: [{skip: 66}],
+          binary: true,
+        };
+        var mockFile1 = {
+          body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAEwsA' +
+              'AAAAAAAAAAAAAAAA/w==',
+          type: 'image/bmp',
+        };
+        var mockFile2 = {
+          body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAEwsA' +
+              'AAAAAAAAAAAA/////w==',
+          type: 'image/bmp'
+        };
+        var mockCommit = {
+          commit: '9a1a1d10baece5efbba10bc4ccf808a67a50ac0a',
+          parents: [{
+            commit: '7338aa9adfe57909f1fdaf88975cdea467d3382f',
+            subject: 'Added a carrot',
+          }],
+          author: {
+            name: 'Wyatt Allen',
+            email: 'wyatta@google.com',
+            date: '2016-05-23 21:44:51.000000000',
+            tz: -420,
+          },
+          committer: {
+            name: 'Wyatt Allen',
+            email: 'wyatta@google.com',
+            date: '2016-05-25 00:25:41.000000000',
+            tz: -420,
+          },
+          subject: 'Updated the carrot',
+          message: 'Updated the carrot\n\nChange-Id: Iabcd123\n',
+        };
+        var mockComments = {baseComments: [], comments: []};
+
+        var stubs = [];
+        stubs.push(sinon.stub(element, '_getDiff',
+            function() { return Promise.resolve(mockDiff); }));
+        stubs.push(sinon.stub(element.$.restAPI, 'getCommitInfo',
+            function() { return Promise.resolve(mockCommit); }));
+        stubs.push(sinon.stub(element.$.restAPI,
+            'getCommitFileContents',
+            function() { return Promise.resolve(mockFile1); }));
+        stubs.push(sinon.stub(element.$.restAPI,
+            'getChangeFileContents',
+            function() { return Promise.resolve(mockFile2); }));
+        stubs.push(sinon.stub(element.$.restAPI, '_getDiffComments',
+            function() { return Promise.resolve(mockComments); }));
+        stubs.push(sinon.stub(element.$.restAPI, 'getDiffDrafts',
+            function() { return Promise.resolve(mockComments); }));
+
+        element.patchRange = {basePatchNum: 'PARENT', patchNum: 1};
+
+        var rendered = function() {
+          // Recognizes that it should be an image diff.
+          assert.isTrue(element.isImageDiff);
+          assert.instanceOf(element._getDiffBuilder(element._diff,
+              element._comments, element.prefs), GrDiffBuilderImage);
+
+          // Left image rendered with the parent commit's version of the file.
+          var leftInmage = element.$.diffTable.querySelector('td.left img');
+          assert.isOk(leftInmage);
+          assert.equal(leftInmage.getAttribute('src'),
+              'data:image/bmp;base64, ' + mockFile1.body);
+
+          // Right image rendered with this change's revision of the image.
+          var rightInmage = element.$.diffTable.querySelector('td.right img');
+          assert.isOk(rightInmage);
+          assert.equal(rightInmage.getAttribute('src'),
+              'data:image/bmp;base64, ' + mockFile2.body);
+
+          // Cleanup.
+          element.removeEventListener('render', rendered);
+          stubs.forEach(function(stub) { stub.restore(); });
+
+          done();
+        };
+
+        element.addEventListener('render', rendered);
+
+        element.$.restAPI.getDiffPreferences().then(function(prefs) {
+          element.prefs = prefs;
+          element.reload();
+        });
       });
     });
 
-    test('remove comment', function() {
-      element._comments = {
-        meta: {
-          changeNum: '42',
-          patchRange: {
-            basePatchNum: 'PARENT',
-            patchNum: 3,
-          },
-          path: '/path/to/foo',
-          projectConfig: {foo: 'bar'},
-        },
-        left: [
-          {id: 'bc1'},
-          {id: 'bc2'},
-          {id: 'bd1', __draft: true},
-          {id: 'bd2', __draft: true},
-        ],
-        right: [
-          {id: 'c1'},
-          {id: 'c2'},
-          {id: 'd1', __draft: true},
-          {id: 'd2', __draft: true},
-        ],
-      };
+    suite('logged in', function() {
 
-      element._removeComment({});
-      // Using JSON.stringify because Safari 9.1 (11601.5.17.1) doesn’t seem to
-      // believe that one object deepEquals another even when they do :-/.
-      assert.equal(JSON.stringify(element._comments), JSON.stringify({
-        meta: {
-          changeNum: '42',
-          patchRange: {
-            basePatchNum: 'PARENT',
-            patchNum: 3,
-          },
-          path: '/path/to/foo',
-          projectConfig: {foo: 'bar'},
-        },
-        left: [
-          {id: 'bc1'},
-          {id: 'bc2'},
-          {id: 'bd1', __draft: true},
-          {id: 'bd2', __draft: true},
-        ],
-        right: [
-          {id: 'c1'},
-          {id: 'c2'},
-          {id: 'd1', __draft: true},
-          {id: 'd2', __draft: true},
-        ],
-      }));
+      setup(function() {
+        stub('gr-rest-api-interface', {
+          getLoggedIn: function() { return Promise.resolve(true); },
+        });
+        element = fixture('basic');
+      });
 
-      element._removeComment({id: 'bc2'});
-      assert.equal(JSON.stringify(element._comments), JSON.stringify({
-        meta: {
-          changeNum: '42',
-          patchRange: {
-            basePatchNum: 'PARENT',
-            patchNum: 3,
-          },
-          path: '/path/to/foo',
-          projectConfig: {foo: 'bar'},
-        },
-        left: [
-          {id: 'bc1'},
-          {id: 'bd1', __draft: true},
-          {id: 'bd2', __draft: true},
-        ],
-        right: [
-          {id: 'c1'},
-          {id: 'c2'},
-          {id: 'd1', __draft: true},
-          {id: 'd2', __draft: true},
-        ],
-      }));
+      test('get drafts', function(done) {
+        element.patchRange = {basePatchNum: 0, patchNum: 0};
+        var draftsResponse = {
+          baseComments: [{id: 'foo'}],
+          comments: [{id: 'bar'}],
+        };
+        var getDraftsStub = sinon.stub(element.$.restAPI, 'getDiffDrafts',
+            function() { return Promise.resolve(draftsResponse); });
+        element._getDiffDrafts().then(function(result) {
+          assert.deepEqual(result, draftsResponse);
+          getDraftsStub.restore();
+          done();
+        });
+      });
 
-      element._removeComment({id: 'd2'});
-      assert.deepEqual(JSON.stringify(element._comments), JSON.stringify({
-        meta: {
-          changeNum: '42',
-          patchRange: {
-            basePatchNum: 'PARENT',
-            patchNum: 3,
-          },
-          path: '/path/to/foo',
-          projectConfig: {foo: 'bar'},
-        },
-        left: [
-          {id: 'bc1'},
-          {id: 'bd1', __draft: true},
-          {id: 'bd2', __draft: true},
-        ],
-        right: [
-          {id: 'c1'},
-          {id: 'c2'},
-          {id: 'd1', __draft: true},
-        ],
-      }));
+      test('get comments and drafts', function(done) {
+        var comments = {
+          baseComments: [
+            {id: 'bc1'},
+            {id: 'bc2'},
+          ],
+          comments: [
+            {id: 'c1'},
+            {id: 'c2'},
+          ],
+        };
+        var diffCommentsStub = sinon.stub(element, '_getDiffComments',
+            function() { return Promise.resolve(comments); });
+
+        var drafts = {
+          baseComments: [
+            {id: 'bd1'},
+            {id: 'bd2'},
+          ],
+          comments: [
+            {id: 'd1'},
+            {id: 'd2'},
+          ],
+        };
+        var diffDraftsStub = sinon.stub(element, '_getDiffDrafts',
+            function() { return Promise.resolve(drafts); });
+
+        element.changeNum = '42';
+        element.patchRange = {
+          basePatchNum: 'PARENT',
+          patchNum: 3,
+        };
+        element.path = '/path/to/foo';
+        element.projectConfig = {foo: 'bar'};
+
+        element._getDiffCommentsAndDrafts().then(function(result) {
+          assert.deepEqual(result, {
+            meta: {
+              changeNum: '42',
+              patchRange: {
+                basePatchNum: 'PARENT',
+                patchNum: 3,
+              },
+              path: '/path/to/foo',
+              projectConfig: {foo: 'bar'},
+            },
+            left: [
+              {id: 'bc1'},
+              {id: 'bc2'},
+              {id: 'bd1', __draft: true},
+              {id: 'bd2', __draft: true},
+            ],
+            right: [
+              {id: 'c1'},
+              {id: 'c2'},
+              {id: 'd1', __draft: true},
+              {id: 'd2', __draft: true},
+            ],
+          });
+
+          diffCommentsStub.restore();
+          diffDraftsStub.restore();
+          done();
+        });
+      });
+
+      suite('handle comment-update', function() {
+
+        setup(function() {
+          element._comments = {
+            meta: {
+              changeNum: '42',
+              patchRange: {
+                basePatchNum: 'PARENT',
+                patchNum: 3,
+              },
+              path: '/path/to/foo',
+              projectConfig: {foo: 'bar'},
+            },
+            left: [
+              {id: 'bc1', side: 'PARENT'},
+              {id: 'bc2', side: 'PARENT'},
+              {id: 'bd1', __draft: true, side: 'PARENT'},
+              {id: 'bd2', __draft: true, side: 'PARENT'},
+            ],
+            right: [
+              {id: 'c1'},
+              {id: 'c2'},
+              {id: 'd1', __draft: true},
+              {id: 'd2', __draft: true},
+            ],
+          };
+        });
+
+        test('creating a draft', function() {
+          var comment = {__draft: true, __draftID: 'tempID', side: 'PARENT'};
+          element.fire('comment-update', {comment: comment});
+          assert.include(element._comments.left, comment);
+        });
+
+        test('saving a draft', function() {
+          var draftID = 'tempID';
+          var id = 'savedID';
+          element._comments.left.push(
+              {__draft: true, __draftID: draftID, side: 'PARENT'});
+          element.fire('comment-update', {comment:
+              {id: id, __draft: true, __draftID: draftID, side: 'PARENT'},
+          });
+          var drafts = element._comments.left.filter(function(item) {
+            return item.__draftID === draftID;
+          });
+          assert.equal(drafts.length, 1);
+          assert.equal(drafts[0].id, id);
+        });
+      });
+    });
+
+    suite('renderDiff', function() {
+      setup(function(done) {
+        sinon.stub(element, 'fire');
+        element._builder = {
+          emitDiff: sinon.stub(),
+        };
+        element._renderDiff();
+        flush(function() {
+          done();
+        });
+      });
+
+      teardown(function() {
+        element.fire.restore();
+      });
+
+      test('fires render', function() {
+        assert(element.fire.calledWithExactly(
+            'render', null, {bubbles: false}));
+      });
+      test('calls emitDiff on builder', function() {
+        assert(element._builder.emitDiff.calledOnce);
+      });
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index 8897967..affcd44 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -17,6 +17,12 @@
   Polymer({
     is: 'gr-app',
 
+    /**
+     * Fired when the URL location changes.
+     *
+     * @event location-change
+     */
+
     properties: {
       params: Object,
       keyEventTarget: {
@@ -68,7 +74,7 @@
       this._viewState = {
         changeView: {
           changeNum: null,
-          patchNum: null,
+          patchRange: null,
           selectedFileIndex: 0,
           showReplyDialog: false,
           diffMode: null,
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
index 3655975..17a0ed9 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
@@ -57,6 +57,12 @@
 
     _buildAvatarURL: function(account) {
       if (!account) { return ''; }
+      var avatars = account.avatars || [];
+      for (var i = 0; i < avatars.length; i++) {
+        if (avatars[i].height === this.imageSize) {
+          return avatars[i].url;
+        }
+      }
       return '/accounts/' + account._account_id + '/avatar?s=' + this.imageSize;
     },
   });
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
index b55a1c3..ae514ba 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
@@ -43,6 +43,36 @@
             _account_id: 123
           }),
           '/accounts/123/avatar?s=16');
+      assert.equal(element._buildAvatarURL(
+          {
+            _account_id: 123,
+            avatars: [
+              {
+                url: 'https://cdn.example.com/s12-p/photo.jpg',
+                height: 12
+              },
+              {
+                url: 'https://cdn.example.com/s16-p/photo.jpg',
+                height: 16
+              },
+              {
+                url: 'https://cdn.example.com/s100-p/photo.jpg',
+                height: 100
+              },
+            ],
+          }),
+          'https://cdn.example.com/s16-p/photo.jpg');
+      assert.equal(element._buildAvatarURL(
+          {
+            _account_id: 123,
+            avatars: [
+              {
+                url: 'https://cdn.example.com/s95-p/photo.jpg',
+                height: 95
+              },
+            ],
+          }),
+          '/accounts/123/avatar?s=16');
     });
 
     test('dom for existing account', function() {
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
index 1df98fd..c815ffd 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
@@ -15,6 +15,7 @@
 -->
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html">
 
 <dom-module id="gr-button">
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
index 772fccc..e109896 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
@@ -31,6 +31,7 @@
 
     behaviors: [
       Gerrit.KeyboardShortcutBehavior,
+      Gerrit.TooltipBehavior,
     ],
 
     hostAttributes: {
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.html b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.html
new file mode 100644
index 0000000..f213312
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.html
@@ -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.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+
+<dom-module id="gr-cursor-manager">
+  <template></template>
+  <script src="gr-cursor-manager.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
new file mode 100644
index 0000000..e1990c4
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
@@ -0,0 +1,227 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  var ScrollBehavior = {
+    ALWAYS: 'always',
+    NEVER: 'never',
+    KEEP_VISIBLE: 'keep-visible',
+  };
+
+  Polymer({
+    is: 'gr-cursor-manager',
+
+    properties: {
+      stops: {
+        type: Array,
+        value: function() {
+          return [];
+        },
+        observer: '_updateIndex',
+      },
+      target: {
+        type: Object,
+        notify: true,
+        observer: '_scrollToTarget',
+      },
+
+      /**
+       * The index of the current target (if any). -1 otherwise.
+       */
+      index: {
+        type: Number,
+        value: -1,
+      },
+
+      /**
+       * The class to apply to the current target. Use null for no class.
+       */
+      cursorTargetClass: {
+        type: String,
+        value: null,
+      },
+
+      /**
+       * The scroll behavior for the cursor. Values are 'never', 'always' and
+       * 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond
+       * the viewport.
+       */
+      scroll: {
+        type: String,
+        value: ScrollBehavior.NEVER,
+      },
+
+      /**
+       * When using the 'keep-visible' scroll behavior, set an offset to the top
+       * of the window for what is considered above the upper fold.
+       */
+      foldOffsetTop: {
+        type: Number,
+        value: 0,
+      },
+    },
+
+    detached: function() {
+      this.unsetCursor();
+    },
+
+    next: function(opt_condition) {
+      this._moveCursor(1, opt_condition);
+    },
+
+    previous: function(opt_condition) {
+      this._moveCursor(-1, opt_condition);
+    },
+
+    /**
+     * Set the cursor to an arbitrary element.
+     * @param {DOMElement}
+     */
+    setCursor: function(element) {
+      this.unsetCursor();
+      this.target = element;
+      this._updateIndex();
+      this._decorateTarget();
+    },
+
+    unsetCursor: function() {
+      this._unDecorateTarget();
+      this.index = -1;
+      this.target = null;
+    },
+
+    isAtStart: function() {
+      return this.index === 0;
+    },
+
+    isAtEnd: function() {
+      return this.index === this.stops.length - 1;
+    },
+
+    moveToStart: function() {
+      if (this.stops.length) {
+        this.setCursor(this.stops[0]);
+      }
+    },
+
+    /**
+     * Move the cursor forward or backward by delta. Noop if moving past either
+     * end of the stop list.
+     * @param {Number} delta: either -1 or 1.
+     * @param {Function} opt_condition Optional stop condition. If a condition
+     *    is passed the cursor will continue to move in the specified direction
+     *    until the condition is met.
+     * @private
+     */
+    _moveCursor: function(delta, opt_condition) {
+      if (!this.stops.length) {
+        this.unsetCursor();
+        return;
+      }
+
+      this._unDecorateTarget();
+
+      var newIndex = this._getNextindex(delta, opt_condition);
+
+      var newTarget = null;
+      if (newIndex != -1) {
+        newTarget = this.stops[newIndex];
+      }
+
+      this.index = newIndex;
+      this.target = newTarget;
+
+      this._decorateTarget();
+    },
+
+    _decorateTarget: function() {
+      if (this.target && this.cursorTargetClass) {
+        this.target.classList.add(this.cursorTargetClass);
+      }
+    },
+
+    _unDecorateTarget: function() {
+      if (this.target && this.cursorTargetClass) {
+        this.target.classList.remove(this.cursorTargetClass);
+      }
+    },
+
+    /**
+     * Get the next stop index indicated by the delta direction.
+     * @param {Number} delta: either -1 or 1.
+     * @param {Function} opt_condition Optional stop condition.
+     * @return {Number} the new index.
+     * @private
+     */
+    _getNextindex: function(delta, opt_condition) {
+      if (!this.stops.length || this.index === -1) {
+        return -1;
+      }
+
+      var newIndex = this.index;
+      do {
+        newIndex = newIndex + delta;
+      } while(newIndex > 0 &&
+              newIndex < this.stops.length - 1 &&
+              opt_condition && !opt_condition(this.stops[newIndex]));
+
+      newIndex = Math.max(0, Math.min(this.stops.length - 1, newIndex));
+
+      // If we failed to satisfy the condition:
+      if (opt_condition && !opt_condition(this.stops[newIndex])) {
+        return this.index;
+      }
+
+      return newIndex;
+    },
+
+    _updateIndex: function() {
+      if (!this.target) {
+        this.index = -1;
+        return;
+      }
+
+      var newIndex = Array.prototype.indexOf.call(this.stops, this.target);
+      if (newIndex === -1) {
+        this.unsetCursor();
+      } else {
+        this.index = newIndex;
+      }
+    },
+
+    _scrollToTarget: function() {
+      if (!this.target || this.scroll === ScrollBehavior.NEVER) { return; }
+
+      // Calculate where the element is relative to the window.
+      var top = this.target.offsetTop;
+      for (var offsetParent = this.target.offsetParent;
+           offsetParent;
+           offsetParent = offsetParent.offsetParent) {
+        top += offsetParent.offsetTop;
+      }
+
+      if (this.scroll === ScrollBehavior.KEEP_VISIBLE &&
+          top > window.pageYOffset + this.foldOffsetTop &&
+          top < window.pageYOffset + window.innerHeight) { return; }
+
+      // Scroll the element to the middle of the window. Dividing by a third
+      // instead of half the inner height feels a bit better otherwise the
+      // element appears to be below the center of the window even when it
+      // isn't.
+      window.scrollTo(0, top - (window.innerHeight / 3) +
+          (this.target.offsetHeight / 2));
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html
new file mode 100644
index 0000000..1ad014d
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html
@@ -0,0 +1,124 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-cursor-manager</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-cursor-manager.html">
+
+<test-fixture id="basic">
+  <template>
+    <gr-cursor-manager
+        cursor-stop-selector="li"
+        cursor-target-class="targeted"></gr-cursor-manager>
+    <ul>
+      <li>A</li>
+      <li>B</li>
+      <li>C</li>
+      <li>D</li>
+    </ul>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-cursor-manager tests', function() {
+    var element;
+    var list;
+
+    setup(function() {
+      var fixtureElements = fixture('basic');
+      element = fixtureElements[0];
+      list = fixtureElements[1];
+    });
+
+    test('core cursor functionality', function() {
+      // The element is initialized into the proper state.
+      assert.isArray(element.stops);
+      assert.equal(element.stops.length, 0);
+      assert.equal(element.index, -1);
+      assert.isNotOk(element.target);
+
+      // Initialize the cursor with its stops.
+      element.stops = list.querySelectorAll('li');
+
+      // It should have the stops but it should not be targeting any of them.
+      assert.isNotNull(element.stops);
+      assert.equal(element.stops.length, 4);
+      assert.equal(element.index, -1);
+      assert.isNotOk(element.target);
+
+      // Select the third stop.
+      element.setCursor(list.children[2]);
+
+      // It should update its internal state and update the element's class.
+      assert.equal(element.index, 2);
+      assert.equal(element.target, list.children[2]);
+      assert.isTrue(list.children[2].classList.contains('targeted'));
+      assert.isFalse(element.isAtStart());
+      assert.isFalse(element.isAtEnd());
+
+      // Progress the cursor.
+      element.next();
+
+      // Confirm that the next stop is selected and that the previous stop is
+      // unselected.
+      assert.equal(element.index, 3);
+      assert.equal(element.target, list.children[3]);
+      assert.isTrue(element.isAtEnd());
+      assert.isFalse(list.children[2].classList.contains('targeted'));
+      assert.isTrue(list.children[3].classList.contains('targeted'));
+
+      // Progress the cursor.
+      element.next();
+
+      // We should still be at the end.
+      assert.equal(element.index, 3);
+      assert.equal(element.target, list.children[3]);
+      assert.isTrue(element.isAtEnd());
+
+      // Wind the cursor all the way back to the first stop.
+      element.previous();
+      element.previous();
+      element.previous();
+
+      // The element state should reflect the end of the list.
+      assert.equal(element.index, 0);
+      assert.equal(element.target, list.children[0]);
+      assert.isTrue(element.isAtStart());
+      assert.isTrue(list.children[0].classList.contains('targeted'));
+
+      var newLi = document.createElement('li');
+      newLi.textContent = 'Z';
+      list.insertBefore(newLi, list.children[0]);
+      element.stops = list.querySelectorAll('li');
+
+      assert.equal(element.index, 1);
+
+      // De-select all targets.
+      element.unsetCursor();
+
+      // There should now be no cursor target.
+      assert.isFalse(list.children[1].classList.contains('targeted'));
+      assert.isNotOk(element.target);
+      assert.equal(element.index, -1);
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
index 10c8a29..72ae4e4 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
@@ -18,7 +18,6 @@
 <script src="../../../bower_components/fetch/fetch.js"></script>
 
 <dom-module id="gr-rest-api-interface">
-  <template></template>
   <script src="gr-rest-api-interface.js"></script>
 </dom-module>
 
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index a2fa039..9604ded 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -220,6 +220,7 @@
 
         return Promise.resolve({
           changes_per_page: 25,
+          diff_view: 'SIDE_BY_SIDE',
         });
       }.bind(this));
     },
@@ -315,18 +316,22 @@
           this.getChangeActionURL(changeNum, patchNum, '/commit?links'));
     },
 
-    getChangeFiles: function(changeNum, patchNum) {
+    getChangeFiles: function(changeNum, patchRange) {
+      var endpoint = '/files';
+      if (patchRange.basePatchNum !== 'PARENT') {
+        endpoint += '?base=' + encodeURIComponent(patchRange.basePatchNum);
+      }
       return this.fetchJSON(
-          this.getChangeActionURL(changeNum, patchNum, '/files'));
+          this.getChangeActionURL(changeNum, patchRange.patchNum, endpoint));
     },
 
-    getChangeFilesAsSpeciallySortedArray: function(changeNum, patchNum) {
-      return this.getChangeFiles(changeNum, patchNum).then(
+    getChangeFilesAsSpeciallySortedArray: function(changeNum, patchRange) {
+      return this.getChangeFiles(changeNum, patchRange).then(
           this._normalizeChangeFilesResponse.bind(this));
     },
 
-    getChangeFilePathsAsSpeciallySortedArray: function(changeNum, patchNum) {
-      return this.getChangeFiles(changeNum, patchNum).then(function(files) {
+    getChangeFilePathsAsSpeciallySortedArray: function(changeNum, patchRange) {
+      return this.getChangeFiles(changeNum, patchRange).then(function(files) {
         return Object.keys(files).sort(this._specialFilePathCompare.bind(this));
       }.bind(this));
     },
@@ -584,6 +589,7 @@
 
       function onlyParent(c) { return c.side == PARENT_PATCH_NUM; }
       function withoutParent(c) { return c.side != PARENT_PATCH_NUM; }
+      function setPath(c) { c.path = opt_path; }
 
       var promises = [];
       var comments;
@@ -594,8 +600,11 @@
         comments = response[opt_path] || [];
         if (opt_basePatchNum == PARENT_PATCH_NUM) {
           baseComments = comments.filter(onlyParent);
+          baseComments.forEach(setPath);
         }
         comments = comments.filter(withoutParent);
+
+        comments.forEach(setPath);
       }.bind(this)));
 
       if (opt_basePatchNum != PARENT_PATCH_NUM) {
@@ -603,6 +612,7 @@
             opt_basePatchNum);
         promises.push(this.fetchJSON(baseURL).then(function(response) {
           baseComments = (response[opt_path] || []).filter(withoutParent);
+          baseComments.forEach(setPath);
         }));
       }
 
@@ -672,5 +682,84 @@
       return '';
     },
 
+    getCommitInfo: function(project, commit) {
+      return this.fetchJSON(
+          '/projects/' + encodeURIComponent(project) +
+          '/commits/' + encodeURIComponent(commit));
+    },
+
+    _fetchB64File: function(url) {
+      return fetch(url).then(function(response) {
+        var type = response.headers.get('X-FYI-Content-Type');
+        return response.text()
+          .then(function(text) {
+            return {body: text, type: type};
+          });
+      });
+    },
+
+    getChangeFileContents: function(changeId, patchNum, path) {
+      return this._fetchB64File(
+          '/changes/' + encodeURIComponent(changeId) +
+          '/revisions/' + encodeURIComponent(patchNum) +
+          '/files/' + encodeURIComponent(path) +
+          '/content');
+    },
+
+    getCommitFileContents: function(projectName, commit, path) {
+      return this._fetchB64File(
+          '/projects/' + encodeURIComponent(projectName) +
+          '/commits/' + encodeURIComponent(commit) +
+          '/files/' + encodeURIComponent(path) +
+          '/content');
+    },
+
+    getImagesForDiff: function(project, commit, changeNum, diff, patchRange) {
+      var promiseA;
+      var promiseB;
+
+      if (diff.meta_a && diff.meta_a.content_type.indexOf('image/') === 0) {
+        if (patchRange.basePatchNum === 'PARENT') {
+          // Need the commit info know the parent SHA.
+          promiseA = this.getCommitInfo(project, commit).then(function(info) {
+            if (info.parents.length !== 1) {
+              return Promise.reject('Change commit has multiple parents.');
+            }
+            var parent = info.parents[0].commit;
+            return this.getCommitFileContents(project, parent,
+                diff.meta_a.name);
+          }.bind(this));
+
+        } else {
+          promiseA = this.getChangeFileContents(changeNum,
+              patchRange.basePatchNum, diff.meta_a.name);
+        }
+      } else {
+        promiseA = Promise.resolve(null);
+      }
+
+      if (diff.meta_b && diff.meta_b.content_type.indexOf('image/') === 0) {
+        promiseB = this.getChangeFileContents(changeNum, patchRange.patchNum,
+            diff.meta_b.name);
+      } else {
+        promiseB = Promise.resolve(null);
+      }
+
+      return Promise.all([promiseA, promiseB])
+        .then(function(results) {
+          var baseImage = results[0];
+          var revisionImage = results[1];
+
+          // Sometimes the server doesn't send back the content type.
+          if (baseImage) {
+            baseImage._expectedType = diff.meta_a.content_type;
+          }
+          if (revisionImage) {
+            revisionImage._expectedType = diff.meta_b.content_type;
+          }
+
+          return {baseImage: baseImage, revisionImage: revisionImage};
+        }.bind(this));
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index e7f7a21..ef0741a 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -140,10 +140,12 @@
           assert.deepEqual(obj.baseComments[0], {
             side: 'PARENT',
             message: 'how did this work in the first place?',
+            path: 'sieve.go',
           });
           assert.equal(obj.comments.length, 1);
           assert.deepEqual(obj.comments[0], {
             message: 'this isn’t quite right',
+            path: 'sieve.go',
           });
           fetchJSONStub.restore();
           done();
@@ -188,13 +190,16 @@
           assert.equal(obj.baseComments.length, 1);
           assert.deepEqual(obj.baseComments[0], {
             message: 'this isn’t quite right',
+            path: 'sieve.go',
           });
           assert.equal(obj.comments.length, 2);
           assert.deepEqual(obj.comments[0], {
             message: 'What on earth are you thinking, here?',
+            path: 'sieve.go',
           });
           assert.deepEqual(obj.comments[1], {
             message: '¯\\_(ツ)_/¯',
+            path: 'sieve.go',
           });
           fetchJSONStub.restore();
           done();
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.html b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.html
new file mode 100644
index 0000000..74bfcdf
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.html
@@ -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.
+-->
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<dom-module id="gr-storage">
+  <script src="gr-storage.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
new file mode 100644
index 0000000..ef3a6c0
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
@@ -0,0 +1,85 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  // Date cutoff is one day:
+  var DRAFT_MAX_AGE = 24*60*60*1000;
+
+  // Clean up old entries no more frequently than one day.
+  var CLEANUP_THROTTLE_INTERVAL = 24*60*60*1000;
+
+  Polymer({
+    is: 'gr-storage',
+
+    properties: {
+      _lastCleanup: Number,
+      _storage: {
+        type: Object,
+        value: function() {
+          return window.localStorage;
+        },
+      },
+    },
+
+    getDraftComment: function(location) {
+      this._cleanupDrafts();
+      return this._getObject(this._getDraftKey(location));
+    },
+
+    setDraftComment: function(location, message) {
+      var key = this._getDraftKey(location);
+      this._setObject(key, {message: message, updated: Date.now()});
+    },
+
+    eraseDraftComment: function(location) {
+      var key = this._getDraftKey(location);
+      this._storage.removeItem(key);
+    },
+
+    _getDraftKey: function(location) {
+      return ['draft', location.changeNum, location.patchNum, location.path,
+          location.line].join(':');
+    },
+
+    _cleanupDrafts: function() {
+      // Throttle cleanup to the throttle interval.
+      if (this._lastCleanup &&
+          Date.now() - this._lastCleanup < CLEANUP_THROTTLE_INTERVAL) {
+        return;
+      }
+      this._lastCleanup = Date.now();
+
+      var draft;
+      for (var key in this._storage) {
+        if (key.indexOf('draft:') === 0) {
+          draft = this._getObject(key);
+          if (Date.now() - draft.updated > DRAFT_MAX_AGE) {
+            this._storage.removeItem(key);
+          }
+        }
+      }
+    },
+
+    _getObject: function(key) {
+      var serial = this._storage.getItem(key);
+      if (!serial) { return null; }
+      return JSON.parse(serial);
+    },
+
+    _setObject: function(key, obj) {
+      this._storage.setItem(key, JSON.stringify(obj));
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
new file mode 100644
index 0000000..4e17cb6
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
@@ -0,0 +1,117 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-storage</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+
+<link rel="import" href="gr-storage.html">
+
+<test-fixture id="basic">
+  <template>
+    <gr-storage></gr-storage>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-storage tests', function() {
+    var element;
+    var storage;
+
+    setup(function() {
+      element = fixture('basic');
+      storage = element._storage;
+      cleanupStorage();
+    });
+
+    function cleanupStorage() {
+      // Make sure there are no entries in storage.
+      for (var key in window.localStorage) {
+        window.localStorage.removeItem(key);
+      }
+    }
+
+    test('storing, retrieving and erasing drafts', function() {
+      var changeNum = 1234;
+      var patchNum = 5;
+      var path = 'my_source_file.js';
+      var line = 123;
+      var location = {
+        changeNum: changeNum,
+        patchNum: patchNum,
+        path: path,
+        line: line,
+      };
+
+      // The key is in the expected format.
+      var key = element._getDraftKey(location);
+      assert.equal(key, ['draft', changeNum, patchNum, path, line].join(':'));
+
+      // There should be no draft initially.
+      var draft = element.getDraftComment(location);
+      assert.isNotOk(draft);
+
+      // Setting the draft stores it under the expected key.
+      element.setDraftComment(location, 'my comment');
+      assert.isOk(storage.getItem(key));
+      assert.equal(JSON.parse(storage.getItem(key)).message, 'my comment');
+      assert.isOk(JSON.parse(storage.getItem(key)).updated);
+
+      // Erasing the draft removes the key.
+      element.eraseDraftComment(location);
+      assert.isNotOk(storage.getItem(key));
+
+      cleanupStorage();
+    });
+
+    test('automatically removes old drafts', function() {
+      var changeNum = 1234;
+      var patchNum = 5;
+      var path = 'my_source_file.js';
+      var line = 123;
+      var location = {
+        changeNum: changeNum,
+        patchNum: patchNum,
+        path: path,
+        line: line,
+      };
+      var key = element._getDraftKey(location);
+
+      // Make sure that the call to cleanup doesn't get throttled.
+      element._lastCleanup = 0;
+
+      var cleanupSpy = sinon.spy(element, '_cleanupDrafts');
+
+      // Create a message with a timestamp that is a second behind the max age.
+      storage.setItem(key, JSON.stringify({
+        message: 'old message',
+        updated: Date.now() - 24*60*60*1000 - 1000,
+      }));
+
+      // Getting the draft should cause it to be removed.
+      var draft = element.getDraftComment(location);
+
+      assert.isTrue(cleanupSpy.called);
+      assert.isNotOk(draft);
+      assert.isNotOk(storage.getItem(key));
+
+      cleanupSpy.restore();
+      cleanupStorage();
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.html b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.html
new file mode 100644
index 0000000..64ef9b2
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.html
@@ -0,0 +1,49 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+
+<dom-module id="gr-tooltip">
+  <template>
+    <style>
+      :host {
+        --gr-tooltip-arrow-size: .6em;
+
+        background-color: #333;
+        box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
+        color: #fff;
+        font-size: .75rem;
+        padding: .5em .85em;
+        position: absolute;
+        z-index: 1000;
+      }
+      .arrow {
+        border-left: var(--gr-tooltip-arrow-size) solid transparent;
+        border-right: var(--gr-tooltip-arrow-size) solid transparent;
+        border-top: var(--gr-tooltip-arrow-size) solid #333;
+        bottom: -var(--gr-tooltip-arrow-size);
+        height: 0;
+        position: absolute;
+        left: calc(50% - var(--gr-tooltip-arrow-size));
+        width: 0;
+      }
+    </style>
+    [[text]]
+    <i class="arrow"></i>
+  </template>
+  <script src="gr-tooltip.js"></script>
+</dom-module>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js
new file mode 100644
index 0000000..76372ba
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js
@@ -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.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-tooltip',
+
+    properties: {
+      text: String,
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index efdbaef..781f457 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -22,45 +22,49 @@
 <script src="../bower_components/web-component-tester/browser.js"></script>
 <script>
   var testFiles = [];
+  var basePath = '../elements/';
 
   [
-    '../elements/change/gr-change-actions/gr-change-actions_test.html',
-    '../elements/change/gr-change-metadata/gr-change-metadata_test.html',
-    '../elements/change/gr-change-view/gr-change-view_test.html',
-    '../elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html',
-    '../elements/change/gr-download-dialog/gr-download-dialog_test.html',
-    '../elements/change/gr-file-list/gr-file-list_test.html',
-    '../elements/change/gr-message/gr-message_test.html',
-    '../elements/change/gr-messages-list/gr-messages-list_test.html',
-    '../elements/change/gr-related-changes-list/gr-related-changes-list_test.html',
-    '../elements/change/gr-reply-dialog/gr-reply-dialog_test.html',
-    '../elements/change/gr-reviewer-list/gr-reviewer-list_test.html',
-    '../elements/change-list/gr-change-list/gr-change-list_test.html',
-    '../elements/change-list/gr-change-list-item/gr-change-list-item_test.html',
-    '../elements/core/gr-account-dropdown/gr-account-dropdown_test.html',
-    '../elements/core/gr-error-manager/gr-error-manager_test.html',
-    '../elements/core/gr-main-header/gr-main-header_test.html',
-    '../elements/core/gr-search-bar/gr-search-bar_test.html',
-    '../elements/diff/gr-diff/gr-diff-builder_test.html',
-    '../elements/diff/gr-diff/gr-diff-group_test.html',
-    '../elements/diff/gr-diff/gr-diff_test.html',
-    '../elements/diff/gr-diff-comment/gr-diff-comment_test.html',
-    '../elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html',
-    '../elements/diff/gr-diff-preferences/gr-diff-preferences_test.html',
-    '../elements/diff/gr-diff-view/gr-diff-view_test.html',
-    '../elements/diff/gr-patch-range-select/gr-patch-range-select_test.html',
-    '../elements/shared/gr-alert/gr-alert_test.html',
-    '../elements/shared/gr-account-label/gr-account-label_test.html',
-    '../elements/shared/gr-account-link/gr-account-link_test.html',
-    '../elements/shared/gr-alert/gr-alert_test.html',
-    '../elements/shared/gr-avatar/gr-avatar_test.html',
-    '../elements/shared/gr-change-star/gr-change-star_test.html',
-    '../elements/shared/gr-confirm-dialog/gr-confirm-dialog_test.html',
-    '../elements/shared/gr-date-formatter/gr-date-formatter_test.html',
-    '../elements/shared/gr-js-api-interface/gr-js-api-interface_test.html',
-    '../elements/shared/gr-linked-text/gr-linked-text_test.html',
-    '../elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html',
+    'change-list/gr-change-list-item/gr-change-list-item_test.html',
+    'change-list/gr-change-list/gr-change-list_test.html',
+    'change/gr-change-actions/gr-change-actions_test.html',
+    'change/gr-change-metadata/gr-change-metadata_test.html',
+    'change/gr-change-view/gr-change-view_test.html',
+    'change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html',
+    'change/gr-download-dialog/gr-download-dialog_test.html',
+    'change/gr-file-list/gr-file-list_test.html',
+    'change/gr-message/gr-message_test.html',
+    'change/gr-messages-list/gr-messages-list_test.html',
+    'change/gr-related-changes-list/gr-related-changes-list_test.html',
+    'change/gr-reply-dialog/gr-reply-dialog_test.html',
+    'change/gr-reviewer-list/gr-reviewer-list_test.html',
+    'core/gr-account-dropdown/gr-account-dropdown_test.html',
+    'core/gr-error-manager/gr-error-manager_test.html',
+    'core/gr-main-header/gr-main-header_test.html',
+    'core/gr-search-bar/gr-search-bar_test.html',
+    'diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html',
+    'diff/gr-diff-comment/gr-diff-comment_test.html',
+    'diff/gr-diff-cursor/gr-diff-cursor_test.html',
+    'diff/gr-diff-preferences/gr-diff-preferences_test.html',
+    'diff/gr-diff-view/gr-diff-view_test.html',
+    'diff/gr-diff/gr-diff-builder_test.html',
+    'diff/gr-diff/gr-diff-group_test.html',
+    'diff/gr-diff/gr-diff_test.html',
+    'diff/gr-patch-range-select/gr-patch-range-select_test.html',
+    'shared/gr-account-label/gr-account-label_test.html',
+    'shared/gr-account-link/gr-account-link_test.html',
+    'shared/gr-alert/gr-alert_test.html',
+    'shared/gr-avatar/gr-avatar_test.html',
+    'shared/gr-change-star/gr-change-star_test.html',
+    'shared/gr-confirm-dialog/gr-confirm-dialog_test.html',
+    'shared/gr-cursor-manager/gr-cursor-manager_test.html',
+    'shared/gr-date-formatter/gr-date-formatter_test.html',
+    'shared/gr-js-api-interface/gr-js-api-interface_test.html',
+    'shared/gr-linked-text/gr-linked-text_test.html',
+    'shared/gr-rest-api-interface/gr-rest-api-interface_test.html',
+    'shared/gr-storage/gr-storage_test.html',
   ].forEach(function(file) {
+    file = basePath + file;
     testFiles.push(file);
     testFiles.push(file + '?dom=shadow');
   });
diff --git a/tools/eclipse/project.py b/tools/eclipse/project.py
index 4b35f7c..46f5680 100755
--- a/tools/eclipse/project.py
+++ b/tools/eclipse/project.py
@@ -42,6 +42,8 @@
                 help='do not attach sources')
 opts.add_option('--plugins', help='create eclipse projects for plugins',
                 action='store_true')
+opts.add_option('--name', help='name of the generated project',
+                action='store', default='gerrit', dest='project_name')
 args, _ = opts.parse_args()
 
 def _query_classpath(targets):
@@ -223,7 +225,7 @@
     except CalledProcessError as err:
       exit(1)
 
-  gen_project()
+  gen_project(args.project_name)
   gen_classpath()
   gen_factorypath()
 
diff --git a/tools/maven/BUCK b/tools/maven/BUCK
index fcd77c0..322b5a2 100644
--- a/tools/maven/BUCK
+++ b/tools/maven/BUCK
@@ -31,8 +31,3 @@
   },
   war = {'gerrit-war': '//:release'},
 )
-
-python_binary(
-  name = 'mvn',
-  main = 'mvn.py',
-)
diff --git a/tools/maven/package.defs b/tools/maven/package.defs
index c3e5595..e43f59e 100644
--- a/tools/maven/package.defs
+++ b/tools/maven/package.defs
@@ -16,7 +16,7 @@
   "echo '#!/bin/sh -eu' > $OUT",
   'echo "# this script should run from the root of your workspace." >> $OUT',
   'echo "" >> $OUT',
-  "echo 'if [[ -n \"$${VERBOSE:-}\" ]]; then set -x ; fi' >> $OUT", 
+  "echo 'if [[ -n \"$${VERBOSE:-}\" ]]; then set -x ; fi' >> $OUT",
   'echo "" >> $OUT',
   'echo %s >> $OUT',
   'echo "" >> $OUT',
@@ -36,9 +36,11 @@
 
   build_cmd = ['buck', 'build']
 
-  mvn_cmd = ['$(exe //tools/maven:mvn)', '-v', version]
+  # This is not using python_binary() to avoid the baggage and bugs
+  # that PEX brings along.
+  mvn_cmd = ['python', 'tools/maven/mvn.py', '-v', version]
   api_cmd = mvn_cmd[:]
-  api_targets = [ '//tools/maven:mvn' ]
+  api_targets = []
   for type,d in [('jar', jar), ('java-source', src), ('javadoc', doc)]:
     for a,t in sorted(d.iteritems()):
       api_cmd.append('-s %s:%s:$(location %s)' % (a,type,t))
@@ -66,7 +68,7 @@
     )
 
   war_cmd = mvn_cmd[:]
-  war_targets = [ '//tools/maven:mvn' ]
+  war_targets = []
   for a,t in sorted(war.iteritems()):
     war_cmd.append('-s %s:war:$(location %s)' % (a,t))
     war_targets.append(t)