Merge "Document Vim setup for Git commit messages"
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 31df2c3..e814daf 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -1205,13 +1205,6 @@
 you need the <<capability_viewCaches,view caches capability>>.
 
 
-[[capability_generateHttpPassword]]
-=== Generate HTTP Password
-
-Allow the user to generate HTTP passwords for other users.  Typically this would
-be assigned to a non-interactive users group.
-
-
 [[capability_kill]]
 === Kill Task
 
diff --git a/Documentation/cmd-set-account.txt b/Documentation/cmd-set-account.txt
index 40f2378..f31f61b 100644
--- a/Documentation/cmd-set-account.txt
+++ b/Documentation/cmd-set-account.txt
@@ -26,12 +26,12 @@
 Caller must be a member of the privileged 'Administrators' group,
 or have been granted
 link:access-control.html#capability_modifyAccount[the 'Modify Account' global capability].
+For security reasons only the members of the privileged 'Administrators'
+group can add or delete SSH keys for a user.
 
 To set the HTTP password for the user account (option --http-password) or
 to clear the HTTP password (option --clear-http-password) caller must be
-a member of the privileged 'Administrators' group, or have been granted
-link:access-control.html#capability_generateHttpPassword[the 'Generate HTTP Password' global capability]
-in addition to 'Modify Account' global capability.
+a member of the privileged 'Administrators' group.
 
 == SCRIPTING
 This command is intended to be used in scripts.
diff --git a/Documentation/dev-buck.txt b/Documentation/dev-buck.txt
index 0bf44d0..3b11382 100644
--- a/Documentation/dev-buck.txt
+++ b/Documentation/dev-buck.txt
@@ -389,6 +389,21 @@
   /home/<user>/projects/jgit/org.eclipse.jgit/target/org.eclipse.jgit-3.3.0-SNAPSHOT.jar
 ----
 
+After `buck clean` and `buck build lib/jgit:jgit` the symbolic link that was
+created the first time is lost due to Buck's caching mechanism. This means that
+when a new version of the local artifact is deployed (by running `mvn package`
+in the JGit project in the example above), Buck is not aware of it, because it
+still has a stale version of it in its cache.
+
+To solve this problem and re-create the symbolic link, you don't need to wipe out
+the entire Buck cache. Just rebuilding the target with the `--no-cache` option
+does the job:
+
+----
+  buck clean
+  buck build --no-cache lib/jgit:jgit
+----
+
 == Building against artifacts from custom Maven repositories
 
 To build against custom Maven repositories, two modes of operations are
@@ -448,6 +463,25 @@
   EOF
 ----
 
+[[clean-cache]]
+=== Cleaning The Buck Cache
+
+The cache for the Gerrit Code Review project is located in
+`~/.gerritcodereview/buck-cache/cache`.
+
+The Buck cache should never need to be manually deleted. If you find yourself
+deleting the Buck cache regularly, then it is likely that there is something
+wrong with your environment or your workflow.
+
+If you really do need to clean the cache manually, then:
+
+----
+ rm -rf ~/.gerritcodereview/buck-cache/cache
+----
+
+Note that the root `buck-cache` folder should not be deleted as this is where
+downloaded artifacts are stored.
+
 [[buck-daemon]]
 === Using Buck daemon
 
@@ -473,7 +507,7 @@
 Prepend the variable to Buck invocation instead:
 
 ----
-  $ NO_BUCKD=1 buck build gerrit
+  NO_BUCKD=1 buck build gerrit
 ----
 
 [[watchman]]
@@ -537,13 +571,13 @@
 needs to be repeated, the unit test cache for that test must be removed first:
 
 ----
-  $ rm -rf buck-out/bin/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/.AddRemoveGroupMembersIT/
+  rm -rf buck-out/bin/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/.AddRemoveGroupMembersIT/
 ----
 
 After clearing the cache, the test can be run again:
 
 ----
-  $ buck test //gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group:AddRemoveGroupMembersIT
+  buck test //gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group:AddRemoveGroupMembersIT
   TESTING //gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group:AddRemoveGroupMembersIT
   PASS  14,9s  8 Passed   0 Failed   com.google.gerrit.acceptance.rest.group.AddRemoveGroupMembersIT
   TESTS PASSED
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index 9b64ddd..6dc8d1c 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -193,7 +193,7 @@
     might appear near the instance methods which they help (but may
     also appear at the top).
   * Getters and setters for the same instance field should usually
-    be near each other baring a good reason not to.
+    be near each other barring a good reason not to.
   * If you are using assisted injection, the factory for your class
     should be before the instance members.
   * Annotations should go before language keywords (final, private...) +
@@ -201,6 +201,9 @@
   * Imports should be mostly alphabetical (uppercase sorts before
     all lowercase, which means classes come before packages at the
     same level).
+  * Prefer to open multiple AutoCloseable resources in the same
+    try-with-resources block instead of nesting the try-with-resources
+    blocks and increasing the indentation level more than necessary.
 
 Wow that's a lot!  But don't worry, you'll get the habit and most
 of the code is organized this way already; so if you pay attention
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index a14aebc..3e76edc 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -1753,6 +1753,7 @@
 ----
 import com.google.gerrit.extensions.annotations.Listen;
 import com.google.gerrit.extensions.webui.PatchSetWebLink;;
+import com.google.gerrit.extensions.webui.WebLinkTarget;
 
 @Listen
 public class MyWeblinkPlugin implements PatchSetWebLink {
@@ -1762,23 +1763,11 @@
   private String imageUrl = "http://placehold.it/16x16.gif";
 
   @Override
-  public String getLinkName() {
-    return name;
-  }
-
-  @Override
-  public String getPatchSetUrl(String project, String commit) {
-    return String.format(placeHolderUrlProjectCommit, project, commit);
-  }
-
-  @Override
-  public String getImageUrl() {
-    return imageUrl;
-  }
-
-  @Override
-  public String getTarget() {
-    return "_blank";
+  public WebLinkInfo getPathSetWebLink(String projectName, String commit) {
+    return new WebLinkInfo(name,
+        imageUrl,
+        String.format(placeHolderUrlProjectCommit, project, commit),
+        WebLinkTarget.BLANK);
   }
 }
 ----
diff --git a/Documentation/install-j2ee.txt b/Documentation/install-j2ee.txt
index 4f438e5..f7252e0 100644
--- a/Documentation/install-j2ee.txt
+++ b/Documentation/install-j2ee.txt
@@ -109,15 +109,18 @@
 `system_config`) is as simple as touching the context config file:
 `'$JETTY_HOME'/contexts/gerrit.xml`
 
+[[tomcat]]
 == Tomcat 7.x
 
-[TIP]
-If reverse proxy is used in front of Tomcat then see the configuration
-instructions for encoding slashes link:config-reverseproxy.html. Otherwise
-Tomcat must be configured to encode slashes, by adding
--Dorg.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH=true to
-CATALINA_OPTS environment variable. Excerpt from the documentation
-https://tomcat.apache.org/tomcat-7.0-doc/config/systemprops.html:
+If a reverse proxy is used in front of Tomcat then see the
+link:config-reverseproxy.html[configuration instructions for encoding
+slashes]. Otherwise Tomcat must be configured to encode slashes, by adding
+`-Dorg.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH=true` to the
+`CATALINA_OPTS` environment variable.
+
+Excerpt from the
+link:https://tomcat.apache.org/tomcat-7.0-doc/config/systemprops.html[
+documentation]:
 
 ----
 Property org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH:
diff --git a/Documentation/prolog-cookbook.txt b/Documentation/prolog-cookbook.txt
index 5bff6bf..e069d85 100644
--- a/Documentation/prolog-cookbook.txt
+++ b/Documentation/prolog-cookbook.txt
@@ -264,7 +264,7 @@
 
 [[HowToWriteSubmitType]]
 == How to write submit type
-Writing custom submit type logic in Prolog is the similar top
+Writing custom submit type logic in Prolog is similar to
 xref:HowToWriteSubmitRules[writing submit rules]. The only difference is that
 one has to implement a `submit_type` predicate (instead of the `submit_rule`)
 and that the return result of the `submit_type` has to be an atom that
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index b1f6bcc..24da5e4 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -908,7 +908,7 @@
 
 .Request
 ----
-  GET /a/accounts/self/preferences HTTP/1.0
+  PUT /a/accounts/self/preferences HTTP/1.0
   Content-Type: application/json;charset=UTF-8
 
   {
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 3a2aebd..20648dc 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -930,6 +930,112 @@
   ]
 ----
 
+[[branch-options]]
+==== Branch Options
+
+Limit(n)::
+Limit the number of branches to be included in the results.
++
+.Request
+----
+  GET /projects/testproject/branches?n=1 HTTP/1.0
+----
++
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  [
+    {
+      "ref": "HEAD",
+      "revision": "master",
+      "can_delete": false
+    }
+  ]
+----
+
+Skip(s)::
+Skip the given number of branches from the beginning of the list.
++
+.Request
+----
+  GET /projects/testproject/branches?n=1&s=0 HTTP/1.0
+----
++
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  [
+    {
+      "ref": "HEAD",
+      "revision": "master",
+      "can_delete": false
+    }
+  ]
+----
+
+Substring(m)::
+Limit the results to those projects that match the specified substring.
++
+List all projects that match substring `test`:
++
+.Request
+----
+  GET /projects/testproject/branches?m=test HTTP/1.0
+----
++
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  [
+    {
+      "ref": "refs/heads/test1",
+      "revision": "9c9d08a438e55e52f33b608415e6dddd9b18550d",
+      "can_delete": true
+    }
+  ]
+----
+
+Regex(r)::
+Limit the results to those branches that match the specified regex.
+Boundary matchers '^' and '$' are implicit. For example: the regex 't*' will
+match any branches that start with 'test' and regex '*t' will match any
+branches that end with 'test'.
++
+List all branches that match regex `t.*1`:
++
+.Request
+----
+  GET /projects/testproject/branches?r=t.*1 HTTP/1.0
+----
++
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  [
+    {
+      "ref": "refs/heads/test1",
+      "revision": "9c9d08a438e55e52f33b608415e6dddd9b18550d",
+      "can_delete": true
+    }
+  ]
+----
+
 [[get-branch]]
 === Get Branch
 --
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt
index 9bc10f4..a9deba8 100644
--- a/Documentation/user-upload.txt
+++ b/Documentation/user-upload.txt
@@ -155,6 +155,7 @@
   git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/experimental%topic=driver/i42
 ====
 
+[[review_labels]]
 Review labels can be applied to the change by using the `label` (or `l`)
 option in the reference:
 
diff --git a/ReleaseNotes/ReleaseNotes-2.10.txt b/ReleaseNotes/ReleaseNotes-2.10.txt
new file mode 100644
index 0000000..78134694
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.10.txt
@@ -0,0 +1,636 @@
+Release notes for Gerrit 2.10
+=============================
+
+
+Gerrit 2.10 is now available:
+
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.10.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.10.war]
+
+Gerrit 2.10 includes the bug fixes done with
+link:ReleaseNotes-2.9.1.html[Gerrit 2.9.1].
+These bug fixes are *not* listed in these release notes.
+
+Important Notes
+---------------
+
+
+*WARNING:* This release contains schema changes.  To upgrade:
+----
+  java -jar gerrit.war init -d site_path
+  java -jar gerrit.war reindex --recheck-mergeable -d site_path
+----
+
+*WARNING:* Upgrading to 2.10.x requires the server be first upgraded to 2.1.7 (or
+a later 2.1.x version), and then to 2.10.x.  If you are upgrading from 2.2.x.x or
+later, you may ignore this warning and upgrade directly to 2.10.x.
+
+*WARNING:* The `auth.allowGoogleAccountUpgrade` setting is no longer supported.
+
+
+Release Highlights
+------------------
+
+
+* Support for externally loaded plugins.
++
+Plugins can be implemented in Scala or Groovy using the
+link:https://gerrit-review.googlesource.com/\#/admin/projects/plugins/scripting/groovy-provider[
+Groovy provider] and
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/scripting/scala-provider[
+Scala provider] plugins.
+
+* Customizable 'My' menu.
++
+Users can customize the contents of the 'My' menu in the top menu.  Administrators
+can configure the default contents of the menu.
+
+
+New Features
+------------
+
+
+Web UI
+~~~~~~
+
+
+Global
+^^^^^^
+
+* Add 'All-Users' project to store meta data for all users.
+
+* Administrators can customize the default contents of the 'My' menu.
+
+* Add 'My' > 'Groups' menu entry that shows the list of own groups.
+
+* Allow UiActions to perform redirects without JavaScript.
+
+
+Change Screen
+^^^^^^^^^^^^^
+
+
+* Remove 'send email' checkbox from reply box on change screen.
+
+* Do not linkify trailing dot or comma in messages.
++
+As linkifying trailing dots and trailing commas does more harm than
+good, we only treat dots and commas as being part of urls, if they are
+neither followed by whitespace nor occur at the end of a string.
+
+* Improve message when removing a reviewer.
+
+* Display avatar for author, committer, and change owner.
+
+* Remove message box when editing topic of change.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2573[Issue 2573]:
+Add option to quickly add current user as reviewer of a change.
+
+* Link project name to dashboard.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2667[Issue 2667]:
+Allow to customize Submit button label and tooltip.
+
+
+Side-by-Side Diff Screen
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+* Allow the user to select the syntax highlighter.
+
+* Add `Shift-a` keybinding to show/hide left side.
+
+* Allow to toggle empty pane for added and deleted files.
+
+* Add syntax highlighting of the commit message.
+
+
+Change List / Dashboards
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+* Remove age operator when drilling down from a dashboard to a query.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2646[Issue 2646]:
+Add option to show Change-ID in the change table.
+
+* Make the own user dashboard available under '/dashboard/self'.
+
+* Add 'R' key binding to refresh custom dashboards.
++
+Account dashboards, search results and the change screen refresh their content
+when 'R' is pressed.  The same binding is added for custom dashboards.
+
+
+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.
+
+* Disable content merge option if project's merge strategy is fast forward only.
+
+* Add branch actions to 'Projects > Branches' view.
+
+User Preferences
+^^^^^^^^^^^^^^^^
+
+
+* Users can customize the contents of the 'My' menu from the preferences
+screen.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=2628[Issue 2628]:
+Replace 'Display name in review category' preference with a list of options.
++
+Including new options 'Show Abbreviated Name' to display abbreviated reviewer
+names and 'Show Username' to show usernames in the change list.
+
+
+Secondary Index / Search
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+
+* Allow to search projects by prefix.
+
+* Add search fields for number of changed lines.
+
+* Add suggestions for 'is:pending' and 'status:pending'.
+
+* Add 'pending' as alias for 'open'.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=2545[Issue 2545]:
+Support `topic:""` to find changes with no topic.
+
+* Search more fields in the default search query.
++
+If a search is given with only a text, search over a variety of fields
+rather than just the project name.
+
+
+ssh
+~~~
+
+
+* Expose SSHD backend in
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/cmd-show-connections.html[
+`show connections`] SSH command.
+
+* Add support for JCE (Java Cryptography Extension) ciphers.
+
+REST API
+~~~~~~~~
+
+
+General
+^^^^^^^
+
+
+* Remove `kind` attribute from REST containers.
+
+* Support `AcceptsPost` on non top-level REST collections.
+
+* Accept `HEAD` in RestApiServlet.
+
+Accounts
+^^^^^^^^
+
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-accounts.html#get-user-preferences[
+Get user preferences].
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-accounts.html#set-user-preferences[
+Set user preferences].
+
+Changes
+^^^^^^^
+
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2338[Issue 2338]:
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-changes.html#create-change[
+Create change].
+
+* Add `other-branches` option on
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-changes.html#get-mergeable[
+Get mergeable] endpoint.
++
+If the `other-branches` option is specified, the mergeability will also be
+checked for all other branches.
+
+Config
+^^^^^^
+
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-config.html#list-tasks[
+List tasks].
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-config.html#get-task[
+Get task].
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-config.html#delete-task[
+Delete task].
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-config.html#list-caches[
+List caches].
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-config.html#flush-cache[
+Flush cache].
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-config.html#flush-several-caches[
+Flush several caches].
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-config.html#flush-all-caches[
+Flush all caches].
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-config.html#get-summary[
+Get server summary].
+
+Projects
+^^^^^^^^
+
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-projects.html#ban-commit[
+Ban commits].
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-projects.html#get-content[
+Get the content of a file from a certain commit].
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2604[Issue 2604]:
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-projects.html#get-commit[
+Get an arbitrary commit from a project].
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-projects.html#get-reflog[
+Get the reflog of a branch].
+
+* Add option 'S' to
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-projects.html#list-projects[
+list projects endpoint] to support query offset.
+
+
+Daemon
+~~~~~~
+
+
+* Add change subject to output of change URL on push.
+
+* Indicate trivial rebase and commit message update on push.
+
+* Add support for
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/user-upload.html#review_labels[
+adding review labels on changes] during git push.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2634[Issue 2634]:
+Add change kind to PatchSetCreatedEvent.
+
+
+Configuration
+~~~~~~~~~~~~~
+
+* Use
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/config-gerrit.html#core.useRecursiveMerge[
+recursive merge] by default.
+
+* Allow to configure the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/config-gerrit.html#download.archive[
+available download archive formats].
+
+* Add support for
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/database-setup.html#createdb_maxdb[
+SAP MaxDB].
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2041[Issue 2041]:
+Allow
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/config-labels.html#label_defaultValue[
+configuration of a default value for a label].
+
+* Allow projects to
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/config-project-config.html#mimetype-section[
+configure MIME types for files].
+
+* Allow to configure
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/config-gerrit.html#gc[
+periodic garbage collection of all projects].
+
+* Remove `auth.allowGoogleAccountUpgrade` setting.
++
+It's been more than 5 years since Gerrit ran on Google AppEngine.  It is assumed
+that everyone has upgraded their installations to a modern 2.x based server, and
+will not need to have this upgrade path enabled.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2618[Issue 2618]:
+Remove `label.Label-Name.abbreviation` setting.
++
+The setting was no longer used, so it has been removed.
+
+* New `httpd.registerMBeans` setting.
++
+The
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/config-gerrit.html#httpd.registerMBeans[
+`httpd.registerMBeans` setting] allows to enable (or disable) registration of
+Jetty MBeans for Java JMX.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2600[Issue 2600]:
+Add documentation of how to
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/install-j2ee.html#tomcat[
+configure Tomcat] to allow embedded slashes.
+
+
+Misc
+~~~~
+
+* Don't allow empty user name and passwords in InternalAuthBackend.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2596[Issue 2596]:
+Add change-owner parameter to gerrit hooks.
+
+
+Plugins
+~~~~~~~
+
+* Support for externally loaded plugins.
++
+Plugins can be implemented in Scala or Groovy using the
+link:https://gerrit-review.googlesource.com/\#/admin/projects/plugins/scripting/groovy-provider[
+Groovy provider] and
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/scripting/scala-provider[
+Scala provider] plugins.
+
+* Allow plugins to replace the WebSession implementation.
++
+Plugins can replace the existing implementation with the statement:
+`DynamicItem.bind(binder(), WebSession.class).to(...);`
+in a module designated as a `<Gerrit-HttpModule>` in the manifest.
++
+Just the Cache implementation used for web sessions can be changed
+by binding to a subclass of the now abstract `CacheBasedWebSession`
+which supplies the Cache in the superclass constructor.
++
+This is a step towards solving web session issues with multi-master.
++
+The link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/websession-flatfile[
+websession-flatfile plugin] replaces the built-in Gerrit WebSession implementation
+with one that uses a flat file based cache.
+
+* Allow http and ssh plugins to replace the Gerrit-provided DynamicItem.
+
+* New extension point to listen to usage data published events.
++
+Plugins implementing the `UsageDataPublishedListener` can listen to
+events published about usage data.
+
+* New extension point to link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/dev-plugins.html#pre-upload-hook[
+register JGit PreUploadHook].
++
+Plugins may register PreUploadHook instances in order to get
+notified when JGit is about to upload a pack. This may be useful
+for those plugins which would like to monitor usage in Git
+repositories.
+
+* New link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/config-validation.html#pre-upload-validation[
+pre-upload validation extension point].
++
+Plugins implementing the `UploadValidationListener` interface can
+perform additional validation checks before any upload operations
+(clone, fetch, pull). The validation is executed right before Gerrit
+begins to send a pack back to the git client.
+
+* New link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/dev-plugins.html#links-to-external-tools[
+external tool links extension points].
++
+Plugins can now contribute project links that will be displayed on the project
+list screen in the 'Repository Browser' column, and revision links that will be
+shown on the change screen.
+
+* Allow creation of persistent caches after server is started.
++
+This enables plugins to create own persistent caches when they are
+installed.
+
+* Make gerrit's HttpServletRequest and HttpServletResponse visible to http
+plugins.
+
+* New extensions in the Java Plugin API:
+
+** Query changes
+** Create/get/list projects
+** Get/set review status
+** Create change
+** Get account
+** Star/unstar changes
+** Check if revision needs rebase
+
+Bug Fixes
+---------
+
+General
+~~~~~~~
+
+* Use fixed rate instead of fixed delay for log file compression.
++
+Log file compression was scheduled using a fixed delay. This caused the start
+times to drift over time. Use a fixed rate instead so that the compression
+reoccurs at the same time every day.
+
+Web UI
+~~~~~~
+
+General
+^^^^^^^
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2595[Issue 2595]:
+Make gitweb redirect to login.
++
+Gitweb redirects to the login page if the user isn't currently logged.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2631[Issue 2631]:
+Re-arrange info at footer of Gerrit web UI pages.
++
+Move the Gerrit info link so that there are no links close to the next page link.
+
+Changes
+^^^^^^^
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=527[Issue 527]:
+Preserve line breaks in inline and review comments.
+
+* Always show 'No Score' as label help for zero votings.
+
+* Only reset the edited commit message text on cancel.
+
+* Only include message on quick approve if reply is open.
+
+* List reviewers with dummy approvals on closed changes.
+
+
+Side-By-Side Diff
+^^^^^^^^^^^^^^^^^
+
+* Fix line length column margin to appear at correct column.
+
+* Give B side full width when A side is hidden.
+
+* Fix scroll alignment when showing hidden A side.
+
+* Bind Shift-N to search-prev in vim mode.
+
+* Allow text selection in diff header.
+
+* Display diff header on mode changes and renames.
+
+* Document Shift-{Left,Right} in `?` help popup.
+
+* Show `[` and `]` shortcut keys in nav arrow tooltips.
+
+* Disable "Render = Slow" mode on files over 4000 lines.
+
+* Keep keyboard bindings alive after click in padding.
+
+* Jump to the first change on either side.
+
+* Expand margin between paragraphs in comments.
+
+* Include content on identical files with mode change.
+
+
+User Settings
+^^^^^^^^^^^^^
+
+* Avoid loading all SSH keys when adding a new one.
+
+
+Secondary Index / Search
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+
+* Omit corrupt changes from search results.
+
+* Allow illegal label names from default search predicate.
+
+REST
+~~~~
+
+General
+^^^^^^^
+
+* Fix REST API responses for 3xx and 4xx classes.
+
+
+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.
+
+* Allow forcing mergeability check on
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-changes.html#get-mergeable[
+Get mergeable].
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2622[Issue 2622]:
+Respect patch set visibility for messages.
++
+Messages retrieval didn't check for patch set visbility and thus messages for
+draft patch sets were returned back to the client.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2782[Issue 2782]:
+Add missing documentation of the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-changes.html#get-related-changes[
+Get Related Changes] endpoint.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2723[Issue 2723]:
+Clarify the response info in the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-changes.html#get-change-detail[
+Get Change Detail] endpoint.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2693[Issue 2693]:
+Clarify the response info in the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/rest-api-changes.html#list-comments[
+List Comments] endpoint.
+
+SSH
+~~~
+
+
+* Prevent double authentication for the same public key.
++
+This is a workaround for link:https://issues.apache.org/jira/browse/SSHD-300[
+SSHD-300].
+
+* Let `kill` SSH command only kill tasks that are visible to the caller.
+
+* Require 'Administrate Server' capability to see server summary output from
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.10/cmd-show-caches.html[
+`show-caches`] command.
+
+Daemon
+~~~~~~
+
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2284[Issue 2284]:
+More detailed error message when failing to upload new change.
++
+When the uploaded change cannot be created on the underlying Git repository, a
+more descriptive error message is displayed on both client and server side. This
+allows to troubleshoot internal errors (e.g. JGit lock failures or other causes)
+and help out in the resolution.
+
+* Enforce HTTP password checking on gitBasicAuth.
+
+* Fix missing commit messages on submodule direct pushes.
++
+The commit message in superproject was missing on submodule's
+directly pushed changes.
+
+
+Plugins
+~~~~~~~
+
+General
+^^^^^^^
+
+
+* Invoke `StartPluginListener` and `ReloadPluginListener` only after start/reload
+is fully done.
+
+* Set `Last-Modified` on cached Documentation resources.
+
+* Return HTTP 304 for not modified SmallResources.
+
+* Fix ChangeListener auto-registered implementations.
+
+Replication
+^^^^^^^^^^^
+
+
+* Move replication logs into a separate file.
+
+* Promote replication scheduled logs to info.
+
+* Show replication ID in the log and in show-queue command.
+
+
+Upgrades
+--------
+
+
+* Update Guava to 17.0
+
+* Update Guice to 4.0-beta5
+
+* Update GWT to 2.6.1
+
+* Update httpclient to 4.3.4
+
+* Update httpcore to 4.3.2
+
+* Update Jcraft SSH to 0.1.51
+
+* Update Jetty to 9.2
+
+* Update JGit to 3.4.0.201406110918-r
+
+* Update log4j to 1.2.17
+
+* Update Servlet API to 8.0.5
+
+* Update slf4j to 1.7.7
+
+* Update Velocity to 1.7
+
diff --git a/ReleaseNotes/ReleaseNotes-2.11.txt b/ReleaseNotes/ReleaseNotes-2.11.txt
index 23f3247..c12d913 100644
--- a/ReleaseNotes/ReleaseNotes-2.11.txt
+++ b/ReleaseNotes/ReleaseNotes-2.11.txt
@@ -61,7 +61,10 @@
 Other
 ~~~~~
 
-TODO
+The 'Generate HTTP Password' capability has been removed to close a
+security vulnerability.  Now only administrators are allowed to generate
+and delete other user's http passwords via the REST or SSH interface.
+We would encourage you to clean up your project.config setting after upgrading.
 
 Upgrades
 --------
diff --git a/ReleaseNotes/index.txt b/ReleaseNotes/index.txt
index 8e7cac6..47f453b 100644
--- a/ReleaseNotes/index.txt
+++ b/ReleaseNotes/index.txt
@@ -1,6 +1,11 @@
 Gerrit Code Review - Release Notes
 ==================================
 
+[[2_10]]
+Version 2.10.x
+--------------
+* link:ReleaseNotes-2.10.html[2.10]
+
 [[2_9]]
 Version 2.9.x
 -------------
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
index 2e28655..d14a5a8 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
@@ -114,6 +114,86 @@
         devCommit, false)), toBranchInfoList(r));
   }
 
+  @Test
+  public void listBranchesUsingPagination() throws Exception {
+    pushTo("refs/heads/master");
+    pushTo("refs/heads/someBranch1");
+    pushTo("refs/heads/someBranch2");
+    pushTo("refs/heads/someBranch3");
+
+    // using only limit
+    RestResponse r =
+        adminSession.get("/projects/" + project.get() + "/branches?n=4");
+    List<BranchInfo> result = toBranchInfoList(r);
+    assertEquals(4, result.size());
+    assertEquals("HEAD", result.get(0).ref);
+    assertEquals("refs/meta/config", result.get(1).ref);
+    assertEquals("refs/heads/master", result.get(2).ref);
+    assertEquals("refs/heads/someBranch1", result.get(3).ref);
+
+    // limit higher than total number of branches
+    r = adminSession.get("/projects/" + project.get() + "/branches?n=25");
+    result = toBranchInfoList(r);
+    assertEquals(6, result.size());
+    assertEquals("HEAD", result.get(0).ref);
+    assertEquals("refs/meta/config", result.get(1).ref);
+    assertEquals("refs/heads/master", result.get(2).ref);
+    assertEquals("refs/heads/someBranch1", result.get(3).ref);
+    assertEquals("refs/heads/someBranch2", result.get(4).ref);
+    assertEquals("refs/heads/someBranch3", result.get(5).ref);
+
+    // using skip only
+    r = adminSession.get("/projects/" + project.get() + "/branches?s=2");
+    result = toBranchInfoList(r);
+    assertEquals(4, result.size());
+    assertEquals("refs/heads/master", result.get(0).ref);
+    assertEquals("refs/heads/someBranch1", result.get(1).ref);
+    assertEquals("refs/heads/someBranch2", result.get(2).ref);
+    assertEquals("refs/heads/someBranch3", result.get(3).ref);
+
+    // skip more branches than the number of available branches
+    r = adminSession.get("/projects/" + project.get() + "/branches?s=7");
+    result = toBranchInfoList(r);
+    assertEquals(0, result.size());
+
+    // using skip and limit
+    r = adminSession.get("/projects/" + project.get() + "/branches?s=2&n=2");
+    result = toBranchInfoList(r);
+    assertEquals(2, result.size());
+    assertEquals("refs/heads/master", result.get(0).ref);
+    assertEquals("refs/heads/someBranch1", result.get(1).ref);
+  }
+
+  @Test
+  public void listBranchesUsingFilter() throws Exception {
+    pushTo("refs/heads/master");
+    pushTo("refs/heads/someBranch1");
+    pushTo("refs/heads/someBranch2");
+    pushTo("refs/heads/someBranch3");
+
+    //using substring
+    RestResponse r =
+        adminSession.get("/projects/" + project.get() + "/branches?m=some");
+    List<BranchInfo> result = toBranchInfoList(r);
+    assertEquals(3, result.size());
+    assertEquals("refs/heads/someBranch1", result.get(0).ref);
+    assertEquals("refs/heads/someBranch2", result.get(1).ref);
+    assertEquals("refs/heads/someBranch3", result.get(2).ref);
+
+    r = adminSession.get("/projects/" + project.get() + "/branches?m=Branch");
+    result = toBranchInfoList(r);
+    assertEquals(3, result.size());
+    assertEquals("refs/heads/someBranch1", result.get(0).ref);
+    assertEquals("refs/heads/someBranch2", result.get(1).ref);
+    assertEquals("refs/heads/someBranch3", result.get(2).ref);
+
+    //using regex
+    r = adminSession.get("/projects/" + project.get() + "/branches?r=.*ast.*r");
+    result = toBranchInfoList(r);
+    assertEquals(1, result.size());
+    assertEquals("refs/heads/master", result.get(0).ref);
+  }
+
   private RestResponse GET(String endpoint) throws IOException {
     return adminSession.get(endpoint);
   }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
index c6c2d50..e4b0381 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
@@ -61,9 +61,6 @@
   /** Can flush any cache except the active web_sessions cache. */
   public static final String FLUSH_CACHES = "flushCaches";
 
-  /** Can generate HTTP passwords for user other than self. */
-  public static final String GENERATE_HTTP_PASSWORD = "generateHttpPassword";
-
   /** Can terminate any task using the kill command. */
   public static final String KILL_TASK = "killTask";
 
@@ -112,7 +109,6 @@
     NAMES_ALL.add(CREATE_PROJECT);
     NAMES_ALL.add(EMAIL_REVIEWERS);
     NAMES_ALL.add(FLUSH_CACHES);
-    NAMES_ALL.add(GENERATE_HTTP_PASSWORD);
     NAMES_ALL.add(KILL_TASK);
     NAMES_ALL.add(MODIFY_ACCOUNT);
     NAMES_ALL.add(PRIORITY);
diff --git a/gerrit-extension-api/BUCK b/gerrit-extension-api/BUCK
index aad79d7..0c1b6a8 100644
--- a/gerrit-extension-api/BUCK
+++ b/gerrit-extension-api/BUCK
@@ -47,7 +47,7 @@
 java_doc(
   name = 'extension-api-javadoc',
   title = 'Gerrit Review Extension API Documentation',
-  pkg = 'com.google.gerrit.extensions',
+  pkgs = ['com.google.gerrit.extensions'],
   paths = ['src/main/java'],
   srcs = SRCS,
   deps = [
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/BranchWebLink.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/BranchWebLink.java
index bc7a1e5..65a45dc 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/BranchWebLink.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/BranchWebLink.java
@@ -15,16 +15,17 @@
 package com.google.gerrit.extensions.webui;
 
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.common.WebLinkInfo;
 
 @ExtensionPoint
-public interface BranchWebLink extends WebLink {
+public interface BranchWebLink {
 
   /**
    * URL to branch in external service.
    *
    * @param projectName Name of the project
    * @param branchName Name of the branch
-   * @return url to branch in external service.
+   * @return WebLinkInfo that links to branch in external service.
    */
-  String getBranchUrl(String projectName, String branchName);
+  WebLinkInfo getBranchWebLink(String projectName, String branchName);
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/FileWebLink.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/FileWebLink.java
index ee3c62f..1bfbd99 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/FileWebLink.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/FileWebLink.java
@@ -15,9 +15,10 @@
 package com.google.gerrit.extensions.webui;
 
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.common.WebLinkInfo;
 
 @ExtensionPoint
-public interface FileWebLink extends WebLink {
+public interface FileWebLink {
 
   /**
    * URL to file in external service.
@@ -25,7 +26,7 @@
    * @param projectName Name of the project
    * @param revision Name of the revision (e.g. branch or commit ID)
    * @param fileName Name of the file
-   * @return url to project in external service.
+   * @return WebLinkInfo that links to project in external service.
    */
-  String getFileUrl(String projectName, String revision, String fileName);
+  WebLinkInfo getFileWebLink(String projectName, String revision, String fileName);
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/PatchSetWebLink.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/PatchSetWebLink.java
index b6086f2..8aaedaf 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/PatchSetWebLink.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/PatchSetWebLink.java
@@ -14,16 +14,17 @@
 package com.google.gerrit.extensions.webui;
 
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.common.WebLinkInfo;
 
 @ExtensionPoint
-public interface PatchSetWebLink extends WebLink {
+public interface PatchSetWebLink {
 
   /**
    * URL to patch set in external service.
    *
    * @param projectName Name of the project
    * @param commit Commit of the patch set
-   * @return url to patch set in external service.
+   * @return WebLinkInfo that links to patch set in external service.
    */
-  String getPatchSetUrl(final String projectName, final String commit);
+  WebLinkInfo getPathSetWebLink(final String projectName, final String commit);
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/ProjectWebLink.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/ProjectWebLink.java
index 61e9982..204c51d 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/ProjectWebLink.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/ProjectWebLink.java
@@ -15,15 +15,16 @@
 package com.google.gerrit.extensions.webui;
 
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.common.WebLinkInfo;
 
 @ExtensionPoint
-public interface ProjectWebLink extends WebLink {
+public interface ProjectWebLink {
 
   /**
    * URL to project in external service.
    *
    * @param projectName Name of the project
-   * @return url to project in external service.
+   * @return WebLinkInfo that links to project in external service.
    */
-  String getProjectUrl(String projectName);
+  WebLinkInfo getProjectWeblink(String projectName);
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/WebLink.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/WebLink.java
deleted file mode 100644
index 4065c68..0000000
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/WebLink.java
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright (C) 2014 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-package com.google.gerrit.extensions.webui;
-
-public interface WebLink {
-
-  public static class Target {
-    public final static String BLANK = "_blank";
-    public final static String SELF = "_self";
-    public final static String PARENT = "_parent";
-    public final static String TOP = "_top";
-  }
-  /**
-   * The link-name displayed in UI.
-   *
-   * @return name of link or title of the link if image URL is available.
-   */
-  String getLinkName();
-
-  /**
-   * URL of image to be displayed
-   *
-   * @return URL to image for link or null for using a text-only link.
-   * Recommended image size is 16x16.
-   */
-  String getImageUrl();
-
-  /**
-   * Target window in which the link should be opened (e.g. "_blank", "_self".).
-   *
-   * @return link target, if null the link is opened in the current window
-   */
-  String getTarget();
-}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/WebLinkTarget.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/WebLinkTarget.java
new file mode 100644
index 0000000..fc27c25
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/WebLinkTarget.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.gerrit.extensions.webui;
+
+/**
+ * Class that holds target defaults for WebLink anchors.
+ */
+public class WebLinkTarget {
+  /**
+   * Opens the link in a new window or tab
+   */
+  public final static String BLANK = "_blank";
+  /**
+   * Opens the link in the frame it was clicked.
+   */
+  public final static String SELF = "_self";
+  /**
+   * Opens link in parent frame.
+   */
+  public final static String PARENT = "_parent";
+  /**
+   * Opens link in the full body of the window.
+   */
+  public final static String TOP = "_top";
+}
diff --git a/gerrit-gwtexpui/BUCK b/gerrit-gwtexpui/BUCK
index e06bf17..b266e126 100644
--- a/gerrit-gwtexpui/BUCK
+++ b/gerrit-gwtexpui/BUCK
@@ -8,10 +8,10 @@
     SRC + 'clippy/client/clippy.css',
     SRC + 'clippy/client/clippy.swf',
   ],
+  provided_deps = ['//lib/gwt:user'],
   deps = [
     ':SafeHtml',
     ':UserAgent',
-    '//lib/gwt:user',
     '//lib:LICENSE-clippy',
   ],
   visibility = ['PUBLIC'],
@@ -21,7 +21,7 @@
   name = 'CSS',
   srcs = glob([SRC + 'css/rebind/*.java']),
   resources = [SRC + 'css/CSS.gwt.xml'],
-  deps = ['//lib/gwt:dev'],
+  provided_deps = ['//lib/gwt:dev'],
   visibility = ['PUBLIC'],
 )
 
@@ -33,10 +33,10 @@
     SRC + 'globalkey/client/KeyConstants.properties',
     SRC + 'globalkey/client/key.css',
   ],
+  provided_deps = ['//lib/gwt:user'],
   deps = [
     ':SafeHtml',
     ':UserAgent',
-    '//lib/gwt:user',
   ],
   visibility = ['PUBLIC'],
 )
@@ -53,7 +53,7 @@
   srcs = glob([SRC + 'progress/client/*.java']),
   gwt_xml = SRC + 'progress/Progress.gwt.xml',
   resources = [SRC + 'progress/client/progress.css'],
-  deps = ['//lib/gwt:user'],
+  provided_deps = ['//lib/gwt:user'],
   visibility = ['PUBLIC'],
 )
 
@@ -62,7 +62,7 @@
   srcs = glob([SRC + 'safehtml/client/*.java']),
   gwt_xml = SRC + 'safehtml/SafeHtml.gwt.xml',
   resources = [SRC + 'safehtml/client/safehtml.css'],
-  deps = ['//lib/gwt:user'],
+  provided_deps = ['//lib/gwt:user'],
   visibility = ['PUBLIC'],
 )
 
@@ -84,7 +84,7 @@
   name = 'UserAgent',
   srcs = glob([SRC + 'user/client/*.java']),
   gwt_xml = SRC + 'user/User.gwt.xml',
-  deps = ['//lib/gwt:user'],
+  provided_deps = ['//lib/gwt:user'],
   visibility = ['PUBLIC'],
 )
 
@@ -94,3 +94,17 @@
   provided_deps = ['//lib:servlet-api-3_1'],
   visibility = ['PUBLIC'],
 )
+
+java_library(
+  name = 'client-src-lib',
+  srcs = [],
+  resources = glob(
+    [SRC + n for n in [
+      'clippy/**/*',
+      'globalkey/**/*',
+      'safehtml/**/*',
+      'user/**/*',
+    ]]
+  ),
+  visibility = ['PUBLIC'],
+)
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/WebLinkInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/WebLinkInfo.java
index 3ed91be..45731f7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/WebLinkInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/WebLinkInfo.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.client;
 
 import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.user.client.ui.Anchor;
+import com.google.gwt.user.client.ui.Image;
 
 public class WebLinkInfo extends JavaScriptObject {
 
@@ -25,4 +27,22 @@
 
   protected WebLinkInfo() {
   }
+
+  public final Anchor toAnchor() {
+    Anchor a = new Anchor();
+    a.setHref(url());
+    if (target() != null && !target().isEmpty()) {
+      a.setTarget(target());
+    }
+    if (imageUrl() != null && !imageUrl().isEmpty()) {
+      Image img = new Image();
+      img.setAltText(name());
+      img.setUrl(imageUrl());
+      img.setTitle(name());
+      a.getElement().appendChild(img.getElement());
+    } else {
+      a.setText("(" + name() + ")");
+    }
+    return a;
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
index 9a6dae4..ae81410 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
@@ -396,22 +396,8 @@
             c.toBranch(new Branch.NameKey(getProjectKey(), k.ref()))));
       }
       if (k.web_links() != null) {
-        for (WebLinkInfo weblink : Natives.asList(k.web_links())) {
-          Anchor a = new Anchor();
-          a.setHref(weblink.url());
-          if (weblink.target() != null && !weblink.target().isEmpty()) {
-            a.setTarget(weblink.target());
-          }
-          if (weblink.imageUrl() != null && !weblink.imageUrl().isEmpty()) {
-            Image img = new Image();
-            img.setAltText(weblink.name());
-            img.setUrl(weblink.imageUrl());
-            img.setTitle(weblink.name());
-            a.getElement().appendChild(img.getElement());
-          } else {
-            a.setText("(" + weblink.name() + ")");
-          }
-          actionsPanel.add(a);
+        for (WebLinkInfo webLink : Natives.asList(k.web_links())) {
+          actionsPanel.add(webLink.toAnchor());
         }
       }
       if (k.actions() != null) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
index af01ad3..3d1b96061 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
@@ -238,21 +238,7 @@
           }
 
           for (WebLinkInfo weblink : webLinks) {
-            Anchor a = new Anchor();
-            a.setHref(weblink.url());
-            if (weblink.target() != null && !weblink.target().isEmpty()) {
-              a.setTarget(weblink.target());
-            }
-            if (weblink.imageUrl() != null && !weblink.imageUrl().isEmpty()) {
-              Image img = new Image();
-              img.setAltText(weblink.name());
-              img.setUrl(weblink.imageUrl());
-              img.setTitle(weblink.name());
-              a.getElement().appendChild(img.getElement());
-            } else {
-              a.setText("(" + weblink.name() + ")");
-            }
-            p.add(a);
+            p.add(weblink.toAnchor());
           }
         }
       }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.java
index 20eb9b0..9bdf49a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.java
@@ -433,7 +433,7 @@
         reviewMode.setVisible(!editMode.isVisible());
         editFileAction = new EditFileAction(
             new PatchSet.Id(changeId, edit == null ? rev._number() : 0),
-            "", "", style.replyBox(), editMessage, reply);
+            "", "", style, editMessage, reply);
       } else {
         editMode.setVisible(false);
         addFile.setVisible(false);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java
index b32a31b..1705675 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java
@@ -126,35 +126,22 @@
       RevisionInfo revInfo) {
     GitwebLink gw = Gerrit.getGitwebLink();
     if (gw != null && gw.canLink(revInfo)) {
-      addWebLink(gw.toRevision(change.project(), revision),
-          gw.getLinkName(), null, null);
+      toAnchor(gw.toRevision(change.project(), revision),
+          gw.getLinkName());
     }
 
     JsArray<WebLinkInfo> links = revInfo.web_links();
     if (links != null) {
       for (WebLinkInfo link : Natives.asList(links)) {
-        addWebLink(link.url(), parenthesize(link.name()), link.imageUrl(),
-            link.target());
+        webLinkPanel.add(link.toAnchor());
       }
     }
   }
 
-  private void addWebLink(String href, String name, String imageUrl,
-      String target) {
+  private void toAnchor(String href, String name) {
     Anchor a = new Anchor();
     a.setHref(href);
-    if (target != null && !target.isEmpty()) {
-      a.setTarget(target);
-    }
-    if (imageUrl != null && !imageUrl.isEmpty()) {
-      Image img = new Image();
-      img.setAltText(name);
-      img.setUrl(imageUrl);
-      img.setTitle(name);
-      a.getElement().appendChild(img.getElement());
-    } else {
-      a.setText(name);
-    }
+    a.setText(name);
     webLinkPanel.add(a);
   }
 
@@ -198,14 +185,6 @@
     date.setInnerText(FormatUtil.mediumFormat(person.date()));
   }
 
-  private static String parenthesize(String str) {
-    return new StringBuilder()
-        .append("(")
-        .append(str)
-        .append(")")
-        .toString();
-  }
-
   private static String renderName(GitPerson person) {
     return person.name() + " <" + person.email() + ">";
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditFileAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditFileAction.java
index 8d31478..951c84d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditFileAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditFileAction.java
@@ -22,22 +22,22 @@
 import com.google.gwtexpui.globalkey.client.GlobalKey;
 import com.google.gwtexpui.user.client.PluginSafePopupPanel;
 
-public class EditFileAction {
+class EditFileAction {
  private final PatchSet.Id id;
  private final String content;
  private final String file;
- private final String style;
+ private final ChangeScreen2.Style style;
  private final Widget editMessageButton;
  private final Widget relativeTo;
 
  private EditFileBox editBox;
  private PopupPanel popup;
 
- public EditFileAction(
+ EditFileAction(
      PatchSet.Id id,
      String content,
      String file,
-     String style,
+     ChangeScreen2.Style style,
      Widget editButton,
      Widget relativeTo) {
    this.id = id;
@@ -62,10 +62,8 @@
    }
 
    final PluginSafePopupPanel p = new PluginSafePopupPanel(true);
-   p.setStyleName(style);
-   if (editMessageButton != null) {
-     p.addAutoHidePartner(editMessageButton.getElement());
-   }
+   p.setStyleName(style.replyBox());
+   p.addAutoHidePartner(editMessageButton.getElement());
    p.addCloseHandler(new CloseHandler<PopupPanel>() {
      @Override
      public void onClose(CloseEvent<PopupPanel> event) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
index 8a61b15..d57a834 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
@@ -332,7 +332,7 @@
             @Override
             public void onSuccess(String result) {
               EditFileAction edit = new EditFileAction(
-                  id, result, path, style.replyBox(), editButton, replyButton);
+                  id, result, path, style, editButton, replyButton);
               edit.onEdit();
             }
           });
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox2.java
index 40ef953..2650a1e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox2.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox2.java
@@ -52,7 +52,6 @@
 
   interface BoxStyle extends CssResource {
     String selected();
-    String replyBox();
   }
 
   @UiField Image icon;
@@ -124,22 +123,8 @@
     }
     List<WebLinkInfo> webLinks = Natives.asList(meta.web_links());
     if (webLinks != null) {
-      for (WebLinkInfo weblink : webLinks) {
-        Anchor a = new Anchor();
-        a.setHref(weblink.url());
-        if (weblink.target() != null && !weblink.target().isEmpty()) {
-          a.setTarget(weblink.target());
-        }
-        if (weblink.imageUrl() != null && !weblink.imageUrl().isEmpty()) {
-          Image img = new Image();
-          img.setAltText(weblink.name());
-          img.setUrl(weblink.imageUrl());
-          img.setTitle(weblink.name());
-          a.getElement().appendChild(img.getElement());
-        } else {
-          a.setText("(" + weblink.name() + ")");
-        }
-        linkPanel.add(a);
+      for (WebLinkInfo webLink : webLinks) {
+        linkPanel.add(webLink.toAnchor());
       }
     }
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox2.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox2.ui.xml
index eba362a..ba7a0a7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox2.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox2.ui.xml
@@ -56,10 +56,6 @@
     .iconCell {
       width: 16px;
     }
-    .replyBox {
-      background-color: trimColor;
-      z-index: 10;
-    }
   </ui:style>
   <g:HTMLPanel>
     <table class='{style.table}'>
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestUtil.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestUtil.java
index c47ca30..04aa44a 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestUtil.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestUtil.java
@@ -18,6 +18,18 @@
 
 /** Utilities for manipulating HTTP request objects. */
 public class RequestUtil {
+  /** HTTP request attribute for storing the Throwable that caused an error condition. */
+  private static final String ATTRIBUTE_ERROR_TRACE =
+      RequestUtil.class.getName() + "/ErrorTraceThrowable";
+
+  public static void setErrorTraceAttribute(HttpServletRequest req, Throwable t) {
+    req.setAttribute(ATTRIBUTE_ERROR_TRACE, t);
+  }
+
+  public static Throwable getErrorTraceAttribute(HttpServletRequest req) {
+    return (Throwable) req.getAttribute(ATTRIBUTE_ERROR_TRACE);
+  }
+
   /**
    * @return the same value as {@link HttpServletRequest#getPathInfo()}, but
    *     without decoding URL-encoded characters.
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
index 3418354..614184a 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.httpd;
 
+import static com.google.gerrit.httpd.restapi.RestApiServlet.replyError;
 import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
 import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
 
 import com.google.gerrit.extensions.registration.DynamicItem;
-import com.google.gerrit.httpd.restapi.RestApiServlet;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountResolver;
@@ -77,17 +77,19 @@
     String runas = req.getHeader(RUN_AS);
     if (runas != null) {
       if (!enabled) {
-        RestApiServlet.replyError(req, res,
+        replyError(req, res,
             SC_FORBIDDEN,
-            RUN_AS + " disabled by auth.enableRunAs = false");
+            RUN_AS + " disabled by auth.enableRunAs = false",
+            null);
         return;
       }
 
       CurrentUser self = session.get().getCurrentUser();
       if (!self.getCapabilities().canRunAs()) {
-        RestApiServlet.replyError(req, res,
+        replyError(req, res,
             SC_FORBIDDEN,
-            "not permitted to use " + RUN_AS);
+            "not permitted to use " + RUN_AS,
+            null);
         return;
       }
 
@@ -96,15 +98,17 @@
         target = accountResolver.find(runas);
       } catch (OrmException e) {
         log.warn("cannot resolve account for " + RUN_AS, e);
-        RestApiServlet.replyError(req, res,
+        replyError(req, res,
             SC_INTERNAL_SERVER_ERROR,
-            "cannot resolve " + RUN_AS);
+            "cannot resolve " + RUN_AS,
+            e);
         return;
       }
       if (target == null) {
-        RestApiServlet.replyError(req, res,
+        replyError(req, res,
             SC_FORBIDDEN,
-            "no account matches " + RUN_AS);
+            "no account matches " + RUN_AS,
+            null);
         return;
       }
       session.get().setUserAccountId(target.getId());
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
index 9183d5c..46843fc 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
@@ -68,7 +68,7 @@
       clp.parseOptionMap(in);
     } catch (CmdLineException e) {
       if (!clp.wasHelpRequestedByOption()) {
-        replyError(req, res, SC_BAD_REQUEST, e.getMessage());
+        replyError(req, res, SC_BAD_REQUEST, e.getMessage(), e);
         return false;
       }
     }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 979990a..4fb6e24 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
@@ -345,27 +345,27 @@
         }
       }
     } catch (AuthException e) {
-      replyError(req, res, status = SC_FORBIDDEN, e.getMessage(), e.caching());
+      replyError(req, res, status = SC_FORBIDDEN, e.getMessage(), e.caching(), e);
     } catch (BadRequestException e) {
-      replyError(req, res, status = SC_BAD_REQUEST, e.getMessage(), e.caching());
+      replyError(req, res, status = SC_BAD_REQUEST, e.getMessage(), e.caching(), e);
     } catch (MethodNotAllowedException e) {
-      replyError(req, res, status = SC_METHOD_NOT_ALLOWED, "Method not allowed", e.caching());
+      replyError(req, res, status = SC_METHOD_NOT_ALLOWED, "Method not allowed", e.caching(), e);
     } catch (ResourceConflictException e) {
-      replyError(req, res, status = SC_CONFLICT, e.getMessage(), e.caching());
+      replyError(req, res, status = SC_CONFLICT, e.getMessage(), e.caching(), e);
     } catch (PreconditionFailedException e) {
       replyError(req, res, status = SC_PRECONDITION_FAILED,
-          MoreObjects.firstNonNull(e.getMessage(), "Precondition failed"), e.caching());
+          MoreObjects.firstNonNull(e.getMessage(), "Precondition failed"), e.caching(), e);
     } catch (ResourceNotFoundException e) {
-      replyError(req, res, status = SC_NOT_FOUND, "Not found", e.caching());
+      replyError(req, res, status = SC_NOT_FOUND, "Not found", e.caching(), e);
     } catch (UnprocessableEntityException e) {
       replyError(req, res, status = 422,
-          MoreObjects.firstNonNull(e.getMessage(), "Unprocessable Entity"), e.caching());
+          MoreObjects.firstNonNull(e.getMessage(), "Unprocessable Entity"), e.caching(), e);
     } catch (AmbiguousViewException e) {
-      replyError(req, res, status = SC_NOT_FOUND, e.getMessage());
+      replyError(req, res, status = SC_NOT_FOUND, e.getMessage(), e);
     } catch (MalformedJsonException e) {
-      replyError(req, res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request");
+      replyError(req, res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request", e);
     } catch (JsonParseException e) {
-      replyError(req, res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request");
+      replyError(req, res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request", e);
     } catch (Exception e) {
       status = SC_INTERNAL_SERVER_ERROR;
       handleException(e, req, res);
@@ -928,21 +928,24 @@
 
     if (!res.isCommitted()) {
       res.reset();
-      replyError(req, res, SC_INTERNAL_SERVER_ERROR, "Internal server error");
+      replyError(req, res, SC_INTERNAL_SERVER_ERROR, "Internal server error", err);
     }
   }
 
-  public static void replyError(HttpServletRequest req,
-      HttpServletResponse res, int statusCode, String msg) throws IOException {
-    replyError(req, res, statusCode, msg, CacheControl.NONE);
+  public static void replyError(HttpServletRequest req, HttpServletResponse res,
+      int statusCode, String msg, @Nullable Throwable err) throws IOException {
+    replyError(req, res, statusCode, msg, CacheControl.NONE, err);
   }
 
   public static void replyError(HttpServletRequest req,
       HttpServletResponse res, int statusCode, String msg,
-      CacheControl c) throws IOException {
+      CacheControl c, @Nullable Throwable err) throws IOException {
     res.setStatus(statusCode);
     configureCaching(req, res, null, c);
     replyText(req, res, msg);
+    if (err != null) {
+      RequestUtil.setErrorTraceAttribute(req, err);
+    }
   }
 
   static void replyText(@Nullable HttpServletRequest req,
diff --git a/gerrit-plugin-api/BUCK b/gerrit-plugin-api/BUCK
index 7147b1c..80ab9a3 100644
--- a/gerrit-plugin-api/BUCK
+++ b/gerrit-plugin-api/BUCK
@@ -56,7 +56,7 @@
 java_doc(
   name = 'plugin-api-javadoc',
   title = 'Gerrit Review Plugin API Documentation',
-  pkg = 'com.google.gerrit',
+  pkgs = ['com.google.gerrit'],
   paths = [n for n in SRCS],
   srcs = glob([n + '**/*.java' for n in SRCS]),
   deps = [
diff --git a/gerrit-plugin-gwtui/BUCK b/gerrit-plugin-gwtui/BUCK
index 419176d..07e475b 100644
--- a/gerrit-plugin-gwtui/BUCK
+++ b/gerrit-plugin-gwtui/BUCK
@@ -1,4 +1,5 @@
 COMMON = ['gerrit-gwtui-common/src/main/java/']
+GWTEXPUI = ['gerrit-gwtexpui/src/main/java/']
 SRC = 'src/main/java/com/google/gerrit/'
 SRCS = glob([SRC + '**/*.java'])
 
@@ -26,7 +27,13 @@
   name = 'gwtui-api-lib2',
   srcs = SRCS,
   resources = glob(['src/main/**/*']),
-  exported_deps = ['//gerrit-gwtui-common:client-lib2'],
+  exported_deps = [
+    '//gerrit-gwtexpui:Clippy',
+    '//gerrit-gwtexpui:GlobalKey',
+    '//gerrit-gwtexpui:SafeHtml',
+    '//gerrit-gwtexpui:UserAgent',
+    '//gerrit-gwtui-common:client-lib2',
+  ],
   provided_deps = DEPS,
   visibility = ['PUBLIC'],
 )
@@ -35,6 +42,7 @@
   name = 'gwtui-api-src',
   deps = [
     ':gwtui-api-src-lib',
+    '//gerrit-gwtexpui:client-src-lib',
     '//gerrit-gwtui-common:client-src-lib',
   ],
   visibility = ['PUBLIC'],
@@ -50,9 +58,16 @@
 java_doc(
   name = 'gwtui-api-javadoc',
   title = 'Gerrit Review GWT Extension API Documentation',
-  pkg = 'com.google.gerrit',
-  paths = ['src/main/java'] + COMMON,
+  pkgs = [
+    'com.google.gerrit',
+    'com.google.gwtexpui.clippy',
+    'com.google.gwtexpui.globalkey',
+    'com.google.gwtexpui.safehtml',
+    'com.google.gwtexpui.user',
+  ],
+  paths = COMMON + GWTEXPUI,
   srcs = SRCS,
   deps = DEPS + ['//gerrit-gwtui-common:client-lib2'],
   visibility = ['PUBLIC'],
+  do_it_wrong = True,
 )
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/WebLinks.java b/gerrit-server/src/main/java/com/google/gerrit/server/WebLinks.java
index 67cb45e..ddd59ec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/WebLinks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/WebLinks.java
@@ -50,10 +50,7 @@
   public List<WebLinkInfo> getPatchSetLinks(String project, String commit) {
     List<WebLinkInfo> links = new ArrayList<>(4);
     for (PatchSetWebLink webLink : patchSetLinks) {
-      links.add(new WebLinkInfo(webLink.getLinkName(),
-          webLink.getImageUrl(),
-          webLink.getPatchSetUrl(project, commit),
-          webLink.getTarget()));
+      links.add(webLink.getPathSetWebLink(project, commit));
     }
     return links;
   }
@@ -62,13 +59,9 @@
       String file) {
     List<WebLinkInfo> links = new ArrayList<>(4);
     for (FileWebLink webLink : fileLinks) {
-      String name = webLink.getLinkName();
-      String url = webLink.getFileUrl(project, revision, file);
-      if (!Strings.isNullOrEmpty(name) && !Strings.isNullOrEmpty(url)) {
-        links.add(new WebLinkInfo(name,
-            webLink.getImageUrl(),
-            url,
-            webLink.getTarget()));
+      WebLinkInfo info = webLink.getFileWebLink(project, revision, file);
+      if (!Strings.isNullOrEmpty(info.name) && !Strings.isNullOrEmpty(info.url)) {
+        links.add(info);
       }
     }
     return links;
@@ -77,10 +70,7 @@
   public Iterable<WebLinkInfo> getProjectLinks(String project) {
     List<WebLinkInfo> links = Lists.newArrayList();
     for (ProjectWebLink webLink : projectLinks) {
-      links.add(new WebLinkInfo(webLink.getLinkName(),
-          webLink.getImageUrl(),
-          webLink.getProjectUrl(project),
-          webLink.getTarget()));
+      links.add(webLink.getProjectWeblink(project));
     }
     return links;
   }
@@ -88,10 +78,7 @@
   public Iterable<WebLinkInfo> getBranchLinks(String project, String branch) {
     List<WebLinkInfo> links = Lists.newArrayList();
     for (BranchWebLink webLink : branchLinks) {
-      links.add(new WebLinkInfo(webLink.getLinkName(),
-          webLink.getImageUrl(),
-          webLink.getBranchUrl(project, branch),
-          webLink.getTarget()));
+      links.add(webLink.getBranchWebLink(project, branch));
     }
     return links;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java
index 58c674c..3c21d17 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java
@@ -63,7 +63,7 @@
   public Response<SshKeyInfo> apply(AccountResource rsrc, Input input)
       throws AuthException, BadRequestException, OrmException, IOException {
     if (self.get() != rsrc.getUser()
-        && !self.get().getCapabilities().canModifyAccount()) {
+        && !self.get().getCapabilities().canAdministrateServer()) {
       throw new AuthException("not allowed to add SSH keys");
     }
     return apply(rsrc.getUser(), input);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
index 631256a..2721057 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
@@ -169,12 +169,6 @@
         || canAdministrateServer();
   }
 
-  /** @return true if the user can generate HTTP passwords for users other than self. */
-  public boolean canGenerateHttpPassword() {
-    return canPerform(GlobalCapability.GENERATE_HTTP_PASSWORD)
-        || canAdministrateServer();
-  }
-
   /** @return true if the user can impersonate another user. */
   public boolean canRunAs() {
     return canPerform(GlobalCapability.RUN_AS);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteSshKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteSshKey.java
index 7df1848..9066858 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteSshKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteSshKey.java
@@ -14,9 +14,11 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.DeleteSshKey.Input;
 import com.google.gerrit.server.ssh.SshKeyCache;
 import com.google.gwtorm.server.OrmException;
@@ -32,18 +34,26 @@
   public static class Input {
   }
 
+  private final Provider<CurrentUser> self;
   private final Provider<ReviewDb> dbProvider;
   private final SshKeyCache sshKeyCache;
 
   @Inject
-  DeleteSshKey(Provider<ReviewDb> dbProvider, SshKeyCache sshKeyCache) {
+  DeleteSshKey(Provider<ReviewDb> dbProvider,
+      Provider<CurrentUser> self,
+      SshKeyCache sshKeyCache) {
+    this.self = self;
     this.dbProvider = dbProvider;
     this.sshKeyCache = sshKeyCache;
   }
 
   @Override
   public Response<?> apply(AccountResource.SshKey rsrc, Input input)
-      throws OrmException {
+      throws AuthException, OrmException {
+    if (self.get() != rsrc.getUser()
+        && !self.get().getCapabilities().canAdministrateServer()) {
+      throw new AuthException("not allowed to delete SSH keys");
+    }
     dbProvider.get().accountSshKeys()
         .deleteKeys(Collections.singleton(rsrc.getSshKey().getKey()));
     sshKeyCache.evict(rsrc.getUser().getUserName());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
index 4ac65ec..43b76e2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
@@ -20,7 +20,6 @@
 import static com.google.gerrit.common.data.GlobalCapability.CREATE_PROJECT;
 import static com.google.gerrit.common.data.GlobalCapability.EMAIL_REVIEWERS;
 import static com.google.gerrit.common.data.GlobalCapability.FLUSH_CACHES;
-import static com.google.gerrit.common.data.GlobalCapability.GENERATE_HTTP_PASSWORD;
 import static com.google.gerrit.common.data.GlobalCapability.KILL_TASK;
 import static com.google.gerrit.common.data.GlobalCapability.MODIFY_ACCOUNT;
 import static com.google.gerrit.common.data.GlobalCapability.PRIORITY;
@@ -115,7 +114,6 @@
     have.put(CREATE_PROJECT, cc.canCreateProject());
     have.put(EMAIL_REVIEWERS, cc.canEmailReviewers());
     have.put(FLUSH_CACHES, cc.canFlushCaches());
-    have.put(GENERATE_HTTP_PASSWORD, cc.canGenerateHttpPassword());
     have.put(KILL_TASK, cc.canKillTask());
     have.put(MODIFY_ACCOUNT, cc.canModifyAccount());
     have.put(RUN_GC, cc.canRunGC());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetHttpPassword.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetHttpPassword.java
index e080015..c49ab98 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetHttpPassword.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetHttpPassword.java
@@ -36,7 +36,7 @@
   public String apply(AccountResource rsrc) throws AuthException,
       ResourceNotFoundException {
     if (self.get() != rsrc.getUser()
-        && !self.get().getCapabilities().canGenerateHttpPassword()) {
+        && !self.get().getCapabilities().canAdministrateServer()) {
       throw new AuthException("not allowed to get http password");
     }
     AccountState s = rsrc.getUser().state();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java
index 93b35c6..c6b6282 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java
@@ -79,19 +79,19 @@
     String newPassword;
     if (input.generate) {
       if (self.get() != rsrc.getUser()
-          && !self.get().getCapabilities().canGenerateHttpPassword()) {
+          && !self.get().getCapabilities().canAdministrateServer()) {
         throw new AuthException("not allowed to generate HTTP password");
       }
       newPassword = generate();
 
     } else if (input.httpPassword == null) {
       if (self.get() != rsrc.getUser()
-          && !self.get().getCapabilities().canGenerateHttpPassword()) {
+          && !self.get().getCapabilities().canAdministrateServer()) {
         throw new AuthException("not allowed to clear HTTP password");
       }
       newPassword = null;
     } else {
-      if (!self.get().getCapabilities().canGenerateHttpPassword()) {
+      if (!self.get().getCapabilities().canAdministrateServer()) {
         throw new AuthException("not allowed to set HTTP password directly, "
             + "requires the Generate HTTP Password permission");
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
index 8375256..1a2c6bd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
@@ -168,9 +168,11 @@
         result.metaB.name = ps.getNewName();
         setContentType(result.metaB, state, ps.getFileModeB(), ps.getMimeTypeB());
         result.metaB.lines = ps.getB().size();
-        result.metaB.webLinks = getFileWebLinks(state.getProject(),
-            resource.getRevision().getPatchSet().getRefName(),
-            result.metaB.name);
+        String rev = resource.getRevision().getEdit().isPresent()
+            ? resource.getRevision().getEdit().get().getRefName()
+            : resource.getRevision().getPatchSet().getRefName();
+        result.metaB.webLinks =
+            getFileWebLinks(state.getProject(), rev, result.metaB.name);
       }
 
       if (intraline) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityChecker.java
index 481e535..270f718 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityChecker.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityChecker.java
@@ -35,7 +35,6 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.MergeabilityChecksExecutor.Priority;
-import com.google.gerrit.server.change.Mergeable.MergeableInfo;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.git.WorkQueue.Executor;
@@ -150,20 +149,18 @@
               List<ListenableFuture<?>> result =
                   Lists.newArrayListWithCapacity(changes.size());
               for (final Change c : changes) {
-                ListenableFuture<Boolean> b =
-                    executor.submit(new Task(c, force));
+                // Don't try to guess whether Mergeable will reindex; just turn
+                // off reindexing in that code path and do it explicitly below.
+                ListenableFuture<Void> b =
+                    executor.submit(new Task(c, force, !reindex));
                 if (reindex) {
                   result.add(Futures.transform(
-                      b, new AsyncFunction<Boolean, Object>() {
+                      b, new AsyncFunction<Void, Void>() {
                         @SuppressWarnings("unchecked")
                         @Override
-                        public ListenableFuture<Object> apply(
-                            Boolean indexUpdated) throws Exception {
-                          if (!indexUpdated) {
-                            return (ListenableFuture<Object>)
-                                indexer.indexAsync(c.getId());
-                          }
-                          return Futures.immediateFuture(null);
+                        public ListenableFuture<Void> apply(Void o) {
+                          return (ListenableFuture<Void>)
+                              indexer.indexAsync(c.getId());
                         }
                       }));
                 } else {
@@ -291,19 +288,21 @@
     return ProjectConfig.read(metaDataUpdateFactory.create(p), id);
   }
 
-  private class Task implements Callable<Boolean> {
+  private class Task implements Callable<Void> {
     private final Change change;
     private final boolean force;
+    private final boolean reindex;
 
     private ReviewDb reviewDb;
 
-    Task(Change change, boolean force) {
+    Task(Change change, boolean force, boolean reindex) {
       this.change = change;
       this.force = force;
+      this.reindex = reindex;
     }
 
     @Override
-    public Boolean call() throws Exception {
+    public Void call() throws Exception {
       mergeabilityCheckQueue.updatingMergeabilityFlag(change, force);
 
       RequestContext context = new RequestContext() {
@@ -335,20 +334,20 @@
         PatchSet ps = db.patchSets().get(change.currentPatchSetId());
         if (ps == null) {
           // Cannot compute mergeability if current patch set is missing.
-          return false;
+          return null;
         }
 
         Mergeable m = mergeable.get();
         m.setForce(force);
+        m.setReindex(reindex);
 
         ChangeControl control =
-            changeControlFactory.controlFor(change.getId(), context.getCurrentUser());
-        MergeableInfo info = m.apply(
-            new RevisionResource(new ChangeResource(control), ps));
-        return change.isMergeable() != info.mergeable;
+            changeControlFactory.controlFor(change, context.getCurrentUser());
+        m.apply(new RevisionResource(new ChangeResource(control), ps));
+        return null;
       } catch (ResourceConflictException e) {
         // change is closed
-        return false;
+        return null;
       } catch (Exception e) {
         log.error(String.format(
             "cannot update mergeability flag of change %d in project %s after update of %s",
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
index f151792..3605868 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
@@ -82,6 +82,10 @@
     this.force = force;
   }
 
+  public void setReindex(boolean reindex) {
+    this.reindex = reindex;
+  }
+
   private final GitRepositoryManager gitManager;
   private final ProjectCache projectCache;
   private final ChangeData.Factory changeDataFactory;
@@ -90,6 +94,7 @@
   private final ChangeIndexer indexer;
 
   private boolean force;
+  private boolean reindex;
 
   @Inject
   Mergeable(GitRepositoryManager gitManager,
@@ -104,6 +109,7 @@
     this.submitStrategyFactory = submitStrategyFactory;
     this.db = db;
     this.indexer = indexer;
+    reindex = true;
   }
 
   @Override
@@ -197,7 +203,7 @@
             }
           }
         });
-    if (c != null) {
+    if (reindex && c != null) {
       indexer.index(db.get(), c);
     }
     return mergeable;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
index 6322668..295189b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
@@ -68,7 +68,8 @@
     }
 
     try {
-      rebaseChange.get().rebase(rsrc.getPatchSet().getId(), rsrc.getUser());
+      rebaseChange.get().rebase(rsrc.getChange(), rsrc.getPatchSet().getId(),
+          rsrc.getUser());
     } catch (InvalidChangeOperationException e) {
       throw new ResourceConflictException(e.getMessage());
     } catch (IOException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
index 1e9f94a..d3869ad 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
@@ -95,6 +95,7 @@
    * E-mail notification and triggering of hooks happens for the creation of the
    * new patch set.
    *
+   * @param change the change to perform the rebase for
    * @param patchSetId the id of the patch set
    * @param uploader the user that creates the rebased patch set
    * @throws NoSuchChangeException thrown if the change to which the patch set
@@ -105,18 +106,17 @@
    * @throws IOException thrown if rebase is not possible or not needed
    * @throws InvalidChangeOperationException thrown if rebase is not allowed
    */
-  public void rebase(final PatchSet.Id patchSetId, final IdentifiedUser uploader)
+  public void rebase(Change change, PatchSet.Id patchSetId, final IdentifiedUser uploader)
       throws NoSuchChangeException, EmailException, OrmException, IOException,
       InvalidChangeOperationException {
     final Change.Id changeId = patchSetId.getParentKey();
     final ChangeControl changeControl =
-        changeControlFactory.validateFor(changeId, uploader);
+        changeControlFactory.validateFor(change, uploader);
     if (!changeControl.canRebase()) {
       throw new InvalidChangeOperationException(
           "Cannot rebase: New patch sets are not allowed to be added to change: "
               + changeId.toString());
     }
-    final Change change = changeControl.getChange();
     Repository git = null;
     RevWalk rw = null;
     ObjectInserter inserter = null;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilityConstants.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilityConstants.java
index 3805a0e..4c93ce0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilityConstants.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilityConstants.java
@@ -29,7 +29,6 @@
   public String createProject;
   public String emailReviewers;
   public String flushCaches;
-  public String generateHttpPassword;
   public String killTask;
   public String modifyAccount;
   public String priority;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAbandonedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAbandonedEvent.java
index b0eb9c6..33cf241 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAbandonedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAbandonedEvent.java
@@ -14,14 +14,38 @@
 
 package com.google.gerrit.server.events;
 
+import static org.eclipse.jgit.lib.Constants.R_HEADS;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.data.AccountAttribute;
 import com.google.gerrit.server.data.ChangeAttribute;
 import com.google.gerrit.server.data.PatchSetAttribute;
 
 public class ChangeAbandonedEvent extends ChangeEvent {
-    public final String type = "change-abandoned";
-    public ChangeAttribute change;
-    public PatchSetAttribute patchSet;
-    public AccountAttribute abandoner;
-    public String reason;
+  public final String type = "change-abandoned";
+  public ChangeAttribute change;
+  public PatchSetAttribute patchSet;
+  public AccountAttribute abandoner;
+  public String reason;
+
+  @Override
+  public String getType() {
+    return type;
+  }
+
+  @Override
+  public Project.NameKey getProjectNameKey() {
+    return new Project.NameKey(change.project);
+  }
+
+  @Override
+  public Change.Key getChangeKey() {
+    return new Change.Key(change.id);
+  }
+
+  @Override
+  public String getRefName() {
+    return R_HEADS + change.branch;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeEvent.java
index 904a0a0..77ba756 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeEvent.java
@@ -14,5 +14,15 @@
 
 package com.google.gerrit.server.events;
 
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+
 public abstract class ChangeEvent {
+  public abstract String getType();
+
+  public abstract Project.NameKey getProjectNameKey();
+
+  public abstract Change.Key getChangeKey();
+
+  public abstract String getRefName();
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeMergedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeMergedEvent.java
index 38996a5..f1aaa0a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeMergedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeMergedEvent.java
@@ -14,13 +14,37 @@
 
 package com.google.gerrit.server.events;
 
+import static org.eclipse.jgit.lib.Constants.R_HEADS;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.data.AccountAttribute;
 import com.google.gerrit.server.data.ChangeAttribute;
 import com.google.gerrit.server.data.PatchSetAttribute;
 
 public class ChangeMergedEvent extends ChangeEvent {
-    public final String type = "change-merged";
-    public ChangeAttribute change;
-    public PatchSetAttribute patchSet;
-    public AccountAttribute submitter;
+  public final String type = "change-merged";
+  public ChangeAttribute change;
+  public PatchSetAttribute patchSet;
+  public AccountAttribute submitter;
+
+  @Override
+  public String getType() {
+    return type;
+  }
+
+  @Override
+  public Project.NameKey getProjectNameKey() {
+    return new Project.NameKey(change.project);
+  }
+
+  @Override
+  public Change.Key getChangeKey() {
+    return new Change.Key(change.id);
+  }
+
+  @Override
+  public String getRefName() {
+    return R_HEADS + change.branch;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoredEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoredEvent.java
index e761190..bf759249 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoredEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoredEvent.java
@@ -14,14 +14,38 @@
 
 package com.google.gerrit.server.events;
 
+import static org.eclipse.jgit.lib.Constants.R_HEADS;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.data.AccountAttribute;
 import com.google.gerrit.server.data.ChangeAttribute;
 import com.google.gerrit.server.data.PatchSetAttribute;
 
 public class ChangeRestoredEvent extends ChangeEvent {
-    public final String type = "change-restored";
-    public ChangeAttribute change;
-    public PatchSetAttribute patchSet;
-    public AccountAttribute restorer;
-    public String reason;
+  public final String type = "change-restored";
+  public ChangeAttribute change;
+  public PatchSetAttribute patchSet;
+  public AccountAttribute restorer;
+  public String reason;
+
+  @Override
+  public String getType() {
+    return type;
+  }
+
+  @Override
+  public Project.NameKey getProjectNameKey() {
+    return new Project.NameKey(change.project);
+  }
+
+  @Override
+  public Change.Key getChangeKey() {
+    return new Change.Key(change.id);
+  }
+
+  @Override
+  public String getRefName() {
+    return R_HEADS + change.branch;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/CommentAddedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/CommentAddedEvent.java
index 52d7409..3b0cf9c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/CommentAddedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/CommentAddedEvent.java
@@ -14,16 +14,40 @@
 
 package com.google.gerrit.server.events;
 
+import static org.eclipse.jgit.lib.Constants.R_HEADS;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.data.AccountAttribute;
 import com.google.gerrit.server.data.ApprovalAttribute;
 import com.google.gerrit.server.data.ChangeAttribute;
 import com.google.gerrit.server.data.PatchSetAttribute;
 
 public class CommentAddedEvent extends ChangeEvent {
-    public final String type = "comment-added";
-    public ChangeAttribute change;
-    public PatchSetAttribute patchSet;
-    public AccountAttribute author;
-    public ApprovalAttribute[] approvals;
-    public String comment;
+  public final String type = "comment-added";
+  public ChangeAttribute change;
+  public PatchSetAttribute patchSet;
+  public AccountAttribute author;
+  public ApprovalAttribute[] approvals;
+  public String comment;
+
+  @Override
+  public String getType() {
+    return type;
+  }
+
+  @Override
+  public Project.NameKey getProjectNameKey() {
+    return new Project.NameKey(change.project);
+  }
+
+  @Override
+  public Change.Key getChangeKey() {
+    return new Change.Key(change.id);
+  }
+
+  @Override
+  public String getRefName() {
+    return R_HEADS + change.branch;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/CommitReceivedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/CommitReceivedEvent.java
index 8dd1084..825b595 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/CommitReceivedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/CommitReceivedEvent.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.events;
 
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.IdentifiedUser;
 
@@ -21,6 +22,7 @@
 import org.eclipse.jgit.transport.ReceiveCommand;
 
 public class CommitReceivedEvent extends ChangeEvent {
+  public final String type = "commit-received";
   public final ReceiveCommand command;
   public final Project project;
   public final String refName;
@@ -35,4 +37,24 @@
     this.commit = commit;
     this.user = user;
   }
+
+  @Override
+  public String getType() {
+    return type;
+  }
+
+  @Override
+  public Project.NameKey getProjectNameKey() {
+    return project.getNameKey();
+  }
+
+  @Override
+  public Change.Key getChangeKey() {
+    return null;
+  }
+
+  @Override
+  public String getRefName() {
+    return refName;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/DraftPublishedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/DraftPublishedEvent.java
index 7fd033a..a595165 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/DraftPublishedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/DraftPublishedEvent.java
@@ -14,13 +14,37 @@
 
 package com.google.gerrit.server.events;
 
+import static org.eclipse.jgit.lib.Constants.R_HEADS;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.data.AccountAttribute;
 import com.google.gerrit.server.data.ChangeAttribute;
 import com.google.gerrit.server.data.PatchSetAttribute;
 
 public class DraftPublishedEvent extends ChangeEvent {
-    public final String type = "draft-published";
-    public ChangeAttribute change;
-    public PatchSetAttribute patchSet;
-    public AccountAttribute uploader;
+  public final String type = "draft-published";
+  public ChangeAttribute change;
+  public PatchSetAttribute patchSet;
+  public AccountAttribute uploader;
+
+  @Override
+  public String getType() {
+    return type;
+  }
+
+  @Override
+  public Project.NameKey getProjectNameKey() {
+    return new Project.NameKey(change.project);
+  }
+
+  @Override
+  public Change.Key getChangeKey() {
+    return new Change.Key(change.id);
+  }
+
+  @Override
+  public String getRefName() {
+    return R_HEADS + change.branch;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/HashtagsChangedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/HashtagsChangedEvent.java
index 4b4bbba..91c14b1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/HashtagsChangedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/HashtagsChangedEvent.java
@@ -14,6 +14,10 @@
 
 package com.google.gerrit.server.events;
 
+import static org.eclipse.jgit.lib.Constants.R_HEADS;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.data.AccountAttribute;
 import com.google.gerrit.server.data.ChangeAttribute;
 
@@ -24,4 +28,24 @@
   public String[] added;
   public String[] removed;
   public String[] hashtags;
+
+  @Override
+  public String getType() {
+    return type;
+  }
+
+  @Override
+  public Project.NameKey getProjectNameKey() {
+    return new Project.NameKey(change.project);
+  }
+
+  @Override
+  public Change.Key getChangeKey() {
+    return new Change.Key(change.id);
+  }
+
+  @Override
+  public String getRefName() {
+    return R_HEADS + change.branch;
+  }
 }
\ No newline at end of file
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
index 599fe60..4ec6da3 100644
--- 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
@@ -14,14 +14,38 @@
 
 package com.google.gerrit.server.events;
 
+import static org.eclipse.jgit.lib.Constants.R_HEADS;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.data.AccountAttribute;
 import com.google.gerrit.server.data.ChangeAttribute;
 import com.google.gerrit.server.data.PatchSetAttribute;
 
 public class MergeFailedEvent extends ChangeEvent {
-    public final String type = "merge-failed";
-    public ChangeAttribute change;
-    public PatchSetAttribute patchSet;
-    public AccountAttribute submitter;
-    public String reason;
+  public final String type = "merge-failed";
+  public ChangeAttribute change;
+  public PatchSetAttribute patchSet;
+  public AccountAttribute submitter;
+  public String reason;
+
+  @Override
+  public String getType() {
+    return type;
+  }
+
+  @Override
+  public Project.NameKey getProjectNameKey() {
+    return new Project.NameKey(change.project);
+  }
+
+  @Override
+  public Change.Key getChangeKey() {
+    return new Change.Key(change.id);
+  }
+
+  @Override
+  public String getRefName() {
+    return R_HEADS + change.branch;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetCreatedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetCreatedEvent.java
index fbaf4ef..830c6cb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetCreatedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetCreatedEvent.java
@@ -14,13 +14,37 @@
 
 package com.google.gerrit.server.events;
 
+import static org.eclipse.jgit.lib.Constants.R_HEADS;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.data.AccountAttribute;
 import com.google.gerrit.server.data.ChangeAttribute;
 import com.google.gerrit.server.data.PatchSetAttribute;
 
 public class PatchSetCreatedEvent extends ChangeEvent {
-    public final String type = "patchset-created";
-    public ChangeAttribute change;
-    public PatchSetAttribute patchSet;
-    public AccountAttribute uploader;
+  public final String type = "patchset-created";
+  public ChangeAttribute change;
+  public PatchSetAttribute patchSet;
+  public AccountAttribute uploader;
+
+  @Override
+  public String getType() {
+    return type;
+  }
+
+  @Override
+  public Project.NameKey getProjectNameKey() {
+    return new Project.NameKey(change.project);
+  }
+
+  @Override
+  public Change.Key getChangeKey() {
+    return new Change.Key(change.id);
+  }
+
+  @Override
+  public String getRefName() {
+    return R_HEADS + change.branch;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/RefOperationReceivedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/RefOperationReceivedEvent.java
index d26632b..da3a215 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/RefOperationReceivedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/RefOperationReceivedEvent.java
@@ -13,6 +13,7 @@
 // limitations under the License.
 package com.google.gerrit.server.events;
 
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.IdentifiedUser;
 
@@ -23,4 +24,24 @@
   public ReceiveCommand command;
   public Project project;
   public IdentifiedUser user;
-}
\ No newline at end of file
+
+  @Override
+  public String getType() {
+    return type;
+  }
+
+  @Override
+  public Project.NameKey getProjectNameKey() {
+    return project.getNameKey();
+  }
+
+  @Override
+  public Change.Key getChangeKey() {
+    return null;
+  }
+
+  @Override
+  public String getRefName() {
+    return command.getRefName();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdatedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdatedEvent.java
index 944c9ad..4b7244e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdatedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdatedEvent.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.events;
 
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.data.AccountAttribute;
 import com.google.gerrit.server.data.RefUpdateAttribute;
 
@@ -21,4 +23,24 @@
   public final String type = "ref-updated";
   public AccountAttribute submitter;
   public RefUpdateAttribute refUpdate;
+
+  @Override
+  public String getType() {
+    return type;
+  }
+
+  @Override
+  public Project.NameKey getProjectNameKey() {
+    return new Project.NameKey(refUpdate.project);
+  }
+
+  @Override
+  public Change.Key getChangeKey() {
+    return null;
+  }
+
+  @Override
+  public String getRefName() {
+    return refUpdate.refName;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ReviewerAddedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ReviewerAddedEvent.java
index e00cc60..20eb30b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ReviewerAddedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ReviewerAddedEvent.java
@@ -14,13 +14,37 @@
 
 package com.google.gerrit.server.events;
 
+import static org.eclipse.jgit.lib.Constants.R_HEADS;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.data.AccountAttribute;
 import com.google.gerrit.server.data.ChangeAttribute;
 import com.google.gerrit.server.data.PatchSetAttribute;
 
 public class ReviewerAddedEvent extends ChangeEvent {
-    public final String type = "reviewer-added";
-    public ChangeAttribute change;
-    public PatchSetAttribute patchSet;
-    public AccountAttribute reviewer;
+  public final String type = "reviewer-added";
+  public ChangeAttribute change;
+  public PatchSetAttribute patchSet;
+  public AccountAttribute reviewer;
+
+  @Override
+  public String getType() {
+    return type;
+  }
+
+  @Override
+  public Project.NameKey getProjectNameKey() {
+    return new Project.NameKey(change.project);
+  }
+
+  @Override
+  public Change.Key getChangeKey() {
+    return new Change.Key(change.id);
+  }
+
+  @Override
+  public String getRefName() {
+    return R_HEADS + change.branch;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/TopicChangedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/TopicChangedEvent.java
index e725eac..f897dbc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/TopicChangedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/TopicChangedEvent.java
@@ -14,6 +14,10 @@
 
 package com.google.gerrit.server.events;
 
+import static org.eclipse.jgit.lib.Constants.R_HEADS;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.data.AccountAttribute;
 import com.google.gerrit.server.data.ChangeAttribute;
 
@@ -22,4 +26,24 @@
   public ChangeAttribute change;
   public AccountAttribute changer;
   public String oldTopic;
-}
\ No newline at end of file
+
+  @Override
+  public String getType() {
+    return type;
+  }
+
+  @Override
+  public Project.NameKey getProjectNameKey() {
+    return new Project.NameKey(change.project);
+  }
+
+  @Override
+  public Change.Key getChangeKey() {
+    return new Change.Key(change.id);
+  }
+
+  @Override
+  public String getRefName() {
+    return R_HEADS + change.branch;
+  }
+}
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 e5c5872..6db0cfa 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
@@ -86,6 +86,7 @@
 import org.eclipse.jgit.revwalk.RevFlag;
 import org.eclipse.jgit.revwalk.RevSort;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.joda.time.format.ISODateTimeFormat;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -129,29 +130,39 @@
   private static final long MAX_SUBMIT_WINDOW =
       MILLISECONDS.convert(12, HOURS);
 
-  private final GitRepositoryManager repoManager;
-  private final SchemaFactory<ReviewDb> schemaFactory;
-  private final ChangeNotes.Factory notesFactory;
-  private final ProjectCache projectCache;
-  private final GitReferenceUpdated gitRefUpdated;
-  private final MergedSender.Factory mergedSenderFactory;
-  private final MergeFailSender.Factory mergeFailSenderFactory;
-  private final PatchSetInfoFactory patchSetInfoFactory;
-  private final IdentifiedUser.GenericFactory identifiedUserFactory;
+  private final AccountCache accountCache;
+  private final ApprovalsUtil approvalsUtil;
   private final ChangeControl.GenericFactory changeControlFactory;
   private final ChangeData.Factory changeDataFactory;
+  private final ChangeHooks hooks;
+  private final ChangeIndexer indexer;
+  private final ChangeMessagesUtil cmUtil;
+  private final ChangeNotes.Factory notesFactory;
   private final ChangeUpdate.Factory updateFactory;
+  private final GitReferenceUpdated gitRefUpdated;
+  private final GitRepositoryManager repoManager;
+  private final IdentifiedUser.GenericFactory identifiedUserFactory;
+  private final MergedSender.Factory mergedSenderFactory;
+  private final MergeFailSender.Factory mergeFailSenderFactory;
   private final MergeQueue mergeQueue;
   private final MergeValidators.Factory mergeValidatorsFactory;
-  private final ApprovalsUtil approvalsUtil;
-  private final ChangeMessagesUtil cmUtil;
+  private final PatchSetInfoFactory patchSetInfoFactory;
+  private final ProjectCache projectCache;
+  private final RequestScopePropagator requestScopePropagator;
+  private final SchemaFactory<ReviewDb> schemaFactory;
+  private final SubmitStrategyFactory submitStrategyFactory;
+  private final SubmoduleOp.Factory subOpFactory;
+  private final TagCache tagCache;
+  private final WorkQueue workQueue;
 
+  private final String logPrefix;
   private final Branch.NameKey destBranch;
-  private ProjectState destProject;
   private final ListMultimap<SubmitType, CodeReviewCommit> toMerge;
   private final List<CodeReviewCommit> potentiallyStillSubmittable;
   private final Map<Change.Id, CodeReviewCommit> commits;
   private final List<Change> toUpdate;
+
+  private ProjectState destProject;
   private ReviewDb db;
   private Repository repo;
   private RevWalk rw;
@@ -161,60 +172,58 @@
   private ObjectInserter inserter;
   private PersonIdent refLogIdent;
 
-  private final ChangeHooks hooks;
-  private final AccountCache accountCache;
-  private final TagCache tagCache;
-  private final SubmitStrategyFactory submitStrategyFactory;
-  private final SubmoduleOp.Factory subOpFactory;
-  private final WorkQueue workQueue;
-  private final RequestScopePropagator requestScopePropagator;
-  private final ChangeIndexer indexer;
-
   @Inject
-  MergeOp(final GitRepositoryManager grm, final SchemaFactory<ReviewDb> sf,
-      final ChangeNotes.Factory nf,
-      final ProjectCache pc,
-      final GitReferenceUpdated gru, final MergedSender.Factory msf,
-      final MergeFailSender.Factory mfsf,
-      final PatchSetInfoFactory psif, final IdentifiedUser.GenericFactory iuf,
-      final ChangeControl.GenericFactory changeControlFactory,
-      final ChangeData.Factory changeDataFactory,
-      final ChangeUpdate.Factory updateFactory,
-      final MergeQueue mergeQueue, @Assisted final Branch.NameKey branch,
-      final ChangeHooks hooks, final AccountCache accountCache,
-      final TagCache tagCache,
-      final SubmitStrategyFactory submitStrategyFactory,
-      final SubmoduleOp.Factory subOpFactory,
-      final WorkQueue workQueue,
-      final RequestScopePropagator requestScopePropagator,
-      final ChangeIndexer indexer,
-      final MergeValidators.Factory mergeValidatorsFactory,
-      final ApprovalsUtil approvalsUtil,
-      final ChangeMessagesUtil cmUtil) {
-    repoManager = grm;
-    schemaFactory = sf;
-    notesFactory = nf;
-    projectCache = pc;
-    gitRefUpdated = gru;
-    mergedSenderFactory = msf;
-    mergeFailSenderFactory = mfsf;
-    patchSetInfoFactory = psif;
-    identifiedUserFactory = iuf;
+  MergeOp(AccountCache accountCache,
+      ApprovalsUtil approvalsUtil,
+      ChangeControl.GenericFactory changeControlFactory,
+      ChangeData.Factory changeDataFactory,
+      ChangeHooks hooks,
+      ChangeIndexer indexer,
+      ChangeMessagesUtil cmUtil,
+      ChangeNotes.Factory notesFactory,
+      ChangeUpdate.Factory updateFactory,
+      GitReferenceUpdated gitRefUpdated,
+      GitRepositoryManager repoManager,
+      IdentifiedUser.GenericFactory identifiedUserFactory,
+      MergedSender.Factory mergedSenderFactory,
+      MergeFailSender.Factory mergeFailSenderFactory,
+      MergeQueue mergeQueue,
+      MergeValidators.Factory mergeValidatorsFactory,
+      PatchSetInfoFactory patchSetInfoFactory,
+      ProjectCache projectCache,
+      RequestScopePropagator requestScopePropagator,
+      SchemaFactory<ReviewDb> schemaFactory,
+      SubmitStrategyFactory submitStrategyFactory,
+      SubmoduleOp.Factory subOpFactory,
+      TagCache tagCache,
+      WorkQueue workQueue,
+      @Assisted Branch.NameKey branch) {
+    this.accountCache = accountCache;
+    this.approvalsUtil = approvalsUtil;
     this.changeControlFactory = changeControlFactory;
     this.changeDataFactory = changeDataFactory;
-    this.updateFactory = updateFactory;
-    this.mergeQueue = mergeQueue;
     this.hooks = hooks;
-    this.accountCache = accountCache;
-    this.tagCache = tagCache;
+    this.indexer = indexer;
+    this.cmUtil = cmUtil;
+    this.notesFactory = notesFactory;
+    this.updateFactory = updateFactory;
+    this.gitRefUpdated = gitRefUpdated;
+    this.repoManager = repoManager;
+    this.identifiedUserFactory = identifiedUserFactory;
+    this.mergedSenderFactory = mergedSenderFactory;
+    this.mergeFailSenderFactory = mergeFailSenderFactory;
+    this.mergeQueue = mergeQueue;
+    this.mergeValidatorsFactory = mergeValidatorsFactory;
+    this.patchSetInfoFactory = patchSetInfoFactory;
+    this.projectCache = projectCache;
+    this.requestScopePropagator = requestScopePropagator;
+    this.schemaFactory = schemaFactory;
     this.submitStrategyFactory = submitStrategyFactory;
     this.subOpFactory = subOpFactory;
+    this.tagCache = tagCache;
     this.workQueue = workQueue;
-    this.requestScopePropagator = requestScopePropagator;
-    this.indexer = indexer;
-    this.mergeValidatorsFactory = mergeValidatorsFactory;
-    this.approvalsUtil = approvalsUtil;
-    this.cmUtil = cmUtil;
+    logPrefix = String.format("[%s@%s]: ", branch.toString(),
+        ISODateTimeFormat.hourMinuteSecond().print(TimeUtil.nowMs()));
     destBranch = branch;
     toMerge = ArrayListMultimap.create();
     potentiallyStillSubmittable = new ArrayList<>();
@@ -235,7 +244,9 @@
     }
   }
 
-  public void merge() throws MergeException, NoSuchChangeException, IOException {
+  public void merge()
+      throws MergeException, NoSuchChangeException, IOException {
+    logDebug("Beginning merge attempt on {}", destBranch);
     setDestProject();
     try {
       openSchema();
@@ -244,21 +255,23 @@
       RefUpdate branchUpdate = openBranch();
       boolean reopen = false;
 
-      final ListMultimap<SubmitType, Change> toSubmit =
+      ListMultimap<SubmitType, Change> toSubmit =
           validateChangeList(db.changes().submitted(destBranch).toList());
-      final ListMultimap<SubmitType, CodeReviewCommit> toMergeNextTurn =
+      ListMultimap<SubmitType, CodeReviewCommit> toMergeNextTurn =
           ArrayListMultimap.create();
-      final List<CodeReviewCommit> potentiallyStillSubmittableOnNextRun =
+      List<CodeReviewCommit> potentiallyStillSubmittableOnNextRun =
           new ArrayList<>();
       while (!toMerge.isEmpty()) {
+        logDebug("Beginning merge iteration with {} left to merge",
+            toMerge.size());
         toMergeNextTurn.clear();
-        final Set<SubmitType> submitTypes =
-            new HashSet<>(toMerge.keySet());
-        for (final SubmitType submitType : submitTypes) {
+        Set<SubmitType> submitTypes = new HashSet<>(toMerge.keySet());
+        for (SubmitType submitType : submitTypes) {
           if (reopen) {
+            logDebug("Reopening branch");
             branchUpdate = openBranch();
           }
-          final SubmitStrategy strategy = createStrategy(submitType);
+          SubmitStrategy strategy = createStrategy(submitType);
           preMerge(strategy, toMerge.get(submitType));
           RefUpdate update = updateBranch(strategy, branchUpdate);
           reopen = true;
@@ -269,40 +282,46 @@
             fireRefUpdated(update);
           }
 
-          for (final Iterator<CodeReviewCommit> it =
+          for (Iterator<CodeReviewCommit> it =
               potentiallyStillSubmittable.iterator(); it.hasNext();) {
-            final CodeReviewCommit commit = it.next();
+            CodeReviewCommit commit = it.next();
             if (containsMissingCommits(toMerge, commit)
                 || containsMissingCommits(toMergeNextTurn, commit)) {
               // change has missing dependencies, but all commits which are
               // missing are still attempted to be merged with another submit
               // strategy, retry to merge this commit in the next turn
+              logDebug("Revision {} of patch set {} has missing dependencies"
+                  + " with different submit types, reconsidering on next run",
+                  commit.name(), commit.getPatchsetId());
               it.remove();
               commit.setStatusCode(null);
               commit.missing = null;
               toMergeNextTurn.put(submitType, commit);
             }
           }
-          potentiallyStillSubmittableOnNextRun.addAll(potentiallyStillSubmittable);
+          logDebug("Adding {} changes potentially submittable on next run",
+              potentiallyStillSubmittable.size());
+          potentiallyStillSubmittableOnNextRun.addAll(
+              potentiallyStillSubmittable);
           potentiallyStillSubmittable.clear();
         }
         toMerge.clear();
         toMerge.putAll(toMergeNextTurn);
+        logDebug("Adding {} changes to merge on next run", toMerge.size());
       }
 
       updateChangeStatus(toUpdate);
 
-      for (final CodeReviewCommit commit : potentiallyStillSubmittableOnNextRun) {
-        final Capable capable = isSubmitStillPossible(commit);
+      for (CodeReviewCommit commit : potentiallyStillSubmittableOnNextRun) {
+        Capable capable = isSubmitStillPossible(commit);
         if (capable != Capable.OK) {
           sendMergeFail(commit.notes(),
               message(commit.change(), capable.getMessage()), false);
         }
       }
     } catch (NoSuchProjectException noProject) {
-      log.warn(String.format(
-          "Project %s no longer exists, abandoning open changes",
-          destBranch.getParentKey().get()));
+      logWarn("Project " + destBranch.getParentKey() + " no longer exists,"
+          + " abandoning open changes");
       abandonAllOpenChanges();
     } catch (OrmException e) {
       throw new MergeException("Cannot query the database", e);
@@ -323,13 +342,12 @@
   }
 
   private boolean containsMissingCommits(
-      final ListMultimap<SubmitType, CodeReviewCommit> map,
-      final CodeReviewCommit commit) {
+      ListMultimap<SubmitType, CodeReviewCommit> map, CodeReviewCommit commit) {
     if (!isSubmitForMissingCommitsStillPossible(commit)) {
       return false;
     }
 
-    for (final CodeReviewCommit missingCommit : commit.missing) {
+    for (CodeReviewCommit missingCommit : commit.missing) {
       if (!map.containsValue(missingCommit)) {
         return false;
       }
@@ -337,8 +355,12 @@
     return true;
   }
 
-  private boolean isSubmitForMissingCommitsStillPossible(final CodeReviewCommit commit) {
+  private boolean isSubmitForMissingCommitsStillPossible(
+      CodeReviewCommit commit) {
+    PatchSet.Id psId = commit.getPatchsetId();
     if (commit.missing == null || commit.missing.isEmpty()) {
+      logDebug("Patch set {} is not submittable: no list of missing commits",
+          psId);
       return false;
     }
 
@@ -346,7 +368,7 @@
       try {
         loadChangeInfo(missingCommit);
       } catch (NoSuchChangeException | OrmException e) {
-        log.error("Cannot check if missing commits can be submitted", e);
+        logError("Cannot check if missing commits can be submitted", e);
         return false;
       }
 
@@ -354,14 +376,21 @@
         // The commit doesn't have a patch set, so it cannot be
         // submitted to the branch.
         //
+        logDebug("Patch set {} is not submittable: dependency {} has no"
+            + " associated patch set", psId, missingCommit.name());
         return false;
       }
 
       if (!missingCommit.change().currentPatchSetId().equals(
           missingCommit.getPatchsetId())) {
+        PatchSet.Id missingId = missingCommit.getPatchsetId();
         // If the missing commit is not the current patch set,
         // the change must be rebased to use the proper parent.
         //
+        logDebug("Patch set {} is not submittable: depends on patch set {} of"
+            + " change {}, but current patch set is {}", psId, missingId,
+            missingId.getParentKey(),
+            missingCommit.change().currentPatchSetId());
         return false;
       }
     }
@@ -369,27 +398,30 @@
     return true;
   }
 
-  private void preMerge(final SubmitStrategy strategy,
-      final List<CodeReviewCommit> toMerge) throws MergeException {
+  private void preMerge(SubmitStrategy strategy,
+      List<CodeReviewCommit> toMerge) throws MergeException {
+    logDebug("Running submit strategy {} for {} commits",
+        strategy.getClass().getSimpleName(), toMerge.size());
     mergeTip = strategy.run(branchTip, toMerge);
     refLogIdent = strategy.getRefLogIdent();
+    logDebug("Produced {} new commits", strategy.getNewCommits().size());
     commits.putAll(strategy.getNewCommits());
   }
 
-  private SubmitStrategy createStrategy(final SubmitType submitType)
+  private SubmitStrategy createStrategy(SubmitType submitType)
       throws MergeException, NoSuchProjectException {
     return submitStrategyFactory.create(submitType, db, repo, rw, inserter,
         canMergeFlag, getAlreadyAccepted(branchTip), destBranch);
   }
 
   private void openRepository() throws MergeException, NoSuchProjectException {
-    final Project.NameKey name = destBranch.getParentKey();
+    Project.NameKey name = destBranch.getParentKey();
     try {
       repo = repoManager.openRepository(name);
     } catch (RepositoryNotFoundException notFound) {
       throw new NoSuchProjectException(name, notFound);
     } catch (IOException err) {
-      final String m = "Error opening repository \"" + name.get() + '"';
+      String m = "Error opening repository \"" + name.get() + '"';
       throw new MergeException(m, err);
     }
 
@@ -401,9 +433,10 @@
     inserter = repo.newObjectInserter();
   }
 
-  private RefUpdate openBranch() throws MergeException, OrmException, NoSuchChangeException {
+  private RefUpdate openBranch()
+      throws MergeException, OrmException, NoSuchChangeException {
     try {
-      final RefUpdate branchUpdate = repo.updateRef(destBranch.get());
+      RefUpdate branchUpdate = repo.updateRef(destBranch.get());
       if (branchUpdate.getOldObjectId() != null) {
         branchTip =
             (CodeReviewCommit) rw.parseCommit(branchUpdate.getOldObjectId());
@@ -411,7 +444,7 @@
         branchTip = null;
         branchUpdate.setExpectedOldObjectId(ObjectId.zeroId());
       } else {
-        for (final Change c : db.changes().submitted(destBranch).toList()) {
+        for (Change c : db.changes().submitted(destBranch).toList()) {
           setNew(c, message(c, "Your change could not be merged, "
               + "because the destination branch does not exist anymore."));
         }
@@ -422,50 +455,49 @@
     }
   }
 
-  private Set<RevCommit> getAlreadyAccepted(final CodeReviewCommit branchTip)
+  private Set<RevCommit> getAlreadyAccepted(CodeReviewCommit branchTip)
       throws MergeException {
-    final Set<RevCommit> alreadyAccepted = new HashSet<>();
+    Set<RevCommit> alreadyAccepted = new HashSet<>();
 
     if (branchTip != null) {
       alreadyAccepted.add(branchTip);
     }
 
     try {
-      for (final Ref r : repo.getRefDatabase().getRefs(ALL).values()) {
-        if (r.getName().startsWith(Constants.R_HEADS)) {
-          try {
-            alreadyAccepted.add(rw.parseCommit(r.getObjectId()));
-          } catch (IncorrectObjectTypeException iote) {
-            // Not a commit? Skip over it.
-          }
+      for (Ref r : repo.getRefDatabase().getRefs(Constants.R_HEADS).values()) {
+        try {
+          alreadyAccepted.add(rw.parseCommit(r.getObjectId()));
+        } catch (IncorrectObjectTypeException iote) {
+          // Not a commit? Skip over it.
         }
       }
     } catch (IOException e) {
-      throw new MergeException("Failed to determine already accepted commits.", e);
+      throw new MergeException(
+          "Failed to determine already accepted commits.", e);
     }
 
+    logDebug("Found {} existing heads", alreadyAccepted.size());
     return alreadyAccepted;
   }
 
   private ListMultimap<SubmitType, Change> validateChangeList(
-      final List<Change> submitted) throws MergeException, NoSuchChangeException {
-    final ListMultimap<SubmitType, Change> toSubmit =
-        ArrayListMultimap.create();
+      List<Change> submitted) throws MergeException, NoSuchChangeException {
+    ListMultimap<SubmitType, Change> toSubmit = ArrayListMultimap.create();
 
-    final Map<String, Ref> allRefs;
+    Map<String, Ref> allRefs;
     try {
       allRefs = repo.getRefDatabase().getRefs(ALL);
     } catch (IOException e) {
       throw new MergeException(e.getMessage(), e);
     }
 
-    final Set<ObjectId> tips = new HashSet<>();
-    for (final Ref r : allRefs.values()) {
+    Set<ObjectId> tips = new HashSet<>();
+    for (Ref r : allRefs.values()) {
       tips.add(r.getObjectId());
     }
 
     int commitOrder = 0;
-    for (final Change chg : submitted) {
+    for (Change chg : submitted) {
       ChangeControl ctl;
       try {
         ctl = changeControlFactory.controlFor(chg,
@@ -473,14 +505,15 @@
       } catch (NoSuchChangeException e) {
         throw new MergeException("Failed to validate changes", e);
       }
-      final Change.Id changeId = chg.getId();
+      Change.Id changeId = chg.getId();
       if (chg.currentPatchSetId() == null) {
+        logError("Missing current patch set on change " + changeId);
         commits.put(changeId, CodeReviewCommit.noPatchSet(ctl));
         toUpdate.add(chg);
         continue;
       }
 
-      final PatchSet ps;
+      PatchSet ps;
       try {
         ps = db.patchSets().get(chg.currentPatchSetId());
       } catch (OrmException e) {
@@ -488,16 +521,18 @@
       }
       if (ps == null || ps.getRevision() == null
           || ps.getRevision().get() == null) {
+        logError("Missing patch set or revision on change " + changeId);
         commits.put(changeId, CodeReviewCommit.noPatchSet(ctl));
         toUpdate.add(chg);
         continue;
       }
 
-      final String idstr = ps.getRevision().get();
-      final ObjectId id;
+      String idstr = ps.getRevision().get();
+      ObjectId id;
       try {
         id = ObjectId.fromString(idstr);
       } catch (IllegalArgumentException iae) {
+        logError("Invalid revision on patch set " + ps.getId());
         commits.put(changeId, CodeReviewCommit.noPatchSet(ctl));
         toUpdate.add(chg);
         continue;
@@ -513,16 +548,19 @@
         // want to merge the issue. We can't safely do that if the
         // tip is not reachable.
         //
+        logError("Revision " + idstr + " of patch set " + ps.getId()
+            + " is not contained in any ref");
         commits.put(changeId, CodeReviewCommit.revisionGone(ctl));
         toUpdate.add(chg);
         continue;
       }
 
-      final CodeReviewCommit commit;
+      CodeReviewCommit commit;
       try {
         commit = (CodeReviewCommit) rw.parseCommit(id);
       } catch (IOException e) {
-        log.error("Invalid commit " + id.name() + " on " + chg.getKey(), e);
+        logError(
+            "Invalid commit " + idstr + " on patch set " + ps.getId(), e);
         commits.put(changeId, CodeReviewCommit.revisionGone(ctl));
         toUpdate.add(chg);
         continue;
@@ -535,8 +573,11 @@
 
       MergeValidators mergeValidators = mergeValidatorsFactory.create();
       try {
-        mergeValidators.validatePreMerge(repo, commit, destProject, destBranch, ps.getId());
+        mergeValidators.validatePreMerge(
+            repo, commit, destProject, destBranch, ps.getId());
       } catch (MergeValidationException mve) {
+        logDebug("Revision {} of patch set {} failed validation: {}",
+            idstr, ps.getId(), mve.getStatus());
         commit.setStatusCode(mve.getStatus());
         toUpdate.add(chg);
         continue;
@@ -549,11 +590,13 @@
         //
         try {
           if (rw.isMergedInto(commit, branchTip)) {
+            logDebug("Revision {} of patch set {} is already merged",
+                idstr, ps.getId());
             commit.setStatusCode(CommitMergeStatus.ALREADY_MERGED);
             try {
               setMerged(chg, null);
             } catch (OrmException e) {
-              log.error("Cannot mark change " + chg.getId() + " merged", e);
+              logError("Cannot mark change " + chg.getId() + " merged", e);
             }
             continue;
           }
@@ -569,6 +612,8 @@
         throw new MergeException("Cannot check submit type", err);
       }
       if (submitType == null) {
+        logError("No submit type for revision " + idstr + " of patch set "
+            + ps.getId());
         commit.setStatusCode(CommitMergeStatus.NO_SUBMIT_TYPE);
         toUpdate.add(chg);
         continue;
@@ -588,24 +633,29 @@
       SubmitTypeRecord r = new SubmitRuleEvaluator(cd).setPatchSet(ps)
           .getSubmitType();
       if (r.status != SubmitTypeRecord.Status.OK) {
-        log.error("Failed to get submit type for " + ctl.getChange().getKey());
+        logError("Failed to get submit type for " + ctl.getChange().getKey());
         return null;
       }
       return r.type;
     } catch (OrmException e) {
-      log.error("Failed to get submit type for " + ctl.getChange().getKey(), e);
+      logError("Failed to get submit type for " + ctl.getChange().getKey(), e);
       return null;
     }
   }
 
-  private RefUpdate updateBranch(final SubmitStrategy strategy,
-      final RefUpdate branchUpdate) throws MergeException {
-    if (branchTip == mergeTip || mergeTip == null) {
-      // nothing to do
+  private RefUpdate updateBranch(SubmitStrategy strategy,
+      RefUpdate branchUpdate) throws MergeException {
+    if (branchTip == mergeTip) {
+      logDebug("Branch already at merge tip {}, no update to perform",
+          mergeTip.name());
+      return null;
+    } else if (mergeTip == null) {
+      logDebug("No merge tip, no update to perform");
       return null;
     }
 
     if (RefNames.REFS_CONFIG.equals(branchUpdate.getName())) {
+      logDebug("Loading new configuration from {}", RefNames.REFS_CONFIG);
       try {
         ProjectConfig cfg =
             new ProjectConfig(destProject.getProject().getNameKey());
@@ -622,7 +672,11 @@
     branchUpdate.setNewObjectId(mergeTip);
     branchUpdate.setRefLogMessage("merged", true);
     try {
-      switch (branchUpdate.update(rw)) {
+      RefUpdate.Result result = branchUpdate.update(rw);
+      logDebug("Update of {}: {}..{} returned status {}",
+          branchUpdate.getName(), branchUpdate.getOldObjectId(),
+          branchUpdate.getNewObjectId(), result);
+      switch (result) {
         case NEW:
         case FAST_FORWARD:
           if (branchUpdate.getResult() == RefUpdate.Result.FAST_FORWARD) {
@@ -633,11 +687,11 @@
           }
 
           if (RefNames.REFS_CONFIG.equals(branchUpdate.getName())) {
-            projectCache.evict(destProject.getProject());
-            destProject = projectCache.get(destProject.getProject().getNameKey());
+            Project p = destProject.getProject();
+            projectCache.evict(p);
+            destProject = projectCache.get(p.getNameKey());
             repoManager.setProjectDescription(
-                destProject.getProject().getNameKey(),
-                destProject.getProject().getDescription());
+                p.getNameKey(), p.getDescription());
           }
 
           return branchUpdate;
@@ -661,6 +715,7 @@
   }
 
   private void fireRefUpdated(RefUpdate branchUpdate) {
+    logDebug("Firing ref updated hooks for {}", branchUpdate.getName());
     gitRefUpdated.fire(destBranch.getParentKey(), branchUpdate);
     hooks.doRefUpdatedHook(destBranch, branchUpdate, getAccount(mergeTip));
   }
@@ -683,18 +738,23 @@
     return "";
   }
 
-  private void updateChangeStatus(final List<Change> submitted) throws NoSuchChangeException {
-    for (final Change c : submitted) {
-      final CodeReviewCommit commit = commits.get(c.getId());
-      final CommitMergeStatus s = commit != null ? commit.getStatusCode() : null;
+  private void updateChangeStatus(List<Change> submitted)
+      throws NoSuchChangeException {
+    logDebug("Updating change status for {} changes", submitted);
+    for (Change c : submitted) {
+      CodeReviewCommit commit = commits.get(c.getId());
+      CommitMergeStatus s = commit != null ? commit.getStatusCode() : null;
       if (s == null) {
         // Shouldn't ever happen, but leave the change alone. We'll pick
         // it up on the next pass.
         //
+        logDebug("Submitted change {} did not appear in set of new commits"
+            + " produced by merge strategy", c.getId());
         continue;
       }
 
-      final String txt = s.getMessage();
+      String txt = s.getMessage();
+      logDebug("Status of change {} on {}: {}", c.getId(), c.getDest(), s);
 
       try {
         switch (s) {
@@ -726,23 +786,27 @@
             break;
 
           case MISSING_DEPENDENCY:
+            logDebug("Change {} is missing dependency", c.getId());
             potentiallyStillSubmittable.add(commit);
             break;
 
           default:
-            setNew(commit, message(c, "Unspecified merge failure: " + s.name()));
+            setNew(commit,
+                message(c, "Unspecified merge failure: " + s.name()));
             break;
         }
       } catch (OrmException err) {
-        log.warn("Error updating change status for " + c.getId(), err);
+        logWarn("Error updating change status for " + c.getId(), err);
       } catch (IOException err) {
-        log.warn("Error updating change status for " + c.getId(), err);
+        logWarn("Error updating change status for " + c.getId(), err);
       }
     }
   }
 
-  private void updateSubscriptions(final List<Change> submitted) {
+  private void updateSubscriptions(List<Change> submitted) {
     if (mergeTip != null && (branchTip == null || branchTip != mergeTip)) {
+      logDebug("Updating submodule subscriptions for {} changes",
+          submitted.size());
       SubmoduleOp subOp =
           subOpFactory.create(destBranch, mergeTip, rw, repo,
               destProject.getProject(), submitted, commits,
@@ -750,30 +814,35 @@
       try {
         subOp.update();
       } catch (SubmoduleException e) {
-        log
-            .error("The gitLinks were not updated according to the subscriptions "
-                + e.getMessage());
+        logError(
+            "The gitLinks were not updated according to the subscriptions" , e);
       }
     }
   }
 
-  private Capable isSubmitStillPossible(final CodeReviewCommit commit) {
-    final Capable capable;
-    final Change c = commit.change();
-    final boolean submitStillPossible = isSubmitForMissingCommitsStillPossible(commit);
-    final long now = TimeUtil.nowMs();
-    final long waitUntil = c.getLastUpdatedOn().getTime() + DEPENDENCY_DELAY;
+  private Capable isSubmitStillPossible(CodeReviewCommit commit) {
+    Capable capable;
+    Change c = commit.change();
+    boolean submitStillPossible =
+        isSubmitForMissingCommitsStillPossible(commit);
+    long now = TimeUtil.nowMs();
+    long waitUntil = c.getLastUpdatedOn().getTime() + DEPENDENCY_DELAY;
     if (submitStillPossible && now < waitUntil) {
+      long recheckIn = waitUntil - now;
+      logDebug("Submit for {} is still possible; rechecking in {}ms",
+          c.getId(), recheckIn);
       // If we waited a short while we might still be able to get
       // this change submitted. Reschedule an attempt in a bit.
       //
-      mergeQueue.recheckAfter(destBranch, waitUntil - now, MILLISECONDS);
+      mergeQueue.recheckAfter(destBranch, recheckIn, MILLISECONDS);
       capable = Capable.OK;
     } else if (submitStillPossible) {
       // It would be possible to submit the change if the missing
       // dependencies are also submitted. Perhaps the user just
       // forgot to submit those.
       //
+      logDebug("Submit for {} is still possible after missing dependencies",
+          c.getId());
       StringBuilder m = new StringBuilder();
       m.append("Change could not be merged because of a missing dependency.");
       m.append("\n");
@@ -793,20 +862,23 @@
       // needs to rebase it in order to work around the missing
       // dependencies.
       //
+      logDebug("Submit for {} is not possible", c.getId());
       StringBuilder m = new StringBuilder();
       m.append("Change cannot be merged due to unsatisfiable dependencies.\n");
       m.append("\n");
       m.append("The following dependency errors were found:\n");
       m.append("\n");
       for (CodeReviewCommit missingCommit : commit.missing) {
-        if (missingCommit.getPatchsetId() != null) {
+        PatchSet.Id missingPsId = missingCommit.getPatchsetId();
+        if (missingPsId != null) {
           m.append("* Depends on patch set ");
-          m.append(missingCommit.getPatchsetId().get());
+          m.append(missingPsId.get());
           m.append(" of ");
           m.append(missingCommit.change().getKey().abbreviate());
-          if (missingCommit.getPatchsetId().get() != missingCommit.change().currentPatchSetId().get()) {
+          PatchSet.Id currPsId = missingCommit.change().currentPatchSetId();
+          if (!missingPsId.equals(currPsId)) {
             m.append(", however the current patch set is ");
-            m.append(missingCommit.change().currentPatchSetId().get());
+            m.append(currPsId.get());
           }
           m.append(".\n");
 
@@ -824,21 +896,21 @@
     return capable;
   }
 
-  private void loadChangeInfo(final CodeReviewCommit commit)
+  private void loadChangeInfo(CodeReviewCommit commit)
       throws NoSuchChangeException, OrmException {
     if (commit.getControl() == null) {
       List<PatchSet> matches =
           db.patchSets().byRevision(new RevId(commit.name())).toList();
       if (matches.size() == 1) {
-        PatchSet ps = matches.get(0);
-        commit.setPatchsetId(ps.getId());
-        commit.setControl(changeControl(db.changes().get(ps.getId().getParentKey())));
+        PatchSet.Id psId = matches.get(0).getId();
+        commit.setPatchsetId(psId);
+        commit.setControl(changeControl(db.changes().get(psId.getParentKey())));
       }
     }
   }
 
-  private ChangeMessage message(final Change c, final String body) {
-    final String uuid;
+  private ChangeMessage message(Change c, String body) {
+    String uuid;
     try {
       uuid = ChangeUtil.messageUUID(db);
     } catch (OrmException e) {
@@ -852,6 +924,7 @@
 
   private void setMerged(Change c, ChangeMessage msg)
       throws OrmException, IOException, NoSuchChangeException {
+    logDebug("Setting change {} merged", c.getId());
     ChangeUpdate update = null;
     try {
       db.changes().beginTransaction(c.getId());
@@ -882,7 +955,7 @@
               accountCache.get(submitter.getAccountId()).getAccount(),
               db.patchSets().get(merged), db);
         } catch (OrmException ex) {
-          log.error("Cannot run hook for submitted patch set " + c.getId(), ex);
+          logError("Cannot run hook for submitted patch set " + c.getId(), ex);
         }
       }
     } finally {
@@ -909,7 +982,7 @@
           try {
             c.setCurrentPatchSet(patchSetInfoFactory.get(db, merged));
           } catch (PatchSetInfoNotAvailableException e1) {
-            log.error("Cannot read merged patch set " + merged, e1);
+            logError("Cannot read merged patch set " + merged, e1);
           }
         }
         ChangeUtil.updated(c);
@@ -932,7 +1005,7 @@
             reviewDb.close();
           }
         } catch (Exception e) {
-          log.error("Cannot send email for submitted patch set " + c.getId(), e);
+          logError("Cannot send email for submitted patch set " + c.getId(), e);
           return;
         }
 
@@ -944,7 +1017,7 @@
           cm.setPatchSet(patchSet);
           cm.send();
         } catch (Exception e) {
-          log.error("Cannot send email for submitted patch set " + c.getId(), e);
+          logError("Cannot send email for submitted patch set " + c.getId(), e);
         }
       }
 
@@ -960,11 +1033,13 @@
         c, identifiedUserFactory.create(c.getOwner()));
   }
 
-  private void setNew(CodeReviewCommit c, ChangeMessage msg) throws NoSuchChangeException, IOException {
+  private void setNew(CodeReviewCommit c, ChangeMessage msg)
+      throws NoSuchChangeException, IOException {
     sendMergeFail(c.notes(), msg, true);
   }
 
-  private void setNew(Change c, ChangeMessage msg) throws OrmException, NoSuchChangeException, IOException {
+  private void setNew(Change c, ChangeMessage msg)
+      throws OrmException, NoSuchChangeException, IOException {
     sendMergeFail(notesFactory.create(c), msg, true);
   }
 
@@ -976,10 +1051,17 @@
       @Nullable PatchSetApproval submitter,
       ChangeMessage msg,
       ChangeNotes notes) {
-    if (submitter != null
-        && TimeUtil.nowMs() - submitter.getGranted().getTime()
-          > MAX_SUBMIT_WINDOW) {
-      return RetryStatus.UNSUBMIT;
+    Change.Id id = notes.getChangeId();
+    if (submitter != null) {
+      long sinceMs = TimeUtil.nowMs() - submitter.getGranted().getTime();
+      if (sinceMs > MAX_SUBMIT_WINDOW) {
+        logDebug("Change {} submitted {}ms ago, unsubmitting", id, sinceMs);
+        return RetryStatus.UNSUBMIT;
+      } else {
+        logDebug("Change {} submitted {}ms ago, within window", id, sinceMs);
+      }
+    } else {
+      logDebug("No submitter for change {}", id);
     }
 
     try {
@@ -989,26 +1071,37 @@
             && Objects.equal(last.getMessage(), msg.getMessage())) {
           long lastMs = last.getWrittenOn().getTime();
           long msgMs = msg.getWrittenOn().getTime();
-          return msgMs - lastMs > MAX_SUBMIT_WINDOW
-              ? RetryStatus.UNSUBMIT
-              : RetryStatus.RETRY_NO_MESSAGE;
+          long sinceMs = msgMs - lastMs;
+          if (sinceMs > MAX_SUBMIT_WINDOW) {
+            logDebug("Last message for change {} was {}ms ago, unsubmitting",
+                id, sinceMs);
+            return RetryStatus.UNSUBMIT;
+          } else {
+            logDebug("Last message for change {} was {}ms ago, within window",
+                id, sinceMs);
+            return RetryStatus.RETRY_NO_MESSAGE;
+          }
+        } else {
+          logDebug("Last message for change {} differed, adding message", id);
         }
       }
       return RetryStatus.RETRY_ADD_MESSAGE;
     } catch (OrmException err) {
-      log.warn("Cannot check previous merge failure, unsubmitting", err);
+      logWarn("Cannot check previous merge failure, unsubmitting", err);
       return RetryStatus.UNSUBMIT;
     }
   }
 
   private void sendMergeFail(ChangeNotes notes, final ChangeMessage msg,
       boolean makeNew) throws NoSuchChangeException, IOException {
+    logDebug("Possibly sending merge failure notification for {}",
+        notes.getChangeId());
     PatchSetApproval submitter = null;
     try {
       submitter = approvalsUtil.getSubmitter(
           db, notes, notes.getChange().currentPatchSetId());
     } catch (Exception e) {
-      log.error("Cannot get submitter", e);
+      logError("Cannot get submitter for change " + notes.getChangeId(), e);
     }
 
     if (!makeNew) {
@@ -1053,7 +1146,7 @@
         db.rollback();
       }
     } catch (OrmException err) {
-      log.warn("Cannot record merge failure message", err);
+      logWarn("Cannot record merge failure message", err);
     }
     if (update != null) {
       update.commit();
@@ -1079,12 +1172,12 @@
             reviewDb.close();
           }
         } catch (Exception e) {
-          log.error("Cannot send email notifications about merge failure", e);
+          logError("Cannot send email notifications about merge failure", e);
           return;
         }
 
         try {
-          final MergeFailSender cm = mergeFailSenderFactory.create(c);
+          MergeFailSender cm = mergeFailSenderFactory.create(c);
           if (from != null) {
             cm.setFrom(from.getAccountId());
           }
@@ -1092,7 +1185,7 @@
           cm.setChangeMessage(msg);
           cm.send();
         } catch (Exception e) {
-          log.error("Cannot send email notifications about merge failure", e);
+          logError("Cannot send email notifications about merge failure", e);
         }
       }
 
@@ -1106,7 +1199,7 @@
       try {
         indexFuture.checkedGet();
       } catch (IOException e) {
-        log.error("Failed to index new change message", e);
+        logError("Failed to index new change message", e);
       }
     }
 
@@ -1116,7 +1209,7 @@
             accountCache.get(submitter.getAccountId()).getAccount(),
             db.patchSets().get(c.currentPatchSetId()), msg.getMessage(), db);
       } catch (OrmException ex) {
-        log.error("Cannot run hook for merge failed " + c.getId(), ex);
+        logError("Cannot run hook for merge failed " + c.getId(), ex);
       }
     }
   }
@@ -1125,7 +1218,8 @@
     Exception err = null;
     try {
       openSchema();
-      for (Change c : db.changes().byProjectOpenAll(destBranch.getParentKey())) {
+      for (Change c
+          : db.changes().byProjectOpenAll(destBranch.getParentKey())) {
         abandonOneChange(c);
       }
       db.close();
@@ -1136,9 +1230,8 @@
       err = e;
     }
     if (err != null) {
-      log.warn(String.format(
-          "Cannot abandon changes for deleted project %s",
-          destBranch.getParentKey().get()), err);
+      logWarn("Cannot abandon changes for deleted project "
+          + destBranch.getParentKey().get(), err);
     }
   }
 
@@ -1184,4 +1277,34 @@
     }
     update.commit();
   }
+
+  private void logDebug(String msg, Object... args) {
+    if (log.isDebugEnabled()) {
+      log.debug(logPrefix + msg, args);
+    }
+  }
+
+  private void logWarn(String msg, Throwable t) {
+    if (log.isWarnEnabled()) {
+      log.warn(logPrefix + msg, t);
+    }
+  }
+
+  private void logWarn(String msg) {
+    if (log.isWarnEnabled()) {
+      log.warn(logPrefix + msg);
+    }
+  }
+
+  private void logError(String msg, Throwable t) {
+    if (log.isErrorEnabled()) {
+      log.error(logPrefix + msg, t);
+    }
+  }
+
+  private void logError(String msg) {
+    if (log.isErrorEnabled()) {
+      log.error(logPrefix + msg);
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchesCollection.java
index 3bf3522..e96d3a5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchesCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchesCollection.java
@@ -16,12 +16,14 @@
 
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AcceptsCreate;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ChildCollection;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.server.project.ListBranches.BranchInfo;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.lib.Constants;
@@ -34,12 +36,12 @@
     ChildCollection<ProjectResource, BranchResource>,
     AcceptsCreate<ProjectResource> {
   private final DynamicMap<RestView<BranchResource>> views;
-  private final ListBranches list;
+  private final Provider<ListBranches> list;
   private final CreateBranch.Factory createBranchFactory;
 
   @Inject
   BranchesCollection(DynamicMap<RestView<BranchResource>> views,
-      ListBranches list, CreateBranch.Factory createBranchFactory) {
+      Provider<ListBranches> list, CreateBranch.Factory createBranchFactory) {
     this.views = views;
     this.list = list;
     this.createBranchFactory = createBranchFactory;
@@ -47,18 +49,18 @@
 
   @Override
   public RestView<ProjectResource> list() {
-    return list;
+    return list.get();
   }
 
   @Override
   public BranchResource parse(ProjectResource parent, IdString id)
-      throws ResourceNotFoundException, IOException {
+      throws ResourceNotFoundException, IOException, BadRequestException {
     String branchName = id.get();
     if (!branchName.startsWith(Constants.R_REFS)
         && !branchName.equals(Constants.HEAD)) {
       branchName = Constants.R_HEADS + branchName;
     }
-    List<BranchInfo> branches = list.apply(parent);
+    List<BranchInfo> branches = list.get().apply(parent);
     for (BranchInfo b : branches) {
       if (branchName.equals(b.ref)) {
         return new BranchResource(parent.getControl(), b);
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 03b13c7..13ad817 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
@@ -66,20 +66,6 @@
       }
     }
 
-    public ChangeControl controlFor(Change.Id id, CurrentUser user)
-        throws NoSuchChangeException {
-      final Change change;
-      try {
-        change = db.get().changes().get(id);
-        if (change == null) {
-          throw new NoSuchChangeException(id);
-        }
-      } catch (OrmException e) {
-        throw new NoSuchChangeException(id, e);
-      }
-      return controlFor(change, user);
-    }
-
     public ChangeControl validateFor(Change change, CurrentUser user)
         throws NoSuchChangeException, OrmException {
       ChangeControl c = controlFor(change, user);
@@ -88,15 +74,6 @@
       }
       return c;
     }
-
-    public ChangeControl validateFor(Change.Id id, CurrentUser user)
-        throws NoSuchChangeException, OrmException {
-      ChangeControl c = controlFor(id, user);
-      if (!c.isVisible(db.get())) {
-        throw new NoSuchChangeException(c.getChange().getId());
-      }
-      return c;
-    }
   }
 
   public static class Factory {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
index 01e705a..6e07fb4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
@@ -14,12 +14,15 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.common.base.Predicate;
 import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.gerrit.extensions.common.ActionInfo;
 import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.RestView;
@@ -29,29 +32,44 @@
 import com.google.gerrit.server.extensions.webui.UiActions;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
-import com.google.inject.Singleton;
 import com.google.inject.util.Providers;
 
+import dk.brics.automaton.RegExp;
+import dk.brics.automaton.RunAutomaton;
+
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.Repository;
+import org.kohsuke.args4j.Option;
 
 import java.io.IOException;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.TreeMap;
 
-@Singleton
 public class ListBranches implements RestReadView<ProjectResource> {
   private final GitRepositoryManager repoManager;
   private final DynamicMap<RestView<BranchResource>> branchViews;
   private final WebLinks webLinks;
 
+  @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", usage = "maximum number of branches to list")
+  private int limit;
+
+  @Option(name = "--start", aliases = {"-s"}, metaVar = "CNT", usage = "number of branches to skip")
+  private int start;
+
+  @Option(name = "--match", aliases = {"-m"}, metaVar = "MATCH", usage = "match branches substring")
+  private String matchSubstring;
+
+  @Option(name = "--regex", aliases = {"-r"}, metaVar = "REGEX", usage = "match branches regex")
+  private String matchRegex;
+
   @Inject
   public ListBranches(GitRepositoryManager repoManager,
       DynamicMap<RestView<BranchResource>> branchViews,
@@ -63,7 +81,7 @@
 
   @Override
   public List<BranchInfo> apply(ProjectResource rsrc)
-      throws ResourceNotFoundException, IOException {
+      throws ResourceNotFoundException, IOException, BadRequestException {
     List<BranchInfo> branches = Lists.newArrayList();
 
     BranchInfo headBranch = null;
@@ -150,6 +168,61 @@
     if (headBranch != null) {
       branches.add(0, headBranch);
     }
+
+    List<BranchInfo> filteredBranches;
+    if ((matchSubstring != null && !matchSubstring.isEmpty())
+        || (matchRegex != null && !matchRegex.isEmpty())) {
+      filteredBranches = filterBranches(branches);
+    } else {
+      filteredBranches = branches;
+    }
+    if (!filteredBranches.isEmpty()) {
+      int end = filteredBranches.size();
+      if (limit > 0 && start + limit < end) {
+        end = start + limit;
+      }
+      if (start <= end) {
+        filteredBranches = filteredBranches.subList(start, end);
+      } else {
+        filteredBranches = Collections.emptyList();
+      }
+    }
+    return filteredBranches;
+  }
+
+  private List<BranchInfo> filterBranches(List<BranchInfo> branches)
+      throws BadRequestException {
+    if (matchSubstring != null) {
+      return ((List<BranchInfo>) Lists.newArrayList(Iterables.filter(branches,
+          new Predicate<BranchInfo>() {
+            public boolean apply(BranchInfo in) {
+              return in.ref.toLowerCase(Locale.US).contains(
+                  matchSubstring.toLowerCase(Locale.US));
+            }
+          })));
+    } else if (matchRegex != null) {
+      if (matchRegex.startsWith("^")) {
+        matchRegex = matchRegex.substring(1);
+        if (matchRegex.endsWith("$") && !matchRegex.endsWith("\\$")) {
+          matchRegex = matchRegex.substring(0, matchRegex.length() - 1);
+        }
+      }
+      if (matchRegex.equals(".*")) {
+        return branches;
+      }
+      try {
+        final RunAutomaton a =
+            new RunAutomaton(new RegExp(matchRegex).toAutomaton());
+        return ((List<BranchInfo>) Lists.newArrayList(Iterables.filter(
+            branches, new Predicate<BranchInfo>() {
+              public boolean apply(BranchInfo in) {
+                return a.run(in.ref);
+              }
+            })));
+      } catch (IllegalArgumentException e) {
+        throw new BadRequestException(e.getMessage());
+      }
+    }
     return branches;
   }
 
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/config/CapabilityConstants.properties b/gerrit-server/src/main/resources/com/google/gerrit/server/config/CapabilityConstants.properties
index 056da87..c8e95bc 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/config/CapabilityConstants.properties
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/config/CapabilityConstants.properties
@@ -5,7 +5,6 @@
 createProject = Create Project
 emailReviewers = Email Reviewers
 flushCaches = Flush Caches
-generateHttpPassword = Generate HTTP Password
 killTask = Kill Task
 modifyAccount = Modify Account
 priority = Priority
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
index 61f5fa40..39a9fe7 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
@@ -269,9 +269,8 @@
 
   private String extractWhat(DispatchCommand dcmd) {
     String commandName = dcmd.getCommandName();
-    String[] args = dcmd.getArguments();
-    if (args.length > 1) {
-      return commandName + "." + args[1];
+    for (String arg : dcmd.getArguments()) {
+      commandName = commandName + "." + arg;
     }
     return commandName;
   }
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 76a5f24..3b3df96 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
@@ -18,6 +18,7 @@
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.RawInput;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -260,7 +261,7 @@
     }
   }
 
-  private void deleteSshKey(SshKeyInfo i) throws OrmException {
+  private void deleteSshKey(SshKeyInfo i) throws AuthException, OrmException {
     AccountSshKey sshKey = new AccountSshKey(
         new AccountSshKey.Id(user.getAccountId(), i.seq), i.sshPublicKey);
     deleteSshKey.apply(
diff --git a/lib/codemirror/BUCK b/lib/codemirror/BUCK
index e8539c6..a18c761 100644
--- a/lib/codemirror/BUCK
+++ b/lib/codemirror/BUCK
@@ -2,8 +2,8 @@
 include_defs('//lib/codemirror/cm3.defs')
 include_defs('//lib/codemirror/closure.defs')
 
-VERSION = '28a638a984'
-SHA1 = '68f8f136092a5965778186fb401a33be34cf73ed'
+VERSION = '57e7ed7177'
+SHA1 = 'd78bc5518707960647d0b8b85d9f1ac011b785d5'
 URL = GERRIT + 'net/codemirror/codemirror-%s.zip' % VERSION
 
 ZIP = 'codemirror-%s.zip' % VERSION
diff --git a/plugins/cookbook-plugin b/plugins/cookbook-plugin
index a2e56f0..15efdb1 160000
--- a/plugins/cookbook-plugin
+++ b/plugins/cookbook-plugin
@@ -1 +1 @@
-Subproject commit a2e56f0f76dac45a5084c28a27e24ba039b57e09
+Subproject commit 15efdb1f7e6e5f6e4db2791b8753cd57e6d5c790
diff --git a/plugins/replication b/plugins/replication
index 211e928..ada673d 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 211e9289cc5771293855845d31bc0ce756545851
+Subproject commit ada673decd7b603734dbbadb31510219fa5b1123
diff --git a/tools/java_doc.defs b/tools/java_doc.defs
index d117bda..514a730 100644
--- a/tools/java_doc.defs
+++ b/tools/java_doc.defs
@@ -1,7 +1,7 @@
 def java_doc(
     name,
     title,
-    pkg,
+    pkgs,
     paths,
     srcs = [],
     deps = [],
@@ -24,7 +24,8 @@
       '-notimestamp',
       '-windowtitle "' + title + '"',
       '-link http://docs.oracle.com/javase/7/docs/api',
-      '-subpackages ' + pkg,
+      '-subpackages ',
+      ':'.join(pkgs),
       '-sourcepath ',
       ':'.join(sourcepath),
       ' -classpath ',