Merge "Implement filtering in branch list screen"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 01d92c6..ca567d1 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -2070,7 +2070,7 @@
 	type = HTTP
 	httpHeader = TRUSTED_USER
 
-[http]
+[httpd]
 	filterClass = org.anyorg.MySecureFilter
 ----
 
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index c1f398f..72d7ddf 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -221,6 +221,7 @@
     should be before the instance members.
   * Annotations should go before language keywords (`final`, `private`, etc) +
     Example: `@Assisted @Nullable final type varName`
+  * The `@Inject`-ed constructor arguments should be listed one per line.
   * Imports should be mostly alphabetical (uppercase sorts before
     all lowercase, which means classes come before packages at the
     same level).
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index c2f2af4..1aeab2c 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -415,6 +415,13 @@
 its own custom event class derived from
 `com.google.gerrit.server.events.Event`.
 
+Plugins which define new Events should register them via the
+`com.google.gerrit.server.events.EventTypes.registerClass()`
+method. This will make the EventType known to the system.
+Deserialzing events with the
+`com.google.gerrit.server.events.EventDeserializer` class requires
+that the event be registered in EventTypes.
+
 [[validation]]
 == Validation Listeners
 
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
index a88b42b..3d3104c 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -145,6 +145,12 @@
   git tag -a v2.5
 ----
 
+Tag the plugins:
+
+----
+  git submodule foreach git tag -a v2.5
+----
+
 [[build-gerrit]]
 === Build Gerrit
 
@@ -313,20 +319,18 @@
 [[push-tag]]
 ==== Push the Release Tag
 
-* Push the new Release Tag
-+
-For an `RC`:
-+
-----
-  git push gerrit-review tag v2.5-rc0
-----
-+
-For a final `stable` release:
-+
+Push the new Release Tag:
+
 ----
   git push gerrit-review tag v2.5
 ----
 
+Push the new Release Tag on the plugins:
+
+----
+  git submodule foreach git push gerrit-review tag v2.5
+----
+
 
 [[upload-documentation]]
 ==== Upload the Documentation
@@ -400,9 +404,8 @@
 * Update the new discussion group announcement to be sticky
 ** Go to: http://groups.google.com/group/repo-discuss/topics
 ** Click on the announcement thread
-** Near the top right, click on options
-** Under options, click the "Display this top first" checkbox
-** and Save
+** Near the top right, click on actions
+** Under actions, click the "Display this top first" checkbox
 
 * Update the previous discussion group announcement to no longer be sticky
 ** See above (unclick checkbox)
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 576cec6..bd72353 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -1445,6 +1445,10 @@
   RnJvbSA3ZGFkY2MxNTNmZGVhMTdhYTg0ZmYzMmE2ZTI0NWRiYjY...
 ----
 
+Alternatively, if the only value of the Accept request header is
+`application/json` the content is returned as JSON string and
+`X-FYI-Content-Encoding` is set to `json`.
+
 [[get-edit-meta-data]]
 === Retrieve meta data of a file from Change Edit
 --
@@ -1512,6 +1516,18 @@
 M2JhNjcxZTk0OTBmNzUxNDU5ZGUzCg==
 ----
 
+Alternatively, if the only value of the Accept request header is
+`application/json` the commit message is returned as JSON string:
+
+.Response
+----
+  HTTP/1.1 200 OK
+
+)]}'
+"Subject of the commit message\n\nThis is the body of the commit message.\n\nChange-Id: Iaf1ba916bf843c175673d675bf7f52862f452db9\n"
+----
+
+
 [[publish-edit]]
 === Publish Change Edit
 --
@@ -2861,6 +2877,10 @@
   Ly8gQ29weXJpZ2h0IChDKSAyMDEwIFRoZSBBbmRyb2lkIE9wZW4gU291cmNlIFByb2plY...
 ----
 
+Alternatively, if the only value of the Accept request header is
+`application/json` the content is returned as JSON string and
+`X-FYI-Content-Encoding` is set to `json`.
+
 [[get-diff]]
 === Get Diff
 --
diff --git a/Documentation/user-review-ui.txt b/Documentation/user-review-ui.txt
index ab1070a..b36792f 100644
--- a/Documentation/user-review-ui.txt
+++ b/Documentation/user-review-ui.txt
@@ -350,21 +350,26 @@
 image::images/user-review-ui-change-screen-file-list-comments.png[width=800, link="images/user-review-ui-change-screen-file-list-comments.png"]
 
 [[size]]
-The size of the modifications in the files can be seen in the `Size`
-column. The footer row shows the total size of the change.
-
-For files, the `Size` column shows the sum of inserted and deleted
-lines as one number. For the total size, inserted and deleted lines are
-shown separately. In addition, the number of insertions and deletions
-is shown as a bar. The size of the bar indicates the amount of changed
-lines, and its coloring in green and red shows the proportion of
-insertions to deletions.
+The size of the modifications in the files can be seen in the `Size` column. The
+footer row shows the total size of the change.
 
 The size information is useful to easily spot the files that contain
 the most modifications; these files are likely to be the most relevant
 files for this change. The total change size gives an estimate of how
 long a review of this change may take.
 
+When the "Show Change Sizes As Colored Bars" user preference is enabled, the
+`Size` column shows the sum of inserted and deleted lines as one number.  In
+addition, the change size is shown as a bar. The size of the bar indicates the
+amount of changed lines, and its coloring shows the proportion of insertions
+(green) to deletions (red).
+
+When the "Show Change Sizes As Colored Bars" user preference is disabled, the
+colored bar is not shown.  For added and renamed files, the `Size` column
+shows the number of inserted and deleted lines. For new files, the column only
+shows the total number of lines in the new file. No size is shown for binary
+files and deleted files.
+
 image::images/user-review-ui-change-screen-file-list-size.png[width=800, link="images/user-review-ui-change-screen-file-list-size.png"]
 
 [[diff-against]]
diff --git a/ReleaseNotes/ReleaseNotes-2.11.txt b/ReleaseNotes/ReleaseNotes-2.11.txt
index 3a7a56e..1d32d72 100644
--- a/ReleaseNotes/ReleaseNotes-2.11.txt
+++ b/ReleaseNotes/ReleaseNotes-2.11.txt
@@ -106,6 +106,25 @@
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/config-gerrit.html#change.replyLabel[
 label and tooltip] to be configured.
 
+* Improve file sorting for C and C++ files.
++
+Header files are now listed before implementation files.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=3148[Issue 3148]:
+Allow display of colored size bars to be enabled or disabled per user.
++
+The 'Show Change Sizes As Colored Bars In Changes Table' setting is renamed to
+'Show Change Sizes As Colored Bars' and is now used to also control how the
+change size is shown per file in the file table.
++
+When enabled (which is the default), the change size per file is shown as a sum
+of lines added/removed, and also representated by a colored bar showing the
+proportion of added/removed lines.
++
+When disabled, the colored bar is not shown and the change size per file is shown
+in the same way as it used to be in the old change screen.
+
+
 Side-By-Side Diff
 ^^^^^^^^^^^^^^^^^
 
@@ -130,10 +149,26 @@
 ** Dart
 ** Dockerfile
 ** GLSL shader
+** Go
 ** Objective C
 ** link:http://code.google.com/p/gerrit/issues/detail?id=2779[Issue 2779]: reStructured text
 ** Soy
 
+
+Projects Screen
+^^^^^^^^^^^^^^^
+
+* Add pagination of the branch list page.
+
+* Add an 'Edit Config' button on the project info page.
++
+The button creates a new change on the `refs/meta/config` branch and opens the
+`project.config` file in the inline editor.
++
+This allows project owners to easily edit the `project.config` file from the
+browser, which is useful since it is possible that not all configuration options
+are available in the UI.
+
 REST
 ~~~~
 
@@ -154,8 +189,8 @@
 In the past, Gerrit bugs, lack of transactions, and unreliable NoSQL backends
 have at various times produced a bewildering variety of corrupt states.
 +
-This endpoint can be used to detect and explain some of these possible states
-of a change.
+This endpoint can be used to detect, explain, and repair some of these possible
+states of a change.
 
 Change Edits
 ^^^^^^^^^^^^
@@ -196,17 +231,21 @@
 Projects
 ^^^^^^^^
 
+* Add new
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-projects.html#delete-branches[
+Delete Branches] endpoint.
+
 * Add filtering and pagination options on the
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-projects.html#list-branches[
-list branches] endpoint.
+List Branches] endpoint.
 
 * Add new
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-projects.html#list-tags[
-list tags] endpoint.
+List Tags] endpoint.
 
 * Add new
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-projects.html#get-tag[
-get tag] endpoint.
+Get Tag] endpoint.
 
 
 Configuration
@@ -281,12 +320,20 @@
 ~~~~~~
 
 * Allow to enable the http daemon when running in slave mode.
-
++
 The `--enable-httpd` option can be used in conjunction with the `--slave` option
 to allow clients to fetch from the slave over the http protocol.
++
+HTTP Authentication may also be used when running in slave mode.
 
 * Include the submitter's name in the change message when a change is submitted.
 
+* Add a message to changes created via cherry pick.
++
+When a change is cherry-picked to another branch using the cherry-pick action,
+the message 'Patch Set <number>: Cherry Picked from branch <name>.' is added as
+a change message on the created change.
+
 
 ssh
 ~~~
@@ -410,22 +457,39 @@
 * Sort list of updated changes in output from push.
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2940[Issue 2940]:
-Improve warning messages when Change-Id is missing in the commit message.
+Improve warning messages when `Change-Id` is missing in the commit message.
 
 ** Add a hint to amend the commit after installing the commit-msg hook.
-** Don't show 'Suggestion for commit message' when Change-Id is missing.
+** Don't show 'Suggestion for commit message' when `Change-Id` is missing.
+
+* Allow to publish draft patch sets even when `allowDrafts` is false.
++
+If a user uploaded a change while `allowDrafts` was enabled, and then it was
+disabled by the administrator, the uploaded change could not be published and
+was stuck in the draft state.
+
+
+Authentication
+~~~~~~~~~~~~~~
+
+* Improve LDAP login times and transfer 40x less data.
++
+When recursively expanding LDAP groups, all attributes were fetched. However
+only one of the attributes is actually needed. By fetching only that attribute,
+the amount of data transferred is significantly reduced and the login time is
+decreased.
 
 
 Secondary Index / Search
 ~~~~~~~~~~~~~~~~~~~~~~~~
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2822[Issue 2822]:
-Improve Lucene analysis of words linked with '_' or '.'.
+Improve Lucene analysis of words linked with underscore or dot.
 +
-Instead of treating words linked with '_' or '.' as one word, Lucene now
+Instead of treating words linked with underscore or dot as one word, Lucene now
 treats them as separate words.
 
-* Fix support for change~branch~id in query syntax.
+* Fix support for `change~branch~id` in query syntax.
 
 
 Configuration
@@ -454,6 +518,22 @@
 For changes in the 'Related Changes' tab that are closed the link was
 bringing the user to GitWeb, and not as expected to the change screen.
 
+* link:http://code.google.com/p/gerrit/issues/detail?id=3147[Issue 3147]:
+Allow to disable muting of common path prefixes in the file list.
++
+In the file table, parts of the file path that are common to the file previously
+listed are muted. The purpose of this is to make it easier to see files that all
+belong under the same path, but some users find it annoying.
++
+This feature can now be enabled or disabled, per user, with the 'Mute Common
+Path Prefixes In File List' setting.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=3130[Issue 3130]:
+Remove special handling of 'LGTM' in review comments
++
+Typing 'LGTM' in the review cover message no longer automatically selects the
+highest available Code-Review score.
+
 * Show a confirmation dialog before deleting a draft change or patch set.
 +
 Previously there was no confirmation and a draft change or revision patch
@@ -497,8 +577,8 @@
 +
 When a cherry-pick operation failed with 'Cherry pick failed' error, there was no
 way to know the reason for the failure: merge conflict or the commit is already
-on the target branch.  These failures are now differentiated and a proper error
-is reported to the client.
+on the target branch.  These failures are now differentiated and an appropriate
+error is reported.
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=2837[Issue 2837]:
 Improve display of long user names for collapsed comments in history.
@@ -535,6 +615,18 @@
 If a change from the 'Same Topic' tab was clicked, the selected tab would reset
 to the default tab ('Related Changes').
 
+* Left-align column titles in the file list.
+
+* Increase right margin of download box to make space for scrollbar.
++
+Under some circumstances the browser's scrollbar would be shown over the
+copy-to-clipboard icons in the download dropdown.
+
+* Display +1 score's text next to the checkbox for simple boolean labels.
++
+In the reply box, the text of the label score is displayed on the right hand
+side when a score is selected, but this was missing for simple boolean labels.
+
 
 Side-By-Side Diff
 ^^^^^^^^^^^^^^^^^
@@ -579,6 +671,15 @@
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-changes.html#suggest-reviewers[
 suggest reviewers] endpoint.
 
+* Return correct response from 'delete draft' endpoints.
++
+When the `change.allowDrafts` setting is False, it is not allowed to delete
+draft changes or patch sets.
++
+In this case the response `405 Method Not Allowed` is now returned, instead of
+`409 Conflict`.
+
+
 Projects
 ^^^^^^^^
 
@@ -587,6 +688,33 @@
 link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-projects.html#list-projects[
 list projects] endpoint.
 
+* link:https://code.google.com/p/gerrit/issues/detail?id=2706[Issue 2706]:
+Do not delete branches concurrently.
++
+Deleting multiple branches from the UI was resulting in a server error when
+branches were in the packed-refs.
+
+* Add retry logic for lock failure when deleting a branch.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=3153[Issue 3153]:
+Fix handling of project names ending with `.git`.
++
+The projects REST API documentation states that the `.git` suffix will be
+stripped off the input project name, if present.
++
+This was working for the 'Create Project' endpoint, but not for any of the
+others.
+
+
+Plugins
+~~~~~~~
+
+Replication
+^^^^^^^^^^^
+
+* Create missing repositories on the remote when replicating with the git
+protocol.
+
 
 Upgrades
 --------
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java
index 41df3e6..07d0f50 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java
@@ -42,7 +42,7 @@
     return cfg;
   }
 
-  static private void parseAnnotation(Config cfg, GerritConfig c) {
+  private static void parseAnnotation(Config cfg, GerritConfig c) {
     ArrayList<String> l = Lists.newArrayList(splitter.split(c.name()));
     if (l.size() == 2) {
       cfg.setString(l.get(0), null, l.get(1), c.value());
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestSession.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestSession.java
index 584186c..bf6f928 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestSession.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestSession.java
@@ -16,9 +16,11 @@
 
 import com.google.common.base.Charsets;
 import com.google.common.base.Preconditions;
+import com.google.common.net.HttpHeaders;
 import com.google.gerrit.extensions.restapi.RawInput;
 import com.google.gerrit.server.OutputFormat;
 
+import org.apache.http.Header;
 import org.apache.http.client.methods.HttpDelete;
 import org.apache.http.client.methods.HttpGet;
 import org.apache.http.client.methods.HttpPost;
@@ -41,7 +43,20 @@
 
   @Override
   public RestResponse get(String endPoint) throws IOException {
+    return getWithHeader(endPoint, null);
+  }
+
+  public RestResponse getJsonAccept(String endPoint) throws IOException {
+    return getWithHeader(endPoint,
+        new BasicHeader(HttpHeaders.ACCEPT, "application/json"));
+  }
+
+  private RestResponse getWithHeader(String endPoint, Header header)
+      throws IOException {
     HttpGet get = new HttpGet(url + "/a" + endPoint);
+    if (header != null) {
+      get.addHeader(header);
+    }
     return new RestResponse(getClient().execute(get));
   }
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index a177d00..9cc8f9c 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
+import com.google.gerrit.extensions.api.changes.RevisionApi;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.client.SubmitType;
@@ -181,6 +182,66 @@
   }
 
   @Test
+  public void cherryPickToSameBranch() throws Exception {
+    PushOneCommit.Result r = createChange();
+    CherryPickInput in = new CherryPickInput();
+    in.destination = "master";
+    in.message = "it generates a new patch set\n\nChange-Id: " + r.getChangeId();
+    ChangeInfo cherryInfo = gApi.changes()
+        .id("p~master~" + r.getChangeId())
+        .revision(r.getCommit().name())
+        .cherryPick(in)
+        .get();
+    assertThat((Iterable<?>)cherryInfo.messages).hasSize(2);
+    Iterator<ChangeMessageInfo> cherryIt = cherryInfo.messages.iterator();
+    assertThat(cherryIt.next().message).isEqualTo("Uploaded patch set 1.");
+    assertThat(cherryIt.next().message).isEqualTo("Uploaded patch set 2.");
+  }
+
+  @Test
+  public void cherryPickToSameBranchWithRebase() throws Exception {
+    // Push a new change, then merge it
+    PushOneCommit.Result baseChange = createChange();
+    RevisionApi baseRevision =
+        gApi.changes().id("p~master~" + baseChange.getChangeId()).current();
+    baseRevision.review(ReviewInput.approve());
+    baseRevision.submit();
+
+    // Push a new change (change 1)
+    PushOneCommit.Result r1 = createChange();
+
+    // Push another new change (change 2)
+    String subject = "Test change\n\n" +
+        "Change-Id: Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+    PushOneCommit push =
+        pushFactory.create(db, admin.getIdent(), subject,
+            "another_file.txt", "another content");
+    PushOneCommit.Result r2 = push.to(git, "refs/for/master");
+
+    // Change 2's parent should be change 1
+    assertThat(r2.getCommit().getParents()[0].name())
+      .isEqualTo(r1.getCommit().name());
+
+    // Cherry pick change 2 onto the same branch
+    ChangeApi orig = gApi.changes().id("p~master~" + r2.getChangeId());
+    CherryPickInput in = new CherryPickInput();
+    in.destination = "master";
+    in.message = subject;
+    ChangeApi cherry = orig.revision(r2.getCommit().name()).cherryPick(in);
+    ChangeInfo cherryInfo = cherry.get();
+    assertThat((Iterable<?>)cherryInfo.messages).hasSize(2);
+    Iterator<ChangeMessageInfo> cherryIt = cherryInfo.messages.iterator();
+    assertThat(cherryIt.next().message).isEqualTo("Uploaded patch set 1.");
+    assertThat(cherryIt.next().message).isEqualTo("Uploaded patch set 2.");
+
+    // Parent of change 2 should now be the change that was merged, i.e.
+    // change 2 is rebased onto the head of the master branch.
+    String newParent = cherryInfo.revisions.get(cherryInfo.currentRevision)
+        .commit.parents.get(0).commit;
+    assertThat(newParent).isEqualTo(baseChange.getCommit().name());
+  }
+
+  @Test
   public void cherryPickIdenticalTree() throws Exception {
     PushOneCommit.Result r = createChange();
     CherryPickInput in = new CherryPickInput();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java
index 2640645..e578aad 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java
@@ -53,10 +53,10 @@
 import com.google.gerrit.server.edit.UnchangedCommitMessageException;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gson.stream.JsonReader;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 
-import org.apache.commons.codec.binary.Base64;
 import org.apache.commons.codec.binary.StringUtils;
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.lib.ObjectId;
@@ -77,12 +77,13 @@
 
 public class ChangeEditIT extends AbstractDaemonTest {
 
-  private final static String FILE_NAME = "foo";
-  private final static String FILE_NAME2 = "foo2";
-  private final static String FILE_NAME3 = "foo3";
-  private final static byte[] CONTENT_OLD = "bar".getBytes(UTF_8);
-  private final static byte[] CONTENT_NEW = "baz".getBytes(UTF_8);
-  private final static byte[] CONTENT_NEW2 = "qux".getBytes(UTF_8);
+  private static final String FILE_NAME = "foo";
+  private static final String FILE_NAME2 = "foo2";
+  private static final String FILE_NAME3 = "foo3";
+  private static final byte[] CONTENT_OLD = "bar".getBytes(UTF_8);
+  private static final byte[] CONTENT_NEW = "baz".getBytes(UTF_8);
+  private static final String CONTENT_NEW2_STR = "quxÄÜÖßµ";
+  private static final byte[] CONTENT_NEW2 = CONTENT_NEW2_STR.getBytes(UTF_8);
 
   @Inject
   private SchemaFactory<ReviewDb> reviewDbProvider;
@@ -301,16 +302,14 @@
     assertThat(adminSession.get(urlEditMessage()).getStatusCode())
         .isEqualTo(SC_NOT_FOUND);
     EditMessage.Input in = new EditMessage.Input();
-    in.message = String.format("New commit message\n\nChange-Id: %s",
+    in.message = String.format("New commit message\n\n" +
+        CONTENT_NEW2_STR + "\n\nChange-Id: %s",
         change.getKey());
     assertThat(adminSession.put(urlEditMessage(), in).getStatusCode())
         .isEqualTo(SC_NO_CONTENT);
-    RestResponse r = adminSession.get(urlEditMessage());
-    assertThat(adminSession.get(urlEditMessage()).getStatusCode())
-        .isEqualTo(SC_OK);
-    String content = r.getEntityContent();
-    assertThat(StringUtils.newStringUtf8(Base64.decodeBase64(content)))
-        .isEqualTo(in.message);
+    RestResponse r = adminSession.getJsonAccept(urlEditMessage());
+    assertThat(r.getStatusCode()).isEqualTo(SC_OK);
+    assertThat(readContentFromJson(r)).isEqualTo(in.message);
     Optional<ChangeEdit> edit = editUtil.byChange(change);
     assertThat(edit.get().getEditCommit().getFullMessage())
         .isEqualTo(in.message);
@@ -538,11 +537,10 @@
     assertThat(modifier.modifyFile(edit.get(), FILE_NAME, RestSession.newRawInput(CONTENT_NEW2)))
         .isEqualTo(RefUpdate.Result.FORCED);
     edit = editUtil.byChange(change);
-    RestResponse r = adminSession.get(urlEditFile());
+    RestResponse r = adminSession.getJsonAccept(urlEditFile());
     assertThat(r.getStatusCode()).isEqualTo(SC_OK);
-    String content = r.getEntityContent();
-    assertThat(StringUtils.newStringUtf8(Base64.decodeBase64(content)))
-        .isEqualTo(StringUtils.newStringUtf8(CONTENT_NEW2));
+    assertThat(readContentFromJson(r)).isEqualTo(
+        StringUtils.newStringUtf8(CONTENT_NEW2));
   }
 
   @Test
@@ -718,4 +716,10 @@
     assertThat(r.getStatusCode()).isEqualTo(SC_OK);
     return newGson().fromJson(r.getReader(), EditInfo.class);
   }
+
+  private String readContentFromJson(RestResponse r) throws IOException {
+    JsonReader jsonReader = new JsonReader(r.getReader());
+    jsonReader.setLenient(true);
+    return newGson().fromJson(jsonReader, String.class);
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index 41918e3..66911b8 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -29,8 +29,8 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.SshSession;
-import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.EventListener;
+import com.google.gerrit.common.EventSource;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
@@ -98,13 +98,13 @@
   private IdentifiedUser.GenericFactory factory;
 
   @Inject
-  ChangeHooks hooks;
+  EventSource source;
 
   @Before
   public void setUp() throws Exception {
     mergeResults = Maps.newHashMap();
     CurrentUser listenerUser = factory.create(user.id);
-    hooks.addEventListener(new EventListener() {
+    source.addEventListener(new EventListener() {
 
       @Override
       public void onEvent(Event event) {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/IoUtil.java b/gerrit-common/src/main/java/com/google/gerrit/common/IoUtil.java
index 98beecf..c45d9f9 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/IoUtil.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/IoUtil.java
@@ -46,6 +46,7 @@
           try {
             src.close();
           } catch (IOException e2) {
+            // Ignore
           }
         }
       }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ParameterizedString.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ParameterizedString.java
index 454324b..4ed296f 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ParameterizedString.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ParameterizedString.java
@@ -145,7 +145,7 @@
     }
   }
 
-  private static abstract class Format {
+  private abstract static class Format {
     abstract void format(StringBuilder b, Map<String, String> p);
   }
 
@@ -200,7 +200,7 @@
     }
   }
 
-  private static abstract class Function {
+  private abstract static class Function {
     abstract String apply(String a);
   }
 
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetPublishDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetPublishDetail.java
deleted file mode 100644
index a9b6335..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetPublishDetail.java
+++ /dev/null
@@ -1,79 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common.data;
-
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.client.PatchSetInfo;
-
-import java.util.List;
-
-public class PatchSetPublishDetail {
-  protected AccountInfoCache accounts;
-  protected PatchSetInfo patchSetInfo;
-  protected Change change;
-  protected List<PatchLineComment> drafts;
-  protected List<SubmitRecord> submitRecords;
-  protected SubmitTypeRecord submitTypeRecord;
-  protected boolean canSubmit;
-
-  public void setSubmitTypeRecord(SubmitTypeRecord submitTypeRecord) {
-    this.submitTypeRecord = submitTypeRecord;
-  }
-
-  public SubmitTypeRecord getSubmitTypeRecord() {
-    return submitTypeRecord;
-  }
-
-  public void setAccounts(AccountInfoCache accounts) {
-    this.accounts = accounts;
-  }
-
-  public void setPatchSetInfo(PatchSetInfo patchSetInfo) {
-    this.patchSetInfo = patchSetInfo;
-  }
-
-  public void setChange(Change change) {
-    this.change = change;
-  }
-
-  public void setDrafts(List<PatchLineComment> drafts) {
-    this.drafts = drafts;
-  }
-
-  public void setCanSubmit(boolean allowed) {
-    canSubmit = allowed;
-  }
-
-  public AccountInfoCache getAccounts() {
-    return accounts;
-  }
-
-  public Change getChange() {
-    return change;
-  }
-
-  public PatchSetInfo getPatchSetInfo() {
-    return patchSetInfo;
-  }
-
-  public List<PatchLineComment> getDrafts() {
-    return drafts;
-  }
-
-  public boolean canSubmit() {
-    return canSubmit;
-  }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java
index bd05baf..3ba7adf 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java
@@ -182,10 +182,14 @@
     }
 
     if (canUseRange && (getMin() != 0 || getMax() != 0)) {
-      if (0 <= getMin()) r.append('+');
+      if (0 <= getMin()) {
+        r.append('+');
+      }
       r.append(getMin());
       r.append("..");
-      if (0 <= getMax()) r.append('+');
+      if (0 <= getMax()) {
+        r.append('+');
+      }
       r.append(getMax());
       r.append(' ');
     }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewerInfo.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewerInfo.java
deleted file mode 100644
index 28a8340..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewerInfo.java
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common.data;
-
-/**
- * Suggested reviewer for a change. Can be a user ({@link AccountInfo}) or a
- * group ({@link GroupReference}).
- */
-public class ReviewerInfo implements Comparable<ReviewerInfo> {
-  private AccountInfo accountInfo;
-  private GroupReference groupReference;
-
-  protected ReviewerInfo() {
-  }
-
-  public ReviewerInfo(final AccountInfo accountInfo) {
-    this.accountInfo = accountInfo;
-  }
-
-  public ReviewerInfo(final GroupReference groupReference) {
-    this.groupReference = groupReference;
-  }
-
-  public AccountInfo getAccountInfo() {
-    return accountInfo;
-  }
-
-  public GroupReference getGroup() {
-    return groupReference;
-  }
-
-  @Override
-  public int compareTo(final ReviewerInfo o) {
-    return getSortValue().compareTo(o.getSortValue());
-  }
-
-  private String getSortValue() {
-    if (accountInfo != null) {
-      if (accountInfo.getPreferredEmail() != null) {
-        return accountInfo.getPreferredEmail();
-      }
-      return accountInfo.getFullName();
-    }
-    return groupReference.getName();
-  }
-}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FixInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FixInput.java
index e87be82..c8856e7 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FixInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FixInput.java
@@ -15,4 +15,5 @@
 package com.google.gerrit.extensions.api.changes;
 
 public class FixInput {
+  public boolean deletePatchSetIfCommitMissing;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProblemInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProblemInfo.java
index 369bcda..a117d07 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProblemInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProblemInfo.java
@@ -22,4 +22,15 @@
   public String message;
   public Status status;
   public String outcome;
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder(getClass().getSimpleName())
+        .append('[').append(message);
+    if (status != null || outcome != null) {
+      sb.append(" (").append(status).append(": ").append(outcome)
+          .append(')');
+    }
+    return sb.append(']').toString();
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java
index e264b31..18f356b 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java
@@ -14,11 +14,17 @@
 
 package com.google.gerrit.extensions.restapi;
 
+import java.io.ByteArrayOutputStream;
 import java.io.Closeable;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.CodingErrorAction;
+import java.nio.charset.StandardCharsets;
+import java.nio.charset.UnsupportedCharsetException;
 
 /**
  * Wrapper around a non-JSON result from a {@link RestView}.
@@ -34,13 +40,7 @@
 
   /** Produce a UTF-8 encoded result from a string. */
   public static BinaryResult create(String data) {
-    try {
-      return create(data.getBytes("UTF-8"))
-        .setContentType("text/plain")
-        .setCharacterEncoding("UTF-8");
-    } catch (UnsupportedEncodingException e) {
-      throw new RuntimeException("JVM does not support UTF-8", e);
-    }
+    return new StringResult(data);
   }
 
   /** Produce an {@code application/octet-stream} result from a byte array. */
@@ -144,6 +144,28 @@
    */
   public abstract void writeTo(OutputStream os) throws IOException;
 
+  /**
+   * Return a copy of the result as a String.
+   * <p>
+   * The default version of this method copies the result into a temporary byte
+   * array and then tries to decode it using the configured encoding.
+   *
+   * @return string version of the result.
+   * @throws IOException if the data cannot be produced or could not be
+   *         decoded to a String.
+   */
+  public String asString() throws IOException {
+    long len = getContentLength();
+    ByteArrayOutputStream buf;
+    if (0 < len) {
+      buf = new ByteArrayOutputStream((int) len);
+    } else {
+      buf = new ByteArrayOutputStream();
+    }
+    writeTo(buf);
+    return decode(buf.toByteArray(), getCharacterEncoding());
+  }
+
   /** Close the result and release any resources it holds. */
   @Override
   public void close() throws IOException {
@@ -161,6 +183,25 @@
         getContentType());
   }
 
+  private static String decode(byte[] data, String enc) {
+    try {
+      Charset cs = enc != null
+          ? Charset.forName(enc)
+          : StandardCharsets.UTF_8;
+      return cs.newDecoder()
+        .onMalformedInput(CodingErrorAction.REPORT)
+        .onUnmappableCharacter(CodingErrorAction.REPORT)
+        .decode(ByteBuffer.wrap(data))
+        .toString();
+    } catch (UnsupportedCharsetException | CharacterCodingException e) {
+      // Fallback to ISO-8850-1 style encoding.
+      StringBuilder r = new StringBuilder(data.length);
+      for (byte b : data)
+          r.append((char) (b & 0xff));
+      return r.toString();
+    }
+  }
+
   private static class Array extends BinaryResult {
     private final byte[] data;
 
@@ -173,6 +214,27 @@
     public void writeTo(OutputStream os) throws IOException {
       os.write(data);
     }
+
+    @Override
+    public String asString() {
+      return decode(data, getCharacterEncoding());
+    }
+  }
+
+  private static class StringResult extends Array {
+    private final String str;
+
+    StringResult(String str) {
+      super(str.getBytes(StandardCharsets.UTF_8));
+      setContentType("text/plain");
+      setCharacterEncoding("UTF-8");
+      this.str = str;
+    }
+
+    @Override
+    public String asString() {
+      return str;
+    }
   }
 
   private static class Stream extends BinaryResult {
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/WebLink.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/WebLink.java
index 998638c..e497f7d 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/WebLink.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/WebLink.java
@@ -28,18 +28,18 @@
     /**
      * Opens the link in a new window or tab
      */
-    public final static String BLANK = "_blank";
+    public static final String BLANK = "_blank";
     /**
      * Opens the link in the frame it was clicked.
      */
-    public final static String SELF = "_self";
+    public static final String SELF = "_self";
     /**
      * Opens link in parent frame.
      */
-    public final static String PARENT = "_parent";
+    public static final String PARENT = "_parent";
     /**
      * Opens link in the full body of the window.
      */
-    public final static String TOP = "_top";
+    public static final String TOP = "_top";
   }
 }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpFlowPanel.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpFlowPanel.java
deleted file mode 100644
index 7ae7fd0..0000000
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpFlowPanel.java
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gwtexpui.globalkey.client;
-
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.user.client.ui.FlowPanel;
-
-public class NpFlowPanel extends FlowPanel {
-  public NpFlowPanel() {
-    addDomHandler(GlobalKey.STOP_PROPAGATION, KeyPressEvent.getType());
-  }
-}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java
index 75337ac..69da38d 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java
@@ -402,7 +402,7 @@
     return isElementName(name);
   }
 
-  private static abstract class Impl {
+  private abstract static class Impl {
     abstract void escapeStr(SafeHtmlBuilder b, String in);
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
index 6d94f1d..e2bf142 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
@@ -895,7 +895,7 @@
     return token.substring(prefixlen);
   }
 
-  private static abstract class AsyncSplit implements RunAsyncCallback {
+  private abstract static class AsyncSplit implements RunAsyncCallback {
     private final boolean isReloadUi;
     protected final String token;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
index eb5f29a..c919c6b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
@@ -22,21 +22,14 @@
   String accountDashboard();
   String accountInfoBlock();
   String accountLinkPanel();
-  String accountName();
   String accountPassword();
   String accountUsername();
   String activeRow();
   String addBranch();
   String addMemberTextBox();
-  String addReviewer();
   String addSshKeyPanel();
   String addWatchPanel();
-  String approvalTable();
-  String approvalhint();
-  String approvalrole();
-  String approvalscore();
   String avatarInfoPanel();
-  String blockHeader();
   String bottomheader();
   String branchTablePrevNextLinks();
   String cAPPROVAL();
@@ -46,44 +39,32 @@
   String cSUBJECT();
   String cSTATUS();
   String cellsNextToFileComment();
-  String changeComments();
-  String changeInfoBlock();
-  String changeInfoTopicPanel();
-  String changeScreen();
   String changeScreenDescription();
   String changeScreenStarIcon();
   String changeSize();
   String changeTable();
   String changeTablePrevNextLinks();
   String changeTypeCell();
-  String changeid();
-  String closedstate();
   String commentCell();
   String commentEditorPanel();
   String commentHolder();
   String commentHolderLeftmost();
   String commentPanel();
   String commentPanelAuthorCell();
-  String commentPanelBorder();
   String commentPanelButtons();
   String commentPanelContent();
   String commentPanelDateCell();
   String commentPanelHeader();
   String commentPanelLast();
-  String commentPanelMenuBar();
   String commentPanelMessage();
   String commentPanelSummary();
   String commentPanelSummaryCell();
   String commentedActionDialog();
   String commentedActionMessage();
-  String complexHeader();
-  String content();
   String contributorAgreementAlreadySubmitted();
   String contributorAgreementButton();
   String contributorAgreementLegal();
   String contributorAgreementShortDescription();
-  String coverMessage();
-  String createGroupLink();
   String createProjectPanel();
   String dataCell();
   String dataCellHidden();
@@ -94,7 +75,6 @@
   String diffTextCONTEXT();
   String diffTextDELETE();
   String diffTextFileHeader();
-  String diffTextForBinaryInSideBySide();
   String diffTextHunkHeader();
   String diffTextINSERT();
   String diffTextNoLF();
@@ -109,7 +89,6 @@
   String downloadLinkHeader();
   String downloadLinkHeaderGap();
   String downloadLinkList();
-  String downloadLinkListCell();
   String downloadLink_Active();
   String drafts();
   String editHeadButton();
@@ -118,23 +97,18 @@
   String errorDialogButtons();
   String errorDialogErrorType();
   String errorDialogGlass();
-  String errorDialogText();
   String errorDialogTitle();
   String loadingPluginsDialog();
   String fileColumnHeader();
   String fileCommentBorder();
   String fileLine();
-  String fileLineCONTEXT();
   String fileLineDELETE();
   String fileLineINSERT();
-  String fileLineMode();
-  String fileLineNone();
   String filePathCell();
   String gerritBody();
   String gerritTopMenu();
   String greenCheckClass();
   String groupDescriptionPanel();
-  String groupExternalNameFilterTextBox();
   String groupIncludesTable();
   String groupMembersTable();
   String groupName();
@@ -143,21 +117,16 @@
   String groupOptionsPanel();
   String groupOwnerPanel();
   String groupOwnerTextBox();
-  String groupTypeSelectListBox();
   String groupUUIDPanel();
   String header();
-  String hyperlink();
   String iconCell();
   String iconCellOfFileCommentRow();
   String iconHeader();
   String identityUntrustedExternalId();
   String infoBlock();
-  String infoTable();
   String inputFieldTypeHint();
-  String labelList();
   String labelNotApplicable();
   String leftMostCell();
-  String lineHeader();
   String lineNumber();
   String link();
   String linkMenuBar();
@@ -170,60 +139,38 @@
   String menuBarUserNamePanel();
   String menuItem();
   String menuScreenMenuBar();
-  String missingApproval();
-  String missingApprovalList();
-  String monospace();
   String needsReview();
   String negscore();
-  String noLineLineNumber();
   String noborder();
-  String notVotable();
-  String outdated();
-  String parentsTable();
   String patchBrowserPopup();
   String patchBrowserPopupBody();
   String patchCellReverseDiff();
-  String patchComments();
   String patchContentTable();
   String patchHistoryTable();
   String patchHistoryTablePatchSetHeader();
   String patchNoDifference();
-  String patchScreenDisplayControls();
   String patchSetActions();
-  String patchSetInfoBlock();
-  String patchSetLink();
-  String patchSetRevision();
-  String patchSetUserIdentity();
   String patchSizeCell();
   String pluginProjectConfigInheritedValue();
   String pluginsTable();
   String posscore();
   String projectActions();
-  String projectAdminLabelRangeLine();
-  String projectAdminLabelValue();
   String projectFilterLabel();
   String projectFilterPanel();
   String projectNameColumn();
-  String publishCommentsScreen();
   String registerScreenExplain();
   String registerScreenNextLinks();
   String registerScreenSection();
-  String removeReviewer();
-  String removeReviewerCell();
   String reviewedPanelBottom();
   String rightBorder();
-  String rightmost();
   String rpcStatus();
   String screen();
   String screenHeader();
-  String screenNoHeader();
   String searchPanel();
   String suggestBoxPopup();
   String sectionHeader();
-  String selectPatchSetOldVersion();
   String sideBySideScreenLinkTable();
   String singleLine();
-  String skipLine();
   String smallHeading();
   String sourceFilePath();
   String specialBranchDataCell();
@@ -245,7 +192,6 @@
   String unifiedTable();
   String unifiedTableHeader();
   String userInfoPopup();
-  String useridentity();
   String usernameField();
   String watchedProjectFilter();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
index 5596ace..36cb765 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
@@ -21,7 +21,7 @@
 contextWholeFile = Whole File
 buttonSaveChanges = Save Changes
 showRelativeDateInChangeTable = Show Relative Dates In Changes Table
-showSizeBarInChangeTable = Show Change Sizes As Colored Bars In Changes Table
+showSizeBarInChangeTable = Show Change Sizes As Colored Bars
 showLegacycidInChangeTable = Show Change Number In Changes Table
 muteCommonPathPrefixes = Mute Common Path Prefixes In File List
 myMenu = My Menu
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountInfo.java
index 3ac626c..1127374 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountInfo.java
@@ -56,7 +56,7 @@
   }
 
   public static class AvatarInfo extends JavaScriptObject {
-    public final static int DEFAULT_SIZE = 26;
+    public static final int DEFAULT_SIZE = 26;
     public final native String url() /*-{ return this.url }-*/;
     public final native int height() /*-{ return this.height || 0 }-*/;
     public final native int width() /*-{ return this.width || 0 }-*/;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
index 20ff993..86f543a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
@@ -143,4 +143,7 @@
   String buttonCreateDescription();
   String buttonCreateChange();
   String buttonCreateChangeDescription();
+  String buttonEditConfig();
+  String buttonEditConfigDescription();
+  String editConfigMessage();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
index 26b8123..4446354 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
@@ -167,3 +167,6 @@
 buttonCreateDescription = Insert the description of the change.
 buttonCreateChange = Create Change
 buttonCreateChangeDescription = Create change directly in the browser.
+buttonEditConfig = Edit Config
+buttonEditConfigDescription = Creates a change to edit the project configuration in the browser.
+editConfigMessage = Edit Project Config
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/EditConfigAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/EditConfigAction.java
new file mode 100644
index 0000000..86a31ee
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/EditConfigAction.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.admin;
+
+import com.google.gerrit.client.Dispatcher;
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gwt.user.client.ui.Button;
+
+public class EditConfigAction {
+  static void call(final Button b, final String project) {
+    b.setEnabled(false);
+
+    ChangeApi.createChange(project, RefNames.REFS_CONFIG,
+        Util.C.editConfigMessage(), null, new GerritCallback<ChangeInfo>() {
+          @Override
+          public void onSuccess(ChangeInfo result) {
+            Gerrit.display(Dispatcher.toEditScreen(
+                new PatchSet.Id(result.legacy_id(), 1), "project.config"));
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+            b.setEnabled(true);
+            super.onFailure(caught);
+          }
+        });
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
index e1fb925..6cb9295 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
@@ -581,6 +581,10 @@
     if (showCreateChange) {
       actionsPanel.add(createChangeAction());
     }
+
+    if (isOwner) {
+      actionsPanel.add(createEditConfigAction());
+    }
   }
 
   private Button createChangeAction() {
@@ -596,6 +600,19 @@
     return createChange;
   }
 
+  private Button createEditConfigAction() {
+    final Button editConfig = new Button(Util.C.buttonEditConfig());
+    editConfig.setStyleName("");
+    editConfig.setTitle(Util.C.buttonEditConfigDescription());
+    editConfig.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(ClickEvent event) {
+        EditConfigAction.call(editConfig, getProjectKey().get());
+      }
+    });
+    return editConfig;
+  }
+
   private void doSave() {
     enableForm(false);
     saveProject.setEnabled(false);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
index f5fa6dd..71231e4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
@@ -150,10 +150,6 @@
     submit.setVisible(canSubmit);
   }
 
-  boolean isSubmitEnabled() {
-    return submit.isVisible() && submit.isEnabled();
-  }
-
   @UiHandler("followUp")
   void onFollowUp(@SuppressWarnings("unused") ClickEvent e) {
     if (followUpAction == null) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
index e9fd4ba..8a27fd0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
@@ -191,6 +191,7 @@
   @UiField Button reviewMode;
   @UiField Button addFile;
   @UiField Button deleteFile;
+  @UiField Button renameFile;
   @UiField Button expandAll;
   @UiField Button collapseAll;
   @UiField QuickApprove quickApprove;
@@ -201,6 +202,7 @@
   private DownloadAction downloadAction;
   private AddFileAction addFileAction;
   private DeleteFileAction deleteFileAction;
+  private RenameFileAction renameFileAction;
 
   public ChangeScreen(Change.Id changeId, String base, String revision,
       boolean openReplyBox, FileTable.Mode mode) {
@@ -486,6 +488,7 @@
         editMode.setVisible(fileTableMode == FileTable.Mode.REVIEW);
         addFile.setVisible(!editMode.isVisible());
         deleteFile.setVisible(!editMode.isVisible());
+        renameFile.setVisible(!editMode.isVisible());
         reviewMode.setVisible(!editMode.isVisible());
         addFileAction = new AddFileAction(
             changeId, info.revision(revision),
@@ -493,6 +496,9 @@
         deleteFileAction = new DeleteFileAction(
             changeId, info.revision(revision),
             style, addFile);
+        renameFileAction = new RenameFileAction(
+            changeId, info.revision(revision),
+            style, addFile);
       } else {
         editMode.setVisible(false);
         addFile.setVisible(false);
@@ -670,6 +676,7 @@
     editMode.setVisible(false);
     addFile.setVisible(true);
     deleteFile.setVisible(true);
+    renameFile.setVisible(true);
     reviewMode.setVisible(true);
   }
 
@@ -680,6 +687,7 @@
     editMode.setVisible(true);
     addFile.setVisible(false);
     deleteFile.setVisible(false);
+    renameFile.setVisible(false);
     reviewMode.setVisible(false);
   }
 
@@ -693,6 +701,11 @@
     deleteFileAction.onDelete();
   }
 
+  @UiHandler("renameFile")
+  void onRenameFile(@SuppressWarnings("unused") ClickEvent e) {
+    renameFileAction.onRename();
+  }
+
   private void refreshFileTable() {
     int idx = diffBase.getSelectedIndex();
     if (0 <= idx) {
@@ -770,16 +783,11 @@
     final RevisionInfo b = resolveRevisionOrPatchSetId(info, base, null);
 
     CallbackGroup group = new CallbackGroup();
+    Timestamp lastReply = myLastReply(info);
     if (rev.is_edit()) {
-      NativeMap<JsArray<CommentInfo>> emptyComment = NativeMap.create();
-      files.set(
-          b != null ? new PatchSet.Id(changeId, b._number()) : null,
-          new PatchSet.Id(changeId, rev._number()),
-          style, reply, fileTableMode, edit != null);
-      files.setValue(info.edit().files(), myLastReply(info), emptyComment,
-          emptyComment);
+      loadFileList(b, rev, lastReply, group, null, null);
     } else {
-      loadDiff(b, rev, myLastReply(info), group);
+      loadDiff(b, rev, lastReply, group);
     }
     loadCommit(rev, group);
 
@@ -819,23 +827,7 @@
       final Timestamp myLastReply, CallbackGroup group) {
     final List<NativeMap<JsArray<CommentInfo>>> comments = loadComments(rev, group);
     final List<NativeMap<JsArray<CommentInfo>>> drafts = loadDrafts(rev, group);
-    DiffApi.list(changeId.get(),
-      base != null ? base.name() : null,
-      rev.name(),
-      group.add(new AsyncCallback<NativeMap<FileInfo>>() {
-        @Override
-        public void onSuccess(NativeMap<FileInfo> m) {
-          files.set(
-              base != null ? new PatchSet.Id(changeId, base._number()) : null,
-              new PatchSet.Id(changeId, rev._number()),
-              style, reply, fileTableMode, edit != null);
-          files.setValue(m, myLastReply, comments.get(0), drafts.get(0));
-        }
-
-        @Override
-        public void onFailure(Throwable caught) {
-        }
-      }));
+    loadFileList(base, rev, myLastReply, group, comments, drafts);
 
     if (Gerrit.isSignedIn() && fileTableMode == FileTable.Mode.REVIEW) {
       ChangeApi.revision(changeId.get(), rev.name())
@@ -854,6 +846,31 @@
     }
   }
 
+  private void loadFileList(final RevisionInfo base, final RevisionInfo rev,
+      final Timestamp myLastReply, CallbackGroup group,
+      final List<NativeMap<JsArray<CommentInfo>>> comments,
+      final List<NativeMap<JsArray<CommentInfo>>> drafts) {
+    DiffApi.list(changeId.get(),
+      base != null ? base.name() : null,
+      rev.name(),
+      group.add(new AsyncCallback<NativeMap<FileInfo>>() {
+        @Override
+        public void onSuccess(NativeMap<FileInfo> m) {
+          files.set(
+              base != null ? new PatchSet.Id(changeId, base._number()) : null,
+              new PatchSet.Id(changeId, rev._number()),
+              style, reply, fileTableMode, edit != null);
+          files.setValue(m, myLastReply,
+              comments != null ? comments.get(0) : null,
+              drafts != null ? drafts.get(0) : null);
+        }
+
+        @Override
+        public void onFailure(Throwable caught) {
+        }
+      }));
+  }
+
   private List<NativeMap<JsArray<CommentInfo>>> loadComments(
       RevisionInfo rev, CallbackGroup group) {
     final int id = rev._number();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml
index 280455a..6f8d3aa 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml
@@ -496,6 +496,13 @@
         <ui:attribute name='title'/>
         <div><ui:msg>Delete&#8230;</ui:msg></div>
       </g:Button>
+      <g:Button ui:field='renameFile'
+         title='Rename file in the repository'
+         styleName=''
+         visible='false'>
+        <ui:attribute name='title'/>
+        <div><ui:msg>Rename&#8230;</ui:msg></div>
+      </g:Button>
       <div class='{style.headerButtons}'>
         <g:Button ui:field='openAll'
             styleName=''
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 5dab3ac..fc62c46 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
@@ -29,6 +29,7 @@
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.client.ui.NavigationTable;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.Patch.ChangeType;
@@ -202,8 +203,8 @@
 
   void setValue(NativeMap<FileInfo> fileMap,
       Timestamp myLastReply,
-      NativeMap<JsArray<CommentInfo>> comments,
-      NativeMap<JsArray<CommentInfo>> drafts) {
+      @Nullable NativeMap<JsArray<CommentInfo>> comments,
+      @Nullable NativeMap<JsArray<CommentInfo>> drafts) {
     JsArray<FileInfo> list = fileMap.values();
     FileInfo.sortFileInfoByPath(list);
 
@@ -441,6 +442,7 @@
     private final NativeMap<JsArray<CommentInfo>> comments;
     private final NativeMap<JsArray<CommentInfo>> drafts;
     private final boolean hasUser;
+    private final boolean showChangeSizeBars;
     private boolean attached;
     private int row;
     private double start;
@@ -453,14 +455,16 @@
     private DisplayCommand(NativeMap<FileInfo> map,
         JsArray<FileInfo> list,
         Timestamp myLastReply,
-        NativeMap<JsArray<CommentInfo>> comments,
-        NativeMap<JsArray<CommentInfo>> drafts) {
+        @Nullable NativeMap<JsArray<CommentInfo>> comments,
+        @Nullable NativeMap<JsArray<CommentInfo>> drafts) {
       this.myTable = new MyTable(map, list);
       this.list = list;
       this.myLastReply = myLastReply;
       this.comments = comments;
       this.drafts = drafts;
       this.hasUser = Gerrit.isSignedIn();
+      this.showChangeSizeBars = !hasUser
+          || Gerrit.getUserAccount().getGeneralPreferences().isSizeBarInChangeTable();
       myTable.addStyleName(R.css().table());
     }
 
@@ -707,7 +711,10 @@
     }
 
     private JsArray<CommentInfo> get(String p, NativeMap<JsArray<CommentInfo>> m) {
-      JsArray<CommentInfo> r =  m.get(p);
+      JsArray<CommentInfo> r = null;
+      if (m != null) {
+        r = m.get(p);
+      }
       if (r == null) {
         r = JsArray.createArray().cast();
       }
@@ -717,14 +724,27 @@
     private void columnDelta1(SafeHtmlBuilder sb, FileInfo info) {
       sb.openTd().setStyleName(R.css().deltaColumn1());
       if (!Patch.COMMIT_MSG.equals(info.path()) && !info.binary()) {
-        sb.append(info.lines_inserted() + info.lines_deleted());
+        if (showChangeSizeBars) {
+          sb.append(info.lines_inserted() + info.lines_deleted());
+        } else if (!ChangeType.DELETED.matches(info.status())) {
+          if (ChangeType.ADDED.matches(info.status())) {
+            sb.append(info.lines_inserted())
+              .append(" lines");
+          } else {
+            sb.append("+")
+              .append(info.lines_inserted())
+              .append(", -")
+              .append(info.lines_deleted());
+          }
+        }
       }
       sb.closeTd();
     }
 
     private void columnDelta2(SafeHtmlBuilder sb, FileInfo info) {
       sb.openTd().setStyleName(R.css().deltaColumn2());
-      if (!Patch.COMMIT_MSG.equals(info.path()) && !info.binary()
+      if (showChangeSizeBars
+          && !Patch.COMMIT_MSG.equals(info.path()) && !info.binary()
           && (info.lines_inserted() != 0 || info.lines_deleted() != 0)) {
         int w = 80;
         int t = inserted + deleted;
@@ -771,26 +791,28 @@
 
       // delta2
       sb.openTh().setStyleName(R.css().deltaColumn2());
-      int w = 80;
-      int t = inserted + deleted;
-      int i = Math.max(1, (int) (((double) w) * inserted / t));
-      int d = Math.max(1, (int) (((double) w) * deleted / t));
-      if (i + d > w && i > d) {
-        i = w - d;
-      } else if (i + d > w && d > i) {
-        d = w - i;
-      }
-      if (0 < inserted) {
-        sb.openDiv()
-        .setStyleName(R.css().inserted())
-        .setAttribute("style", "width:" + i + "px")
-        .closeDiv();
-      }
-      if (0 < deleted) {
-        sb.openDiv()
-          .setStyleName(R.css().deleted())
-          .setAttribute("style", "width:" + d + "px")
+      if (showChangeSizeBars) {
+        int w = 80;
+        int t = inserted + deleted;
+        int i = Math.max(1, (int) (((double) w) * inserted / t));
+        int d = Math.max(1, (int) (((double) w) * deleted / t));
+        if (i + d > w && i > d) {
+          i = w - d;
+        } else if (i + d > w && d > i) {
+          d = w - i;
+        }
+        if (0 < inserted) {
+          sb.openDiv()
+          .setStyleName(R.css().inserted())
+          .setAttribute("style", "width:" + i + "px")
           .closeDiv();
+        }
+        if (0 < deleted) {
+          sb.openDiv()
+            .setStyleName(R.css().deleted())
+            .setAttribute("style", "width:" + d + "px")
+            .closeDiv();
+        }
       }
       sb.closeTh();
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.java
index 072b78e..faba10d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.java
@@ -169,7 +169,7 @@
           .openElement("button")
           .setAttribute("title", "Remove hashtag")
           .setAttribute("onclick", REMOVE + "(event)")
-          .append("☒")
+          .append("×")
           .closeElement("button")
           .closeSpan();
       if (itr.hasNext()) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java
index 849512b..a416894 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java
@@ -273,7 +273,7 @@
         html.openElement("button")
             .setAttribute("title", Util.M.removeReviewer(name))
             .setAttribute("onclick", REMOVE + "(event)")
-            .append("☒")
+            .append("×")
             .closeElement("button");
       }
       html.closeSpan();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileAction.java
new file mode 100644
index 0000000..1f11e65
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileAction.java
@@ -0,0 +1,71 @@
+//Copyright (C) 2015 The Android Open Source Project
+//
+//Licensed under the Apache License, Version 2.0 (the "License");
+//you may not use this file except in compliance with the License.
+//You may obtain a copy of the License at
+//
+//http://www.apache.org/licenses/LICENSE-2.0
+//
+//Unless required by applicable law or agreed to in writing, software
+//distributed under the License is distributed on an "AS IS" BASIS,
+//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//See the License for the specific language governing permissions and
+//limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.user.client.PluginSafePopupPanel;
+
+class RenameFileAction {
+  private final Change.Id changeId;
+  private final RevisionInfo revision;
+  private final ChangeScreen.Style style;
+  private final Widget renameButton;
+
+  private RenameFileBox renameBox;
+  private PopupPanel popup;
+
+  RenameFileAction(Change.Id changeId, RevisionInfo revision,
+      ChangeScreen.Style style, Widget renameButton) {
+    this.changeId = changeId;
+    this.revision = revision;
+    this.style = style;
+    this.renameButton = renameButton;
+  }
+
+  void onRename() {
+    if (popup != null) {
+      popup.hide();
+      return;
+    }
+
+    if (renameBox == null) {
+      renameBox = new RenameFileBox(changeId, revision);
+    }
+    renameBox.clearPath();
+
+    final PluginSafePopupPanel p = new PluginSafePopupPanel(true);
+    p.setStyleName(style.replyBox());
+    p.addAutoHidePartner(renameButton.getElement());
+    p.addCloseHandler(new CloseHandler<PopupPanel>() {
+      @Override
+      public void onClose(CloseEvent<PopupPanel> event) {
+        if (popup == p) {
+          popup = null;
+        }
+      }
+    });
+    p.add(renameBox);
+    p.showRelativeTo(renameButton);
+    GlobalKey.dialog(p);
+    renameBox.setFocus(true);
+    popup = p;
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileBox.java
new file mode 100644
index 0000000..77348f7
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileBox.java
@@ -0,0 +1,107 @@
+//Copyright (C) 2015 The Android Open Source Project
+//
+//Licensed under the Apache License, Version 2.0 (the "License");
+//you may not use this file except in compliance with the License.
+//You may obtain a copy of the License at
+//
+//http://www.apache.org/licenses/LICENSE-2.0
+//
+//Unless required by applicable law or agreed to in writing, software
+//distributed under the License is distributed on an "AS IS" BASIS,
+//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//See the License for the specific language governing permissions and
+//limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.VoidResult;
+import com.google.gerrit.client.changes.ChangeEditApi;
+import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.ui.RemoteSuggestBox;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwtexpui.globalkey.client.NpTextBox;
+
+class RenameFileBox extends Composite {
+  interface Binder extends UiBinder<HTMLPanel, RenameFileBox> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  private final Change.Id changeId;
+
+  @UiField Button rename;
+  @UiField Button cancel;
+
+  @UiField(provided = true)
+  RemoteSuggestBox path;
+  @UiField NpTextBox newPath;
+
+  RenameFileBox(Change.Id changeId, RevisionInfo revision) {
+    this.changeId = changeId;
+
+    path = new RemoteSuggestBox(new PathSuggestOracle(changeId, revision));
+    path.addCloseHandler(new CloseHandler<RemoteSuggestBox>() {
+      @Override
+      public void onClose(CloseEvent<RemoteSuggestBox> event) {
+        hide();
+      }
+    });
+
+    initWidget(uiBinder.createAndBindUi(this));
+  }
+
+  void setFocus(boolean focus) {
+    path.setFocus(focus);
+  }
+
+  void clearPath() {
+    path.setText("");
+  }
+
+  @UiHandler("rename")
+  void onRename(@SuppressWarnings("unused") ClickEvent e) {
+    rename(path.getText(), newPath.getText());
+  }
+
+  private void rename(String path, String newPath) {
+    hide();
+    ChangeEditApi.rename(changeId.get(), path, newPath,
+        new AsyncCallback<VoidResult>() {
+          @Override
+          public void onSuccess(VoidResult result) {
+            Gerrit.display(PageLinks.toChangeInEditMode(changeId));
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+          }
+        });
+  }
+
+  @UiHandler("cancel")
+  void onCancel(@SuppressWarnings("unused") ClickEvent e) {
+    hide();
+  }
+
+  private void hide() {
+    for (Widget w = getParent(); w != null; w = w.getParent()) {
+      if (w instanceof PopupPanel) {
+        ((PopupPanel) w).hide();
+        break;
+      }
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileBox.ui.xml
new file mode 100644
index 0000000..27849ee
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileBox.ui.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2013 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<ui:UiBinder
+    xmlns:ui='urn:ui:com.google.gwt.uibinder'
+    xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'
+    xmlns:u='urn:import:com.google.gerrit.client.ui'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+  <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
+  <ui:style>
+    .cancel { float: right; }
+  </ui:style>
+  <g:HTMLPanel>
+    <div class='{res.style.section}'>
+      <ui:msg>Old: <u:RemoteSuggestBox ui:field='path' visibleLength='86'/></ui:msg>
+    </div>
+    <div class='{res.style.section}'>
+      <ui:msg>New: <c:NpTextBox ui:field='newPath' visibleLength='86'/></ui:msg>
+    </div>
+    <div class='{res.style.section}'>
+      <g:Button ui:field='rename'
+          title='Rename file in the repository'
+          styleName='{res.style.button}'>
+        <ui:attribute name='title'/>
+        <div><ui:msg>Rename</ui:msg></div>
+      </g:Button>
+      <g:Button ui:field='cancel'
+          styleName='{res.style.button}'
+          addStyleNames='{style.cancel}'>
+          <div>Cancel</div>
+      </g:Button>
+    </div>
+  </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
index fd9f4bc..f9054fe 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
@@ -351,7 +351,6 @@
     final String id = lv.info.name();
     final CheckBox b = new CheckBox();
     b.setText(id);
-    b.setTitle(lv.info.value_text("+1"));
     b.setEnabled(lv.permitted.contains((short) 1));
     if (self != null && self.value() == 1) {
       b.setValue(true);
@@ -364,6 +363,10 @@
     });
     b.setStyleName(style.label_name());
     labelsTable.setWidget(row, 0, b);
+
+    CellFormatter fmt = labelsTable.getCellFormatter();
+    fmt.setStyleName(row, labelHelpColumn, style.label_help());
+    labelsTable.setText(row, labelHelpColumn, lv.info.value_text("+1"));
   }
 
   private static boolean isCheckBox(Set<Short> values) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css
index 53729e7..2803db3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css
@@ -25,6 +25,10 @@
   width: 20px;
 }
 
+.table th {
+  vertical-align: top;
+  text-align: left;
+}
 .table tr {
   vertical-align: top;
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeEditApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeEditApi.java
index 22204dc..a00e329 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeEditApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeEditApi.java
@@ -72,6 +72,15 @@
     editFile(id, path).delete(cb);
   }
 
+  /** Rename a file in the pending edit. */
+  public static void rename(int id, String path, String newPath,
+      AsyncCallback<VoidResult> cb) {
+    Input in = Input.create();
+    in.old_path(path);
+    in.new_path(newPath);
+    ChangeApi.edit(id).post(in, cb);
+  }
+
   /** Restore (undo delete/modify) a file in the pending edit. */
   public static void restore(int id, String path, AsyncCallback<VoidResult> cb) {
     Input in = Input.create();
@@ -93,6 +102,8 @@
     }
 
     final native void restore_path(String p) /*-{ this.restore_path=p }-*/;
+    final native void old_path(String p) /*-{ this.old_path=p }-*/;
+    final native void new_path(String p) /*-{ this.new_path=p }-*/;
 
     protected Input() {
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
index 1047cf0..5cf7d62 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
@@ -130,7 +130,7 @@
     super.onLoad();
 
     CallbackGroup group1 = new CallbackGroup();
-    CallbackGroup group2 = new CallbackGroup();
+    final CallbackGroup group2 = new CallbackGroup();
     final CallbackGroup group3 = new CallbackGroup();
 
     CodeMirror.initLibrary(group1.add(new AsyncCallback<Void>() {
@@ -140,6 +140,9 @@
       public void onSuccess(Void result) {
         // Load theme after CM library to ensure theme can override CSS.
         ThemeLoader.loadTheme(prefs.theme(), themeCallback);
+
+        group2.done();
+        group3.done();
       }
 
       @Override
@@ -229,8 +232,6 @@
       }
     });
     group1.done();
-    group2.done();
-    group3.done();
   }
 
   @Override
@@ -402,7 +403,7 @@
     InlineHyperlink unified = new InlineHyperlink();
     unified.setHTML(new ImageResourceRenderer()
         .render(Gerrit.RESOURCES.unifiedDiff()));
-    sbs.setTargetHistoryToken(
+    unified.setTargetHistoryToken(
         Dispatcher.toPatch("unified", base, new Patch.Key(revision, path)));
     unified.setTitle(PatchUtil.C.unifiedDiff());
     linkPanel.add(unified);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
index ea84f45..0d0f793 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
@@ -84,10 +84,6 @@
   color: black;
 }
 
-.hyperlink {
-  text-decoration: underline;
-}
-
 .accountLinkPanel {
   display: inline;
 }
@@ -105,10 +101,6 @@
   top: -1px;
 }
 
-.accountName {
-  white-space: nowrap;
-}
-
 .inputFieldTypeHint {
   color: grey;
 }
@@ -118,13 +110,6 @@
   font-weight: bold;
 }
 
-.blockHeader {
-  font-size: small;
-  font-weight: bold;
-  background: trimColor;
-  padding: 0.2em 0.2em 0.2em 0.5em;
-}
-
 .link {
   cursor: pointer;
 }
@@ -156,47 +141,6 @@
 }
 
 /** CommentPanel **/
-.commentPanelBorder {
-  border-top: 1px solid lightgray;
-  border-left: 1px solid lightgray;
-  border-right: 1px solid lightgray;
-  border-top-left-radius: 8px;
-  border-top-right-radius: 8px;
-  border-bottom-left-radius: 0px;
-  border-bottom-right-radius: 0px;
-}
-.commentPanelBorder.commentPanelLast {
-  border-bottom: 1px solid lightgray;
-  border-bottom-left-radius: 8px;
-  border-bottom-right-radius: 8px;
-}
-
-@if user.agent safari {
-  .commentPanelBorder {
-    \-webkit-border-top-left-radius: 8px;
-    \-webkit-border-top-right-radius: 8px;
-    \-webkit-border-bottom-left-radius: 0px;
-    \-webkit-border-bottom-right-radius: 0px;
-  }
-  .commentPanelBorder.commentPanelLast {
-    \-webkit-border-bottom-left-radius: 8px;
-    \-webkit-border-bottom-right-radius: 8px;
-  }
-}
-
-@if user.agent gecko1_8 {
-  .commentPanelBorder {
-    \-moz-border-radius-topleft: 8px;
-    \-moz-border-radius-topright: 8px;
-    \-moz-border-radius-bottomleft: 0;
-    \-moz-border-radius-bottomright: 0;
-  }
-  .commentPanelBorder.commentPanelLast {
-    \-moz-border-radius-bottomleft: 8px;
-    \-moz-border-radius-bottomright: 8px;
-  }
-}
-
 .commentPanelHeader {
   cursor: pointer;
   width: 100%;
@@ -224,9 +168,6 @@
   padding-left: 0.5em;
   padding-right: 0.5em;
 }
-.commentPanelMenuBar {
-  float: right;
-}
 .commentPanelMessage p {
   margin-top: 0px;
   margin-bottom: 0px;
@@ -428,10 +369,6 @@
   width: 100%;
   margin-top: 15px;
 }
-.errorDialogText {
-  font-size: 15px;
-  font-family: verdana;
-}
 .errorDialog a,
 .errorDialog a:visited,
 .errorDialog a:hover {
@@ -459,10 +396,6 @@
   overflow: hidden;
 }
 
-.screenNoHeader {
-  margin-top: 5px;
-}
-
 /** ChangeTable **/
 .changeTable {
   border-collapse: separate;
@@ -477,10 +410,6 @@
   background: tableOddRowColor;
 }
 
-.changeTable .outdated {
-  background: changeTableOutdatedColor !important;
-}
-
 .changeTable .iconCell {
   width: 1px;
   padding: 0px;
@@ -675,10 +604,6 @@
 
 
 /** PatchScreen **/
-.patchScreenDisplayControls .gwt-CheckBox {
-  margin-right: 1em;
-}
-
 .reviewedPanelBottom {
   float: right;
   font-size: small;
@@ -716,10 +641,6 @@
   border-left: thin solid #b0bdcc;
 }
 
-.patchContentTable .diffTextForBinaryInSideBySide {
- width: 50%;
-}
-
 .patchContentTable .diffTextFileHeader {
   color: grey;
   font-weight: bold;
@@ -787,9 +708,6 @@
   background: white;
   border-bottom: 1px solid white;
 }
-.lineNumber.rightmost {
-  border-left: thin solid #b0bdcc;
-}
 .lineNumber.rightBorder {
   border-right: thin solid #b0bdcc;
 }
@@ -809,14 +727,6 @@
 .lineNumber.fileColumnHeader {
   border-bottom: 1px solid trimColor;
 }
-.noLineLineNumber {
-  font-family: mono-font;
-  width: 3.5em;
-  padding-left: 0.2em;
-  padding-right: 0.2em;
-  background: white;
-  border-bottom: 1px solid white;
-}
 
 .fileLine {
   padding-left: 0;
@@ -824,22 +734,11 @@
   white-space: pre;
   border-left: thin solid #b0bdcc;
 }
-.fileLineNone {
-  background: #eeeeee;
-  border-bottom: 1px solid #eeeeee;
-}
-.fileLineMode {
-  font-weight: bold;
-}
 .fileLineDELETE,
 .fileLineDELETE .wdc {
   background: #ffeeee;
   border-bottom: 1px solid #ffeeee;
 }
-.fileLineCONTEXT {
-  background: white;
-  border-bottom: 1px solid white;
-}
 .fileLineINSERT,
 .fileLineINSERT .wdc {
   background: #ddffdd;
@@ -852,27 +751,6 @@
   border-bottom: 1px solid #9F9;
 }
 
-.patchContentTable .skipLine .iconCell,
-.patchContentTable .skipLine {
-  font-family: norm-font;
-  text-align: center;
-  font-style: italic;
-  background: #def;
-  color: grey;
-}
-.patchContentTable .skipLine div {
-  display: inline;
-}
-.patchContentTable a.skipLine {
-  color: grey;
-  text-decoration: none;
-}
-.patchContentTable a:hover.skipLine {
-  background: white;
-  color: #00A;
-  text-decoration: underline;
-}
-
 .patchContentTable td.cellsNextToFileComment {
   background: trimColor;
   border-top: trimColor;
@@ -908,36 +786,6 @@
   margin-right: 5px;
 }
 
-.changeScreen .gwt-DisclosurePanel .header td {
-  font-weight: bold;
-  white-space: nowrap;
-}
-
-.changeScreen .gwt-DisclosurePanel .complexHeader {
-  white-space: nowrap;
-}
-.changeScreen .gwt-DisclosurePanel .complexHeader span {
-  white-space: nowrap;
-}
-
-.patchSetRevision {
-  padding-left: 20px;
-  font-size: 8pt;
-}
-
-.patchSetLink {
-  padding-left: 0.5em;
-  font-size: 8pt;
-}
-
-.changeScreen .gwt-DisclosurePanel .content {
-  margin-bottom: 10px;
-}
-
-.gwt-DisclosurePanel .content {
-  margin-left: 10px;
-}
-
 .changeScreenDescription,
 .changeScreenDescription textarea {
   white-space: pre;
@@ -949,74 +797,6 @@
   padding-top: 0.5em;
 }
 
-.changeComments {
-  padding-top: 1em;
-  width: 60em;
-}
-
-.infoTable {
-  border-collapse: collapse;
-  border-spacing: 0;
-}
-
-.infoTable td {
-  border-left: 1px solid trimColor;
-  border-bottom: 1px solid trimColor;
-  padding: 2px 6px 1px;
-}
-
-.infoTable td.header {
-  background-color: trimColor;
-  font-weight: normal;
-  padding: 2px 4px 0 6px;
-  font-style: italic;
-  text-align: left;
-  vertical-align: top;
-  white-space: nowrap;
-}
-
-.rightmost  {
-  border-right: 1px solid trimColor;
-}
-
-.infoTable td.approvalrole {
-  width: 5em;
-  border-left: none;
-  font-style: italic;
-  white-space: nowrap;
-}
-
-.infoTable td.approvalscore {
-  text-align: center;
-}
-.infoTable td.notVotable {
-  background: #F5F5F5;
-}
-.infoTable td.negscore {
-  color: red;
-}
-.infoTable td.posscore {
-  color: #08a400;
-}
-
-.infoTable td.approvalhint {
-  white-space: nowrap;
-  text-align: left;
-  color: #444444;
-}
-
-.changeInfoBlock {
-  margin-right: 15px;
-}
-
-.changeInfoTopicPanel img {
-  float: right;
-}
-
-.changeInfoTopicPanel a {
-  float: left;
-}
-
 .avatarInfoPanel {
   margin-right: 10px;
 }
@@ -1060,27 +840,6 @@
   border-bottom: 1px solid trimColor;
 }
 
-.infoBlock td.closedstate {
-  font-weight: bold;
-}
-
-.infoBlock td.useridentity {
-  white-space: nowrap;
-}
-.changeInfoBlock td.changeid {
-  font-size: x-small;
-}
-
-
-.patchSetInfoBlock {
-  margin-bottom: 10px;
-}
-.patchSetUserIdentity {
-  white-space: nowrap;
-}
-.patchSetUserIdentity .gwt-InlineLabel {
-  margin-left: 0.2em;
-}
 
 .patchSetActions {
   margin-bottom: 10px;
@@ -1090,42 +849,10 @@
   font-size: 8pt;
 }
 
-.selectPatchSetOldVersion {
-  font-weight: bold;
-  margin-right: 30px;
-  margin-top: 5px;
-}
-
-.approvalTable {
-  margin-top: 1em;
-  margin-bottom: 1em;
-}
-.missingApprovalList {
-  margin-top: 5px;
-  margin-left: 1em;
-  padding-left: 1em;
-  margin-bottom: 0px;
-}
-.missingApproval {
-  font-size: small;
-  white-space: nowrap;
-}
-.addReviewer {
-  margin-left: 1em;
-  margin-top: 5px;
-  white-space: nowrap;
-}
-.removeReviewer {
-  padding: 0px;
-}
-td.removeReviewerCell {
-  padding-left: 4em;
-  border-left: none;
-}
-
 .downloadBox {
   min-width: 580px;
   margin: 5px;
+  margin-right: 15px;
 }
 .downloadBoxTable {
   border-spacing: 0;
@@ -1167,9 +894,6 @@
 .downloadBoxCopyLabel div {
   float: right;
 }
-td.downloadLinkListCell {
-  padding: 0px;
-}
 .downloadLinkHeader {
   background: trimColor;
   white-space: nowrap;
@@ -1208,27 +932,6 @@
   width: 30em;
 }
 
-.parentsTable {
-  border-style: none;
-  outline: 0px;
-  padding: 0px;
-  border-spacing: 0px;
-  text-align: left;
-  font-family: mono-font;
-  font-size: 10px;
-}
-
-.parentsTable td.noborder {
-  border: none;
-}
-
-.parentsTable td.monospace {
-  font-family: mono-font;
-  font-size: 10px;
-  margin: 0px;
-  padding-left: 0px;
-}
-
 /** UnifiedScreen **/
 .unifiedTable {
   width: 100%;
@@ -1347,10 +1050,6 @@
   width: 100%;
 }
 
-.createGroupLink {
-  margin-bottom: 10px;
-}
-
 .createProjectPanel {
   margin-bottom: 10px;
   background-color: trimColor;
@@ -1436,66 +1135,10 @@
   width: 45em;
 }
 
-.projectAdminLabelRangeLine {
-  white-space: nowrap;
-}
-.projectAdminLabelValue {
-  font-family: mono-font;
-  font-size: small;
-}
 .projectActions {
   margin-bottom: 10px;
 }
 
-/** PublishCommentsScreen **/
-.publishCommentsScreen .smallHeading {
-  font-size: small;
-  font-weight: bold;
-  white-space: nowrap;
-}
-.publishCommentsScreen .labelList {
-  margin-bottom: 10px;
-  margin-left: 10px;
-  background: trimColor;
-  width: 25em;
-  white-space: nowrap;
-  padding-top: 2px;
-  padding-bottom: 2px;
-}
-.publishCommentsScreen .coverMessage {
-  margin-left: 10px;
-  padding: 5px 5px 5px 5px;
-}
-.publishCommentsScreen .coverMessage textarea {
-  font-size: small;
-}
-.publishCommentsScreen .labelList .gwt-RadioButton {
-  font-size: smaller;
-}
-.publishCommentsScreen .patchComments {
-  margin-left: 1em;
-  margin-right: 0.5em;
-  margin-bottom: 0.5em;
-}
-.publishCommentsScreen .gwt-Hyperlink {
-  white-space: nowrap;
-  font-size: small;
-}
-.publishCommentsScreen .lineHeader {
-  white-space: nowrap;
-  font-family: mono-font;
-  font-size: small;
-  font-style: italic;
-  padding-left: 3px;
-}
-.publishCommentsScreen .commentPanel {
-  border: none;
-  width: 35em;
-}
-.publishCommentsScreen .commentPanelDateCell {
-  display: none;
-}
-
 
 /** CommentedActionDialog **/
 .commentedActionDialog .gwt-DisclosurePanel .header td {
@@ -1538,10 +1181,6 @@
 .groupDescriptionPanel {
   margin-bottom: 3px;
 }
-.groupExternalNameFilterTextBox {
-  margin-right: 2px;
-  margin-bottom: 2px;
-}
 .groupNamePanel {
   margin-bottom: 3px;
 }
@@ -1557,9 +1196,6 @@
 .groupOwnerTextBox {
   margin-bottom: 2px;
 }
-.groupTypeSelectListBox {
-  margin-bottom: 2px;
-}
 
 
 /** AccountGroupMembersScreen **/
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTable.java
index 218a6c3..178a583 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTable.java
@@ -261,8 +261,9 @@
 
   private static boolean isUnifiedPatchLink(final Patch patch) {
     return (patch.getPatchType().equals(PatchType.BINARY)
-        || Gerrit.getUserAccount().getGeneralPreferences().getDiffView()
-        .equals(DiffView.UNIFIED_DIFF));
+        || (Gerrit.isSignedIn()
+            && Gerrit.getUserAccount().getGeneralPreferences().getDiffView()
+            .equals(DiffView.UNIFIED_DIFF)));
   }
 
   private static String getFileNameOnly(Patch patch) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java
index e87853b..e48477f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java
@@ -45,6 +45,7 @@
   private static final String JSON_TYPE = "application/json";
   private static final String JSON_UTF8 = JSON_TYPE + "; charset=utf-8";
   private static final String TEXT_TYPE = "text/plain";
+  private static final String TEXT_UTF8 = TEXT_TYPE + "; charset=utf-8";
 
   /**
    * Expected JSON content body prefix that prevents XSSI.
@@ -129,9 +130,14 @@
         final String type;
         if (isJsonBody(res)) {
           try {
-            // javac generics bug
-            data = RestApi.<T> cast(parseJson(res));
-            type = JSON_TYPE;
+            JSONValue val = parseJson(res);
+            if (isJsonEncoded(res) && val.isString() != null) {
+              data = NativeString.wrap(val.isString().stringValue()).cast();
+              type = simpleType(res.getHeader("X-FYI-Content-Type"));
+            } else {
+              data = RestApi.<T> cast(val);
+              type = JSON_TYPE;
+            }
           } catch (JSONException e) {
             if (!background) {
               RpcStatus.INSTANCE.onRpcComplete();
@@ -140,9 +146,6 @@
                 "Invalid JSON: " + e.getMessage()));
             return;
           }
-        } else if (isEncodedBase64(res)) {
-          data = NativeString.wrap(decodeBase64(res.getText())).cast();
-          type = simpleType(res.getHeader("X-FYI-Content-Type"));
         } else if (isTextBody(res)) {
           data = NativeString.wrap(res.getText()).cast();
           type = TEXT_TYPE;
@@ -371,7 +374,7 @@
 
   public <T extends JavaScriptObject> void post(String content,
       HttpCallback<T> cb) {
-    sendRaw(POST, content, cb);
+    sendText(POST, content, cb);
   }
 
   public <T extends JavaScriptObject> void put(AsyncCallback<T> cb) {
@@ -389,7 +392,7 @@
 
   public <T extends JavaScriptObject> void put(String content,
       HttpCallback<T> cb) {
-    sendRaw(PUT, content, cb);
+    sendText(PUT, content, cb);
   }
 
   public <T extends JavaScriptObject> void put(
@@ -423,10 +426,7 @@
   private static native String str(JavaScriptObject jso)
   /*-{ return JSON.stringify(jso) }-*/;
 
-  private static native String decodeBase64(String a)
-  /*-{ return $wnd.atob(a) }-*/;
-
-  private <T extends JavaScriptObject> void sendRaw(Method method, String body,
+  private <T extends JavaScriptObject> void sendText(Method method, String body,
       HttpCallback<T> cb) {
     HttpImpl<T> httpCallback = new HttpImpl<>(background, cb);
     try {
@@ -434,7 +434,7 @@
         RpcStatus.INSTANCE.onRpcStart();
       }
       RequestBuilder req = request(method);
-      req.setHeader("Content-Type", TEXT_TYPE);
+      req.setHeader("Content-Type", TEXT_UTF8);
       req.sendRequest(body, httpCallback);
     } catch (RequestException e) {
       httpCallback.onError(null, e);
@@ -461,9 +461,8 @@
     return isContentType(res, TEXT_TYPE);
   }
 
-  private static boolean isEncodedBase64(Response res) {
-    return "base64".equals(res.getHeader("X-FYI-Content-Encoding"))
-        && isTextBody(res);
+  private static boolean isJsonEncoded(Response res) {
+    return "json".equals(res.getHeader("X-FYI-Content-Encoding"));
   }
 
   private static boolean isContentType(Response res, String want) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ComplexDisclosurePanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ComplexDisclosurePanel.java
index eb62809..4cc6a16 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ComplexDisclosurePanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ComplexDisclosurePanel.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.client.ui;
 
-import com.google.gerrit.client.Gerrit;
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.event.logical.shared.CloseHandler;
 import com.google.gwt.event.logical.shared.HasCloseHandlers;
@@ -56,7 +55,6 @@
       {
         setElement((Element)(DOM.createTD()));
         getElement().setInnerHTML("&nbsp;");
-        addStyleName(Gerrit.RESOURCES.css().complexHeader());
       }
 
       @Override
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 a7b53cf..dd36495 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
@@ -91,6 +91,7 @@
 import com.google.gson.JsonPrimitive;
 import com.google.gson.stream.JsonReader;
 import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.JsonWriter;
 import com.google.gson.stream.MalformedJsonException;
 import com.google.gwtexpui.server.CacheHeaders;
 import com.google.inject.Inject;
@@ -719,6 +720,7 @@
     }
   }
 
+  @SuppressWarnings("resource")
   static void replyBinaryResult(
       @Nullable HttpServletRequest req,
       HttpServletResponse res,
@@ -731,7 +733,11 @@
             "attachment; filename=\"" + bin.getAttachmentName() + "\"");
       }
       if (bin.isBase64()) {
-        bin = stackBase64(res, bin);
+        if (req != null && JSON_TYPE.equals(req.getHeader(HttpHeaders.ACCEPT))) {
+          bin = stackJsonString(res, bin);
+        } else {
+          bin = stackBase64(res, bin);
+        }
       }
       if (bin.canGzip() && acceptsGzip(req)) {
         bin = stackGzip(res, bin);
@@ -758,6 +764,24 @@
     }
   }
 
+  private static BinaryResult stackJsonString(HttpServletResponse res,
+      final BinaryResult src) throws IOException {
+    TemporaryBuffer.Heap buf = heap(Integer.MAX_VALUE);
+    buf.write(JSON_MAGIC);
+    try(Writer w = new BufferedWriter(new OutputStreamWriter(buf, UTF_8));
+        JsonWriter json = new JsonWriter(w)) {
+      json.setLenient(true);
+      json.setHtmlSafe(true);
+      json.value(src.asString());
+      w.write('\n');
+    }
+    res.setHeader("X-FYI-Content-Encoding", "json");
+    res.setHeader("X-FYI-Content-Type", src.getContentType());
+    return asBinaryResult(buf)
+      .setContentType(JSON_TYPE)
+      .setCharacterEncoding(UTF_8.name());
+  }
+
   private static BinaryResult stackBase64(HttpServletResponse res,
       final BinaryResult src) throws IOException {
     BinaryResult b64;
diff --git a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
index 474b284..4ee9676 100644
--- a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -294,8 +294,8 @@
     return name;
   }
 
-  private volatile static File myArchive;
-  private volatile static File myHome;
+  private static volatile File myArchive;
+  private static volatile File myHome;
 
   /**
    * Locate the JAR/WAR file we were launched from.
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/Plugin.java b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/Plugin.java
index 4248e54..bf19352 100644
--- a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/Plugin.java
+++ b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/Plugin.java
@@ -96,7 +96,7 @@
 
   native void _initialized() /*-{ this._success = true }-*/;
   native void _loaded() /*-{ this._loadedGwt() }-*/;
-  private static native final Plugin install(String u)
+  private static final native Plugin install(String u)
   /*-{ return $wnd.Gerrit.installGwt(u) }-*/;
 
   private static final native JavaScriptObject wrap(Screen.EntryPoint b) /*-{
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/rpc/RestApi.java b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/rpc/RestApi.java
index cfbc8a5..8b2a6b8 100644
--- a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/rpc/RestApi.java
+++ b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/rpc/RestApi.java
@@ -109,7 +109,7 @@
     get(NativeString.unwrap(cb));
   }
 
-  private native static void get(String p, JavaScriptObject r)
+  private static native void get(String p, JavaScriptObject r)
   /*-{ $wnd.Gerrit.get(p, r) }-*/;
 
   public <T extends JavaScriptObject>
@@ -117,7 +117,7 @@
     put(path(), wrap(cb));
   }
 
-  private native static void put(String p, JavaScriptObject r)
+  private static native void put(String p, JavaScriptObject r)
   /*-{ $wnd.Gerrit.put(p, r) }-*/;
 
   public <T extends JavaScriptObject>
@@ -125,7 +125,7 @@
     put(path(), content, wrap(cb));
   }
 
-  private native static
+  private static native
   void put(String p, String c, JavaScriptObject r)
   /*-{ $wnd.Gerrit.put(p, c, r) }-*/;
 
@@ -134,7 +134,7 @@
     put(path(), content, wrap(cb));
   }
 
-  private native static
+  private static native
   void put(String p, JavaScriptObject c, JavaScriptObject r)
   /*-{ $wnd.Gerrit.put(p, c, r) }-*/;
 
@@ -143,7 +143,7 @@
     post(path(), content, wrap(cb));
   }
 
-  private native static
+  private static native
   void post(String p, String c, JavaScriptObject r)
   /*-{ $wnd.Gerrit.post(p, c, r) }-*/;
 
@@ -152,7 +152,7 @@
     post(path(), content, wrap(cb));
   }
 
-  private native static
+  private static native
   void post(String p, JavaScriptObject c, JavaScriptObject r)
   /*-{ $wnd.Gerrit.post(p, c, r) }-*/;
 
@@ -160,10 +160,10 @@
     delete(path(), wrap(cb));
   }
 
-  private native static void delete(String p, JavaScriptObject r)
+  private static native void delete(String p, JavaScriptObject r)
   /*-{ $wnd.Gerrit.del(p, r) }-*/;
 
-  private native static <T extends JavaScriptObject>
+  private static native <T extends JavaScriptObject>
   JavaScriptObject wrap(AsyncCallback<T> b) /*-{
     return function(r) {
       b.@com.google.gwt.user.client.rpc.AsyncCallback::onSuccess(Ljava/lang/Object;)(r)
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java
index 668ebed5..cdf800c 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java
@@ -28,7 +28,7 @@
 import java.util.Set;
 
 public abstract class PrettyFormatter implements SparseHtmlFile {
-  public static abstract class EditFilter {
+  public abstract static class EditFilter {
     abstract String getStyleName();
 
     abstract int getBegin(Edit edit);
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
index 4c0bb69..fe2cd96 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
@@ -598,4 +598,14 @@
   public void setTopic(String topic) {
     this.topic = topic;
   }
+
+  @Override
+  public String toString() {
+    return new StringBuilder(getClass().getSimpleName())
+        .append('{').append(changeId)
+        .append(" (").append(changeKey).append("), ")
+        .append("dest=").append(dest).append(", ")
+        .append("status=").append(status).append('}')
+        .toString();
+  }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/InheritedBoolean.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/InheritedBoolean.java
deleted file mode 100644
index ce9b0f2..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/InheritedBoolean.java
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.reviewdb.client;
-
-import com.google.gerrit.extensions.client.InheritableBoolean;
-
-public class InheritedBoolean {
-
-  public InheritableBoolean value;
-  public boolean inheritedValue;
-
-  public InheritedBoolean() {
-  }
-
-  public void setValue(final InheritableBoolean value) {
-    this.value = value;
-  }
-
-  public void setInheritedValue(final boolean inheritedValue) {
-    this.inheritedValue = inheritedValue;
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
index 90efc48..086a6d0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
@@ -86,7 +86,8 @@
 
 /** Spawns local executables when a hook action occurs. */
 @Singleton
-public class ChangeHookRunner implements ChangeHooks, LifecycleListener {
+public class ChangeHookRunner implements ChangeHooks, EventDispatcher,
+  EventSource, LifecycleListener {
     /** A logger for this class. */
     private static final Logger log = LoggerFactory.getLogger(ChangeHookRunner.class);
 
@@ -95,6 +96,8 @@
       protected void configure() {
         bind(ChangeHookRunner.class);
         bind(ChangeHooks.class).to(ChangeHookRunner.class);
+        bind(EventDispatcher.class).to(ChangeHookRunner.class);
+        bind(EventSource.class).to(ChangeHookRunner.class);
         listener().to(ChangeHookRunner.class);
       }
     }
@@ -924,8 +927,7 @@
           while ((line = br.readLine()) != null) {
             log.info("hook[" + getName() + "] output: " + line);
           }
-        }
-        catch(IOException  iox) {
+        } catch (IOException iox) {
           log.error("Error writing hook output", iox);
         }
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
index ec8e39d..7f7e8b2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
@@ -22,8 +22,6 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.events.Event;
 import com.google.gwtorm.server.OrmException;
 
 import org.eclipse.jgit.lib.ObjectId;
@@ -34,10 +32,6 @@
 
 /** Invokes hooks on server actions. */
 public interface ChangeHooks {
-  public void addEventListener(EventListener listener, CurrentUser user);
-
-  public void removeEventListener(EventListener listener);
-
   /**
    * Fire the Patchset Created Hook.
    *
@@ -187,23 +181,4 @@
   public void doHashtagsChangedHook(Change change, Account account,
       Set<String>added, Set<String> removed, Set<String> hashtags,
       ReviewDb db) throws OrmException;
-
-  /**
-   * Post a stream event that is related to a change
-   *
-   * @param change The change that the event is related to
-   * @param event The event to post
-   * @param db The database
-   * @throws OrmException
-   */
-  public void postEvent(Change change, Event event, ReviewDb db)
-      throws OrmException;
-
-  /**
-   * Post a stream event that is related to a branch
-   *
-   * @param branchName The branch that the event is related to
-   * @param event The event to post
-   */
-  public void postEvent(Branch.NameKey branchName, Event event);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java b/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
index bd3ed13..156672e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
@@ -32,7 +32,8 @@
 import java.util.Set;
 
 /** Does not invoke hooks. */
-public final class DisabledChangeHooks implements ChangeHooks {
+public final class DisabledChangeHooks implements ChangeHooks, EventDispatcher,
+    EventSource {
   @Override
   public void addEventListener(EventListener listener, CurrentUser user) {
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/EventDispatcher.java b/gerrit-server/src/main/java/com/google/gerrit/common/EventDispatcher.java
new file mode 100644
index 0000000..b74771f8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/EventDispatcher.java
@@ -0,0 +1,44 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.common;
+
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.events.Event;
+import com.google.gwtorm.server.OrmException;
+
+
+/** Interface for posting (dispatching) Events */
+public interface EventDispatcher {
+  /**
+   * Post a stream event that is related to a change
+   *
+   * @param change The change that the event is related to
+   * @param event The event to post
+   * @param db The database
+   * @throws OrmException
+   */
+  public void postEvent(Change change, Event event, ReviewDb db)
+      throws OrmException;
+
+  /**
+   * Post a stream event that is related to a branch
+   *
+   * @param branchName The branch that the event is related to
+   * @param event The event to post
+   */
+  public void postEvent(Branch.NameKey branchName, Event event);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/EventSource.java b/gerrit-server/src/main/java/com/google/gerrit/common/EventSource.java
new file mode 100644
index 0000000..e2c4b34
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/EventSource.java
@@ -0,0 +1,24 @@
+// 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.common;
+
+import com.google.gerrit.server.CurrentUser;
+
+/** Distributes Events to ChangeListeners.  Register listeners here. */
+public interface EventSource {
+  public void addEventListener(EventListener listener, CurrentUser user);
+
+  public void removeEventListener(EventListener listener);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
index 6f5618b..ef28ed8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.account.CapabilityControl;
@@ -39,8 +40,9 @@
     InternalUser create();
   }
 
+  @VisibleForTesting
   @Inject
-  protected InternalUser(CapabilityControl.Factory capabilityControlFactory) {
+  public InternalUser(CapabilityControl.Factory capabilityControlFactory) {
     super(capabilityControlFactory);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/TimestampHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/TimestampHandler.java
index 8dd4270..21ef31a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/TimestampHandler.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/TimestampHandler.java
@@ -31,7 +31,7 @@
 import java.util.TimeZone;
 
 public class TimestampHandler extends OptionHandler<Timestamp> {
-  public final static String TIMESTAMP_FORMAT = "yyyyMMdd_HHmm";
+  public static final String TIMESTAMP_FORMAT = "yyyyMMdd_HHmm";
 
   @Inject
   public TimestampHandler(@Assisted CmdLineParser parser,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
index 7197269..ad1e160 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
@@ -17,6 +17,7 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.base.Function;
 import com.google.common.collect.Collections2;
+import com.google.common.collect.Lists;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.Ordering;
@@ -28,7 +29,12 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
@@ -39,7 +45,9 @@
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -49,6 +57,7 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
@@ -63,7 +72,7 @@
       LoggerFactory.getLogger(ConsistencyChecker.class);
 
   @AutoValue
-  public static abstract class Result {
+  public abstract static class Result {
     private static Result create(Change.Id id, List<ProblemInfo> problems) {
       return new AutoValue_ConsistencyChecker_Result(id, null, problems);
     }
@@ -82,6 +91,9 @@
 
   private final Provider<ReviewDb> db;
   private final GitRepositoryManager repoManager;
+  private final Provider<CurrentUser> user;
+  private final Provider<PersonIdent> serverIdent;
+  private final PatchSetInfoFactory patchSetInfoFactory;
 
   private FixInput fix;
   private Change change;
@@ -95,9 +107,15 @@
 
   @Inject
   ConsistencyChecker(Provider<ReviewDb> db,
-      GitRepositoryManager repoManager) {
+      GitRepositoryManager repoManager,
+      Provider<CurrentUser> user,
+      @GerritPersonIdent Provider<PersonIdent> serverIdent,
+      PatchSetInfoFactory patchSetInfoFactory) {
     this.db = db;
     this.repoManager = repoManager;
+    this.user = user;
+    this.serverIdent = serverIdent;
+    this.patchSetInfoFactory = patchSetInfoFactory;
     reset();
   }
 
@@ -192,26 +210,36 @@
     }
   }
 
+  private static final Function<PatchSet, Integer> TO_PS_ID =
+      new Function<PatchSet, Integer>() {
+        @Override
+        public Integer apply(PatchSet in) {
+          return in.getId().get();
+        }
+      };
+
+  private static final Ordering<PatchSet> PS_ID_ORDER = Ordering.natural()
+    .onResultOf(TO_PS_ID);
+
   private boolean checkPatchSets() {
     List<PatchSet> all;
     try {
-      all = db.get().patchSets().byChange(change.getId()).toList();
+      all = Lists.newArrayList(db.get().patchSets().byChange(change.getId()));
     } catch (OrmException e) {
       return error("Failed to look up patch sets", e);
     }
-    Function<PatchSet, Integer> toPsId = new Function<PatchSet, Integer>() {
-      @Override
-      public Integer apply(PatchSet in) {
-        return in.getId().get();
-      }
-    };
+    // Iterate in descending order so deletePatchSet can assume the latest patch
+    // set exists.
+    Collections.sort(all, PS_ID_ORDER.reverse());
     Multimap<ObjectId, PatchSet> bySha = MultimapBuilder.hashKeys(all.size())
-        .treeSetValues(Ordering.natural().onResultOf(toPsId))
+        .treeSetValues(PS_ID_ORDER)
         .build();
     for (PatchSet ps : all) {
+      // Check revision format.
       ObjectId objId;
       String rev = ps.getRevision().get();
       int psNum = ps.getId().get();
+      String refName = ps.getId().toRefName();
       try {
         objId = ObjectId.fromString(rev);
       } catch (IllegalArgumentException e) {
@@ -221,22 +249,48 @@
       }
       bySha.put(objId, ps);
 
+      // Check ref existence.
+      ProblemInfo refProblem = null;
+      try {
+        Ref ref = repo.getRef(refName);
+        if (ref == null) {
+          refProblem = problem("Ref missing: " + refName);
+        } else if (!objId.equals(ref.getObjectId())) {
+          String actual = ref.getObjectId() != null
+              ? ref.getObjectId().name()
+              : "null";
+          refProblem = problem(String.format(
+              "Expected %s to point to %s, found %s",
+              ref.getName(), objId.name(), actual));
+        }
+      } catch (IOException e) {
+        error("Error reading ref: " + refName, e);
+        refProblem = lastProblem();
+      }
+
+      // Check object existence.
       RevCommit psCommit = parseCommit(
           objId, String.format("patch set %d", psNum));
       if (psCommit == null) {
+        if (fix != null && fix.deletePatchSetIfCommitMissing) {
+          deletePatchSet(lastProblem(), ps.getId());
+        }
         continue;
+      } else if (refProblem != null && fix != null) {
+        fixPatchSetRef(refProblem, ps);
       }
       if (ps.getId().equals(change.currentPatchSetId())) {
         currPsCommit = psCommit;
       }
     }
 
+    // Check for duplicates.
     for (Map.Entry<ObjectId, Collection<PatchSet>> e
         : bySha.asMap().entrySet()) {
       if (e.getValue().size() > 1) {
         problem(String.format("Multiple patch sets pointing to %s: %s",
             e.getKey().name(),
-            Collections2.transform(e.getValue(), toPsId)));
+            Collections2.transform(e.getValue(), TO_PS_ID)));
       }
     }
 
@@ -305,6 +359,104 @@
     }
   }
 
+  private void fixPatchSetRef(ProblemInfo p, PatchSet ps) {
+    try {
+      RefUpdate ru = repo.updateRef(ps.getId().toRefName());
+      ru.setForceUpdate(true);
+      ru.setNewObjectId(ObjectId.fromString(ps.getRevision().get()));
+      ru.setRefLogIdent(newRefLogIdent());
+      ru.setRefLogMessage("Repair patch set ref", true);
+      RefUpdate.Result result = ru.update();
+      switch (result) {
+        case NEW:
+        case FORCED:
+        case FAST_FORWARD:
+        case NO_CHANGE:
+          p.status = Status.FIXED;
+          p.outcome = "Repaired patch set ref";
+          return;
+        default:
+          p.status = Status.FIX_FAILED;
+          p.outcome = "Failed to update patch set ref: " + result;
+          return;
+      }
+    } catch (IOException e) {
+      String msg = "Error fixing patch set ref";
+      log.warn(msg + ' ' + ps.getId().toRefName(), e);
+      p.status = Status.FIX_FAILED;
+      p.outcome = msg;
+    }
+  }
+
+  private void deletePatchSet(ProblemInfo p, PatchSet.Id psId) {
+    ReviewDb db = this.db.get();
+    Change.Id cid = psId.getParentKey();
+    try {
+      db.changes().beginTransaction(cid);
+      try {
+        Change c = db.changes().get(cid);
+        if (c == null) {
+          throw new OrmException("Change missing: " + cid);
+        }
+
+        if (psId.equals(c.currentPatchSetId())) {
+          List<PatchSet> all = Lists.newArrayList(db.patchSets().byChange(cid));
+          if (all.size() == 1 && all.get(0).getId().equals(psId)) {
+            p.status = Status.FIX_FAILED;
+            p.outcome = "Cannot delete patch set; no patch sets would remain";
+            return;
+          }
+          // If there were multiple missing patch sets, assumes deletePatchSet
+          // has been called in decreasing order, so the max remaining PatchSet
+          // is the effective current patch set.
+          Collections.sort(all, PS_ID_ORDER.reverse());
+          PatchSet.Id latest = null;
+          for (PatchSet ps : all) {
+            latest = ps.getId();
+            if (!ps.getId().equals(psId)) {
+              break;
+            }
+          }
+          c.setCurrentPatchSet(patchSetInfoFactory.get(db, latest));
+          db.changes().update(Collections.singleton(c));
+        }
+
+        // Delete dangling primary key references. Don't delete ChangeMessages,
+        // which don't use patch sets as a primary key, and may provide useful
+        // historical information.
+        db.accountPatchReviews().delete(
+            db.accountPatchReviews().byPatchSet(psId));
+        db.patchSetAncestors().delete(
+            db.patchSetAncestors().byPatchSet(psId));
+        db.patchSetApprovals().delete(
+            db.patchSetApprovals().byPatchSet(psId));
+        db.patchComments().delete(
+            db.patchComments().byPatchSet(psId));
+        db.patchSets().deleteKeys(Collections.singleton(psId));
+        db.commit();
+
+        p.status = Status.FIXED;
+        p.outcome = "Deleted patch set";
+      } finally {
+        db.rollback();
+      }
+    } catch (PatchSetInfoNotAvailableException | OrmException e) {
+      String msg = "Error deleting patch set";
+      log.warn(msg + ' ' + psId, e);
+      p.status = Status.FIX_FAILED;
+      p.outcome = msg;
+    }
+  }
+
+  private PersonIdent newRefLogIdent() {
+    CurrentUser u = user.get();
+    if (u.isIdentifiedUser()) {
+      return ((IdentifiedUser) u).newRefLogIdent();
+    } else {
+      return serverIdent.get();
+    }
+  }
+
   private RevCommit parseCommit(ObjectId objId, String desc) {
     try {
       return rw.parseCommit(objId);
@@ -325,6 +477,10 @@
     return p;
   }
 
+  private ProblemInfo lastProblem() {
+    return problems.get(problems.size() - 1);
+  }
+
   private boolean error(String msg, Throwable t) {
     problem(msg);
     // TODO(dborowitz): Expose stack trace to administrators.
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 f731b5f..81f3b9b 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
@@ -70,7 +70,7 @@
 import java.util.concurrent.TimeUnit;
 
 public class GetDiff implements RestReadView<FileResource> {
-  private final static ImmutableMap<Patch.ChangeType, ChangeType> CHANGE_TYPE =
+  private static final ImmutableMap<Patch.ChangeType, ChangeType> CHANGE_TYPE =
       Maps.immutableEnumMap(
           new ImmutableMap.Builder<Patch.ChangeType, ChangeType>()
               .put(Patch.ChangeType.ADDED, ChangeType.ADDED)
@@ -339,11 +339,13 @@
         int lastB = 0;
         for (Edit edit : internalEdit) {
           if (edit.getBeginA() != edit.getEndA()) {
-            e.editA.add(ImmutableList.of(edit.getBeginA() - lastA, edit.getEndA() - edit.getBeginA()));
+            e.editA.add(ImmutableList.of(
+                edit.getBeginA() - lastA, edit.getEndA() - edit.getBeginA()));
             lastA = edit.getEndA();
           }
           if (edit.getBeginB() != edit.getEndB()) {
-            e.editB.add(ImmutableList.of(edit.getBeginB() - lastB, edit.getEndB() - edit.getBeginB()));
+            e.editB.add(ImmutableList.of(
+                edit.getBeginB() - lastB, edit.getEndB() - edit.getBeginB()));
             lastB = edit.getEndB();
           }
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestionCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestionCache.java
index 83fa938..554d760 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestionCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestionCache.java
@@ -34,8 +34,10 @@
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 /**
- * The suggest oracle may be called many times in rapid succession during the course of one operation.
- * It would be easy to have a simple Cache<Boolean, List<Account>> with a short expiration time of 30s.
+ * The suggest oracle may be called many times in rapid succession during the
+ * course of one operation.
+ * It would be easy to have a simple Cache<Boolean, List<Account>> with a short
+ * expiration time of 30s.
  * Cache only has a single key we're just using Cache for the expiration behavior.
  */
 @Singleton
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
index 6891fc7..6e9b3f2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
@@ -537,17 +537,23 @@
                 continue;
 
               case REJECT:
-                if (msg.length() > 0) msg.append("; ");
+                if (msg.length() > 0) {
+                  msg.append("; ");
+                }
                 msg.append("blocked by ").append(lbl.label);
                 continue;
 
               case NEED:
-                if (msg.length() > 0) msg.append("; ");
+                if (msg.length() > 0) {
+                  msg.append("; ");
+                }
                 msg.append("needs ").append(lbl.label);
                 continue;
 
               case IMPOSSIBLE:
-                if (msg.length() > 0) msg.append("; ");
+                if (msg.length() > 0) {
+                  msg.append("; ");
+                }
                 msg.append("needs ").append(lbl.label)
                    .append(" (check project access)");
                 continue;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
index 9d7c54a..fbff7c4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
@@ -100,12 +100,13 @@
 
     if (site_path.exists()) {
       final String[] contents = site_path.list();
-      if (contents != null)
+      if (contents != null) {
         isNew = contents.length == 0;
-      else if (site_path.isDirectory())
+      } else if (site_path.isDirectory()) {
         throw new FileNotFoundException("Cannot access " + site_path);
-      else
+      } else {
         throw new FileNotFoundException("Not a directory: " + site_path);
+      }
     } else {
       isNew = true;
     }
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 29dcb17..8843dbb 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
@@ -21,15 +21,19 @@
 import org.eclipse.jgit.transport.ReceiveCommand;
 
 public class CommitReceivedEvent extends RefEvent {
-  public final ReceiveCommand command;
-  public final Project project;
-  public final String refName;
-  public final RevCommit commit;
-  public final IdentifiedUser user;
+  public ReceiveCommand command;
+  public Project project;
+  public String refName;
+  public RevCommit commit;
+  public IdentifiedUser user;
+
+  public CommitReceivedEvent() {
+    super("commit-received");
+  }
 
   public CommitReceivedEvent(ReceiveCommand command, Project project,
       String refName, RevCommit commit, IdentifiedUser user) {
-    super("commit-received");
+    this();
     this.command = command;
     this.project = project;
     this.refName = refName;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventDeserializer.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventDeserializer.java
new file mode 100644
index 0000000..3508acf
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventDeserializer.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.events;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+
+import java.lang.reflect.Type;
+
+/**
+ * JSON deserializer for {@link Event}s.
+ * <p>
+ * Deserialized objects are of an appropriate subclass based on the value of the
+ * top-level "type" element.
+ */
+public class EventDeserializer implements JsonDeserializer<Event> {
+  @Override
+  public Event deserialize(JsonElement json, Type typeOfT,
+      JsonDeserializationContext context) throws JsonParseException {
+    if (!json.isJsonObject()) {
+      throw new JsonParseException("Not an object");
+    }
+    JsonElement typeJson = json.getAsJsonObject().get("type");
+    if (typeJson == null || !typeJson.isJsonPrimitive()
+        || !typeJson.getAsJsonPrimitive().isString()) {
+      throw new JsonParseException("Type is not a string: " + typeJson);
+    }
+    String type = typeJson.getAsJsonPrimitive().getAsString();
+    Class<?> cls = EventTypes.getClass(type);
+    if (cls == null) {
+      throw new JsonParseException("Unknown event type: " + type);
+    }
+    return context.deserialize(json, cls);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventTypes.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventTypes.java
new file mode 100644
index 0000000..4d26e13
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventTypes.java
@@ -0,0 +1,64 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.events;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/** Class for registering event types */
+public class EventTypes {
+  private static final Map<String, Class<?>> typesByString = new HashMap<>();
+
+  static {
+    registerClass(new ChangeAbandonedEvent());
+    registerClass(new ChangeMergedEvent());
+    registerClass(new ChangeRestoredEvent());
+    registerClass(new CommentAddedEvent());
+    registerClass(new CommitReceivedEvent());
+    registerClass(new DraftPublishedEvent());
+    registerClass(new HashtagsChangedEvent());
+    registerClass(new MergeFailedEvent());
+    registerClass(new RefUpdatedEvent());
+    registerClass(new RefReceivedEvent());
+    registerClass(new ReviewerAddedEvent());
+    registerClass(new PatchSetCreatedEvent());
+    registerClass(new TopicChangedEvent());
+  }
+
+  /** Register an event.
+   *
+   *  @param event The event to register.
+   *  @throws IllegalArgumentException if the event's type is already
+   *  registered.
+   **/
+  public static void registerClass(Event event) {
+    String type = event.getType();
+    if (typesByString.containsKey(type)) {
+      throw new IllegalArgumentException(
+          "Event type already registered: " + type);
+    }
+    typesByString.put(type, event.getClass());
+  }
+
+  /** Get the class for an event type.
+   *
+   * @param type The type.
+   * @return The event class, or null if no class is registered with the
+   * given type
+   **/
+  public static Class<?> getClass(String type) {
+    return typesByString.get(type);
+  }
+}
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 8acbcd3..1f0f6648 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
@@ -494,6 +494,7 @@
 
   private ListMultimap<SubmitType, Change> validateChangeList(
       List<ChangeData> submitted) throws MergeException {
+    logDebug("Validating {} changes", submitted.size());
     ListMultimap<SubmitType, Change> toSubmit = ArrayListMultimap.create();
 
     Map<String, Ref> allRefs;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java
index 872179d..557faeb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java
@@ -32,7 +32,7 @@
  */
 public abstract class FieldDef<I, T> {
   /** Definition of a single (non-repeatable) field. */
-  public static abstract class Single<I, T> extends FieldDef<I, T> {
+  public abstract static class Single<I, T> extends FieldDef<I, T> {
     Single(String name, FieldType<T> type, boolean stored) {
       super(name, type, stored);
     }
@@ -44,7 +44,7 @@
   }
 
   /** Definition of a repeatable field. */
-  public static abstract class Repeatable<I, T>
+  public abstract static class Repeatable<I, T>
       extends FieldDef<I, Iterable<T>> {
     Repeatable(String name, FieldType<T> type, boolean stored) {
       super(name, type, stored);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
index 01fa6b1..fbca668 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
@@ -182,12 +182,12 @@
   }
 
   /** Writes commit to a BatchMetaDataUpdate without committing the batch. */
-  abstract public void writeCommit(BatchMetaDataUpdate batch)
+  public abstract void writeCommit(BatchMetaDataUpdate batch)
       throws OrmException, IOException;
 
   /**
    * @return the NameKey for the project where the update will be stored,
    *    which is not necessarily the same as the change's project.
    */
-  abstract protected Project.NameKey getProjectName();
+  protected abstract Project.NameKey getProjectName();
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java
index fb39462..1053f09 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java
@@ -220,7 +220,7 @@
         && event.psId.equals(update.getPatchSetId());
   }
 
-  private static abstract class Event implements Comparable<Event> {
+  private abstract static class Event implements Comparable<Event> {
     final PatchSet.Id psId;
     final Account.Id who;
     final Timestamp when;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
index 4961bef..dd67c20 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
@@ -178,10 +178,11 @@
               int nb = lf + 1;
               int p = 0;
               while (p < ae - ab) {
-                if (cmp.equals(a, ab + p, a, ab + p))
+                if (cmp.equals(a, ab + p, a, ab + p)) {
                   p++;
-                else
+                } else {
                   break;
+                }
               }
               if (p == ae - ab) {
                 ab = nb;
@@ -214,10 +215,11 @@
               int nb = lf + 1;
               int p = 0;
               while (p < be - bb) {
-                if (cmp.equals(b, bb + p, b, bb + p))
+                if (cmp.equals(b, bb + p, b, bb + p)) {
                   p++;
-                else
+                } else {
                   break;
+                }
               }
               if (p == be - bb) {
                 bb = nb;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java
index 7d44912..4fff619 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java
@@ -150,12 +150,13 @@
     while (low < high) {
       final int mid = (low + high) >>> 1;
       final int cmp = patches[mid].getNewName().compareTo(fileName);
-      if (cmp < 0)
+      if (cmp < 0) {
         low = mid + 1;
-      else if (cmp == 0)
+      } else if (cmp == 0) {
         return mid;
-      else
+      } else {
         high = mid;
+      }
     }
     return -(low + 1);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarPluginProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarPluginProvider.java
index 7c35014..53f39f1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarPluginProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarPluginProvider.java
@@ -142,9 +142,10 @@
           new URLClassLoader(urls.toArray(new URL[urls.size()]),
               PluginLoader.parentFor(type));
 
+      JarScanner jarScanner = createJarScanner(srcJar);
       ServerPlugin plugin =
           new ServerPlugin(name, description.canonicalUrl, description.user,
-              srcJar, snapshot, new JarScanner(srcJar), description.dataDir,
+              srcJar, snapshot, jarScanner, description.dataDir,
               pluginLoader);
       plugin.setCleanupHandle(new CleanupHandle(tmp, jarFile));
       keep = true;
@@ -155,4 +156,13 @@
       }
     }
   }
+
+  private JarScanner createJarScanner(File srcJar)
+      throws InvalidPluginException {
+    try {
+      return new JarScanner(srcJar);
+    } catch (IOException e) {
+      throw new InvalidPluginException("Cannot scan plugin file " + srcJar, e);
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java
index d7d0efd..df4ead8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java
@@ -43,9 +43,11 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.lang.annotation.Annotation;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Enumeration;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.jar.Attributes;
@@ -67,12 +69,8 @@
 
   private final JarFile jarFile;
 
-  public JarScanner(File srcFile) throws InvalidPluginException {
-    try {
-      this.jarFile = new JarFile(srcFile);
-    } catch (IOException e) {
-      throw new InvalidPluginException("Cannot scan plugin file " + srcFile, e);
-    }
+  public JarScanner(File srcFile) throws IOException {
+    this.jarFile = new JarFile(srcFile);
   }
 
   @Override
@@ -136,6 +134,36 @@
     return result.build();
   }
 
+  public List<String> findImplementationsOf(Class<?> requestedInterface)
+      throws IOException {
+    List<String> result = Lists.newArrayList();
+    String name = requestedInterface.getName().replace('.', '/');
+
+    Enumeration<JarEntry> e = jarFile.entries();
+    while (e.hasMoreElements()) {
+      JarEntry entry = e.nextElement();
+      if (skip(entry)) {
+        continue;
+      }
+
+      ClassData def = new ClassData(Collections.<String>emptySet());
+      try {
+        new ClassReader(read(jarFile, entry)).accept(def, SKIP_ALL);
+      } catch (RuntimeException err) {
+        PluginLoader.log.warn(String.format("Jar %s has invalid class file %s",
+            jarFile.getName(), entry.getName()), err);
+        continue;
+      }
+
+      if (def.isConcrete() && def.interfaces != null
+          && Iterables.contains(Arrays.asList(def.interfaces), name)) {
+        result.add(def.className);
+      }
+    }
+
+    return result;
+  }
+
   private static boolean skip(JarEntry entry) {
     if (!entry.getName().endsWith(".class")) {
       return true; // Avoid non-class resources.
@@ -166,6 +194,7 @@
     String className;
     String annotationName;
     String annotationValue;
+    String[] interfaces;
     Iterable<String> exports;
 
     private ClassData(Iterable<String> exports) {
@@ -183,6 +212,7 @@
         String superName, String[] interfaces) {
       this.className = Type.getObjectType(name).getClassName();
       this.access = access;
+      this.interfaces = interfaces;
     }
 
     @Override
@@ -234,7 +264,7 @@
     }
   }
 
-  private static abstract class AbstractAnnotationVisitor extends
+  private abstract static class AbstractAnnotationVisitor extends
       AnnotationVisitor {
     AbstractAnnotationVisitor() {
       super(Opcodes.ASM4);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
index 8b99286..b51359d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -214,7 +214,7 @@
     }
   }
 
-  synchronized private void unloadPlugin(Plugin plugin) {
+  private synchronized void unloadPlugin(Plugin plugin) {
     persistentCacheFactory.onStop(plugin);
     String name = plugin.getName();
     log.info(String.format("Unloading plugin %s", name));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkInfo.java
index 4035c7e..a91e745 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkInfo.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkInfo.java
@@ -47,7 +47,7 @@
   public final String html;
   public final Boolean enabled; // null means true
 
-  public transient final String name;
+  public final transient String name;
 
   public CommentLinkInfo(String name, String match, String link, String html,
       Boolean enabled) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java
index 4142a30..9009aad 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java
@@ -119,7 +119,7 @@
   }
 
   @AutoValue
-  static abstract class EntryKey {
+  abstract static class EntryKey {
     public abstract String ref();
     public abstract List<String> patterns();
     public abstract int cachedHashCode();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java
index 953dabf..39b0fa3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java
@@ -92,8 +92,9 @@
 
   @Override
   public boolean equals(final Object other) {
-    if (other == null)
+    if (other == null) {
       return false;
+    }
     return getClass() == other.getClass()
         && getChildren().equals(((Predicate<?>) other).getChildren());
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index d9dad78..62e1ba9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -656,7 +656,13 @@
 
   @Override
   public String toString() {
-    return MoreObjects.toStringHelper(this).addValue(getId()).toString();
+    MoreObjects.ToStringHelper h = MoreObjects.toStringHelper(this);
+    if (change != null) {
+      h.addValue(change);
+    } else {
+      h.addValue(legacyId);
+    }
+    return h.toString();
   }
 
   public static class ChangedLines {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsCacheImpl.java
index 1b3473e..8cca00d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsCacheImpl.java
@@ -23,7 +23,7 @@
 
 @Singleton
 public class ConflictsCacheImpl implements ConflictsCache {
-  public final static String NAME = "conflicts";
+  public static final String NAME = "conflicts";
 
   public static Module module() {
     return new CacheModule() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
index 4e9e096..945baa8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -46,7 +46,7 @@
     this.versionNbr = guessVersion(getClass());
   }
 
-  private static int guessVersion(Class<?> c) {
+  public static int guessVersion(Class<?> c) {
     String n = c.getName();
     n = n.substring(n.lastIndexOf('_') + 1);
     while (n.startsWith("0"))
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/change/ConsistencyCheckerTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/change/ConsistencyCheckerTest.java
index 51aa283..358620f 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/change/ConsistencyCheckerTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/change/ConsistencyCheckerTest.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.change;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.testutil.TestChanges.incrementPatchSet;
 import static com.google.gerrit.testutil.TestChanges.newChange;
 import static com.google.gerrit.testutil.TestChanges.newPatchSet;
 import static java.util.Collections.singleton;
@@ -29,15 +29,20 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.InternalUser;
+import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.testutil.FakeAccountByEmailCache;
 import com.google.gerrit.testutil.InMemoryDatabase;
 import com.google.gerrit.testutil.InMemoryRepositoryManager;
+import com.google.gerrit.testutil.TestChanges;
 import com.google.inject.util.Providers;
 
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.After;
@@ -59,14 +64,21 @@
 
   @Before
   public void setUp() throws Exception {
+    FakeAccountByEmailCache accountCache = new FakeAccountByEmailCache();
     schemaFactory = InMemoryDatabase.newDatabase();
     schemaFactory.create();
     db = schemaFactory.open();
     repoManager = new InMemoryRepositoryManager();
-    checker = new ConsistencyChecker(Providers.<ReviewDb> of(db), repoManager);
+    checker = new ConsistencyChecker(
+        Providers.<ReviewDb> of(db),
+        repoManager,
+        Providers.<CurrentUser> of(new InternalUser(null)),
+        Providers.of(new PersonIdent("server", "noreply@example.com")),
+        new PatchSetInfoFactory(repoManager, accountCache));
     project = new Project.NameKey("repo");
     repo = new TestRepository<>(repoManager.createRepository(project));
     userId = new Account.Id(1);
+    accountCache.putAny(userId);
     db.accounts().insert(singleton(new Account(userId, TimeUtil.nowTs())));
     tip = repo.branch("master").commit().create();
   }
@@ -83,31 +95,19 @@
 
   @Test
   public void validNewChange() throws Exception {
-    Change c = newChange(project, userId);
-    db.changes().insert(singleton(c));
-    RevCommit commit1 = repo.branch(c.currentPatchSetId().toRefName()).commit()
-        .parent(tip).create();
-    PatchSet ps1 = newPatchSet(c.currentPatchSetId(), commit1, userId);
-    db.patchSets().insert(singleton(ps1));
-
+    Change c = insertChange();
+    insertPatchSet(c);
     incrementPatchSet(c);
-    RevCommit commit2 = repo.branch(c.currentPatchSetId().toRefName()).commit()
-        .parent(tip).create();
-    PatchSet ps2 = newPatchSet(c.currentPatchSetId(), commit2, userId);
-    db.patchSets().insert(singleton(ps2));
-
+    insertPatchSet(c);
     assertProblems(c);
   }
 
   @Test
   public void validMergedChange() throws Exception {
-    Change c = newChange(project, userId);
+    Change c = insertChange();
     c.setStatus(Change.Status.MERGED);
-    db.changes().insert(singleton(c));
-    RevCommit commit1 = repo.branch(c.currentPatchSetId().toRefName()).commit()
-        .parent(tip).create();
-    PatchSet ps1 = newPatchSet(c.currentPatchSetId(), commit1, userId);
-    db.patchSets().insert(singleton(ps1));
+    insertPatchSet(c);
+    incrementPatchSet(c);
 
     incrementPatchSet(c);
     RevCommit commit2 = repo.branch(c.currentPatchSetId().toRefName()).commit()
@@ -135,68 +135,202 @@
   public void missingRepo() throws Exception {
     Change c = newChange(new Project.NameKey("otherproject"), userId);
     db.changes().insert(singleton(c));
-    PatchSet ps = newPatchSet(c.currentPatchSetId(),
-        ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), userId);
-    db.patchSets().insert(singleton(ps));
+    insertMissingPatchSet(c, "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
     assertProblems(c, "Destination repository not found: otherproject");
   }
 
   @Test
   public void invalidRevision() throws Exception {
-    Change c = newChange(project, userId);
-    db.changes().insert(singleton(c));
+    Change c = insertChange();
 
-    PatchSet ps = new PatchSet(c.currentPatchSetId());
-    ps.setRevision(new RevId("fooooooooooooooooooooooooooooooooooooooo"));
-    ps.setUploader(userId);
-    ps.setCreatedOn(TimeUtil.nowTs());
-    db.patchSets().insert(singleton(ps));
-
+    db.patchSets().insert(singleton(newPatchSet(c.currentPatchSetId(),
+            "fooooooooooooooooooooooooooooooooooooooo", userId)));
     incrementPatchSet(c);
-    RevCommit commit2 = repo.branch(c.currentPatchSetId().toRefName()).commit()
-        .parent(tip).create();
-    PatchSet ps2 = newPatchSet(c.currentPatchSetId(), commit2, userId);
-    db.patchSets().insert(singleton(ps2));
+    insertPatchSet(c);
 
     assertProblems(c,
         "Invalid revision on patch set 1:"
         + " fooooooooooooooooooooooooooooooooooooooo");
   }
 
+  // No test for ref existing but object missing; InMemoryRepository won't let
+  // us do such a thing.
+
   @Test
-  public void patchSetObjectMissing() throws Exception {
-    Change c = newChange(project, userId);
-    db.changes().insert(singleton(c));
+  public void patchSetObjectAndRefMissing() throws Exception {
+    Change c = insertChange();
     PatchSet ps = newPatchSet(c.currentPatchSetId(),
         ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), userId);
     db.patchSets().insert(singleton(ps));
 
     assertProblems(c,
+        "Ref missing: " + ps.getId().toRefName(),
         "Object missing: patch set 1: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
   }
 
   @Test
+  public void patchSetObjectAndRefMissingWithFix() throws Exception {
+    Change c = insertChange();
+    PatchSet ps = newPatchSet(c.currentPatchSetId(),
+        ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), userId);
+    db.patchSets().insert(singleton(ps));
+
+    String refName = ps.getId().toRefName();
+    List<ProblemInfo> problems = checker.check(c, new FixInput()).problems();
+    ProblemInfo p = problems.get(0);
+    assertThat(p.message).isEqualTo("Ref missing: " + refName);
+    assertThat(p.status).isNull();
+  }
+
+  @Test
+  public void patchSetRefMissing() throws Exception {
+    Change c = insertChange();
+    PatchSet ps = insertPatchSet(c);
+    String refName = ps.getId().toRefName();
+    repo.update("refs/other/foo", ObjectId.fromString(ps.getRevision().get()));
+    deleteRef(refName);
+
+    assertProblems(c, "Ref missing: " + refName);
+  }
+
+  @Test
+  public void patchSetRefMissingWithFix() throws Exception {
+    Change c = insertChange();
+    PatchSet ps = insertPatchSet(c);
+    String refName = ps.getId().toRefName();
+    repo.update("refs/other/foo", ObjectId.fromString(ps.getRevision().get()));
+    deleteRef(refName);
+
+    List<ProblemInfo> problems = checker.check(c, new FixInput()).problems();
+    ProblemInfo p = problems.get(0);
+    assertThat(p.message).isEqualTo("Ref missing: " + refName);
+    assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
+    assertThat(p.outcome).isEqualTo("Repaired patch set ref");
+
+    assertThat(repo.getRepository().getRef(refName).getObjectId().name())
+        .isEqualTo(ps.getRevision().get());
+  }
+
+  @Test
+  public void patchSetObjectAndRefMissingWithDeletingPatchSet()
+      throws Exception {
+    Change c = insertChange();
+    PatchSet ps1 = insertPatchSet(c);
+    incrementPatchSet(c);
+    PatchSet ps2 = insertMissingPatchSet(c,
+        "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+
+    FixInput fix = new FixInput();
+    fix.deletePatchSetIfCommitMissing = true;
+    List<ProblemInfo> problems = checker.check(c, fix).problems();
+    assertThat(problems).hasSize(2);
+    ProblemInfo p = problems.get(0);
+    assertThat(p.message).isEqualTo("Ref missing: " + ps2.getId().toRefName());
+    assertThat(p.status).isNull();
+    p = problems.get(1);
+    assertThat(p.message).isEqualTo(
+        "Object missing: patch set 2: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+    assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
+    assertThat(p.outcome).isEqualTo("Deleted patch set");
+
+    c = db.changes().get(c.getId());
+    assertThat(c.currentPatchSetId().get()).isEqualTo(1);
+    assertThat(db.patchSets().get(ps1.getId())).isNotNull();
+    assertThat(db.patchSets().get(ps2.getId())).isNull();
+  }
+
+  @Test
+  public void patchSetMultipleObjectsMissingWithDeletingPatchSets()
+      throws Exception {
+    Change c = insertChange();
+    PatchSet ps1 = insertPatchSet(c);
+
+    incrementPatchSet(c);
+    PatchSet ps2 = insertMissingPatchSet(c,
+        "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+
+    incrementPatchSet(c);
+    PatchSet ps3 = insertPatchSet(c);
+
+    incrementPatchSet(c);
+    PatchSet ps4 = insertMissingPatchSet(c,
+        "c0ffeeeec0ffeeeec0ffeeeec0ffeeeec0ffeeee");
+
+    FixInput fix = new FixInput();
+    fix.deletePatchSetIfCommitMissing = true;
+    List<ProblemInfo> problems = checker.check(c, fix).problems();
+    assertThat(problems).hasSize(4);
+
+    ProblemInfo p = problems.get(0);
+    assertThat(p.message).isEqualTo("Ref missing: " + ps4.getId().toRefName());
+    assertThat(p.status).isNull();
+
+    p = problems.get(1);
+    assertThat(p.message).isEqualTo(
+        "Object missing: patch set 4: c0ffeeeec0ffeeeec0ffeeeec0ffeeeec0ffeeee");
+    assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
+    assertThat(p.outcome).isEqualTo("Deleted patch set");
+
+    p = problems.get(2);
+    assertThat(p.message).isEqualTo("Ref missing: " + ps2.getId().toRefName());
+    assertThat(p.status).isNull();
+
+    p = problems.get(3);
+    assertThat(p.message).isEqualTo(
+        "Object missing: patch set 2: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+    assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
+    assertThat(p.outcome).isEqualTo("Deleted patch set");
+
+    c = db.changes().get(c.getId());
+    assertThat(c.currentPatchSetId().get()).isEqualTo(3);
+    assertThat(db.patchSets().get(ps1.getId())).isNotNull();
+    assertThat(db.patchSets().get(ps2.getId())).isNull();
+    assertThat(db.patchSets().get(ps3.getId())).isNotNull();
+    assertThat(db.patchSets().get(ps4.getId())).isNull();
+  }
+
+  @Test
+  public void onlyPatchSetObjectMissingWithFix() throws Exception {
+    Change c = insertChange();
+    PatchSet ps1 = insertMissingPatchSet(c,
+        "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+
+    FixInput fix = new FixInput();
+    fix.deletePatchSetIfCommitMissing = true;
+    List<ProblemInfo> problems = checker.check(c, fix).problems();
+    assertThat(problems).hasSize(2);
+    ProblemInfo p = problems.get(0);
+    assertThat(p.message).isEqualTo("Ref missing: " + ps1.getId().toRefName());
+    assertThat(p.status).isNull();
+    p = problems.get(1);
+    assertThat(p.message).isEqualTo(
+        "Object missing: patch set 1: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+    assertThat(p.status).isEqualTo(ProblemInfo.Status.FIX_FAILED);
+    assertThat(p.outcome)
+        .isEqualTo("Cannot delete patch set; no patch sets would remain");
+
+    c = db.changes().get(c.getId());
+    assertThat(c.currentPatchSetId().get()).isEqualTo(1);
+    assertThat(db.patchSets().get(ps1.getId())).isNotNull();
+  }
+
+  @Test
   public void currentPatchSetMissing() throws Exception {
-    Change c = newChange(project, userId);
-    db.changes().insert(singleton(c));
+    Change c = insertChange();
     assertProblems(c, "Current patch set 1 not found");
   }
 
   @Test
   public void duplicatePatchSetRevisions() throws Exception {
-    Change c = newChange(project, userId);
-    db.changes().insert(singleton(c));
-    RevCommit commit1 = repo.branch(c.currentPatchSetId().toRefName()).commit()
-        .parent(tip).create();
-    PatchSet ps1 = newPatchSet(c.currentPatchSetId(), commit1, userId);
-    db.patchSets().insert(singleton(ps1));
-
+    Change c = insertChange();
+    PatchSet ps1 = insertPatchSet(c);
+    String rev = ps1.getRevision().get();
     incrementPatchSet(c);
-    PatchSet ps2 = newPatchSet(c.currentPatchSetId(), commit1, userId);
-    db.patchSets().insert(singleton(ps2));
+    PatchSet ps2 = insertMissingPatchSet(c, rev);
+    updatePatchSetRef(ps2);
 
     assertProblems(c,
-        "Multiple patch sets pointing to " + commit1.name() + ": [1, 2]");
+        "Multiple patch sets pointing to " + rev + ": [1, 2]");
   }
 
   @Test
@@ -204,10 +338,10 @@
     RefUpdate ru = repo.getRepository().updateRef("refs/heads/master");
     ru.setForceUpdate(true);
     assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
-    Change c = newChange(project, userId);
-    db.changes().insert(singleton(c));
+    Change c = insertChange();
     RevCommit commit = repo.commit().create();
     PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, userId);
+    updatePatchSetRef(ps);
     db.patchSets().insert(singleton(ps));
 
     assertProblems(c, "Destination ref not found (may be new branch): master");
@@ -215,23 +349,19 @@
 
   @Test
   public void mergedChangeIsNotMerged() throws Exception {
-    Change c = newChange(project, userId);
+    Change c = insertChange();
     c.setStatus(Change.Status.MERGED);
-    db.changes().insert(singleton(c));
-    RevCommit commit = repo.branch(c.currentPatchSetId().toRefName()).commit()
-        .parent(tip).create();
-    PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, userId);
-    db.patchSets().insert(singleton(ps));
+    PatchSet ps = insertPatchSet(c);
+    String rev = ps.getRevision().get();
 
     assertProblems(c,
-        "Patch set 1 (" + commit.name() + ") is not merged into destination ref"
+        "Patch set 1 (" + rev + ") is not merged into destination ref"
         + " master (" + tip.name() + "), but change status is MERGED");
   }
 
   @Test
   public void newChangeIsMerged() throws Exception {
-    Change c = newChange(project, userId);
-    db.changes().insert(singleton(c));
+    Change c = insertChange();
     RevCommit commit = repo.branch(c.currentPatchSetId().toRefName()).commit()
         .parent(tip).create();
     PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, userId);
@@ -245,8 +375,7 @@
 
   @Test
   public void newChangeIsMergedWithFix() throws Exception {
-    Change c = newChange(project, userId);
-    db.changes().insert(singleton(c));
+    Change c = insertChange();
     RevCommit commit = repo.branch(c.currentPatchSetId().toRefName()).commit()
         .parent(tip).create();
     PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, userId);
@@ -267,11 +396,52 @@
     assertProblems(c);
   }
 
+  private Change insertChange() throws Exception {
+    Change c = newChange(project, userId);
+    db.changes().insert(singleton(c));
+    return c;
+  }
+
+  private void incrementPatchSet(Change c) throws Exception {
+    TestChanges.incrementPatchSet(c);
+    db.changes().upsert(singleton(c));
+  }
+
+  private PatchSet insertPatchSet(Change c) throws Exception {
+    db.changes().upsert(singleton(c));
+    RevCommit commit = repo.branch(c.currentPatchSetId().toRefName()).commit()
+        .parent(tip).create();
+    PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, userId);
+    updatePatchSetRef(ps);
+    db.patchSets().insert(singleton(ps));
+    return ps;
+  }
+
+  private PatchSet insertMissingPatchSet(Change c, String id) throws Exception {
+    PatchSet ps = newPatchSet(c.currentPatchSetId(),
+        ObjectId.fromString(id), userId);
+    db.patchSets().insert(singleton(ps));
+    return ps;
+  }
+
+  private void updatePatchSetRef(PatchSet ps) throws Exception {
+    repo.update(ps.getId().toRefName(),
+        ObjectId.fromString(ps.getRevision().get()));
+  }
+
+  private void deleteRef(String refName) throws Exception {
+    RefUpdate ru = repo.getRepository().updateRef(refName, true);
+    ru.setForceUpdate(true);
+    assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
+  }
+
   private void assertProblems(Change c, String... expected) {
     assertThat(Lists.transform(checker.check(c).problems(),
           new Function<ProblemInfo, String>() {
             @Override
             public String apply(ProblemInfo in) {
+              checkArgument(in.status == null,
+                  "Status is not null: " + in.message);
               return in.message;
             }
           })).containsExactly((Object[]) expected);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/events/EventTypesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/events/EventTypesTest.java
new file mode 100644
index 0000000..c22c7fb
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/events/EventTypesTest.java
@@ -0,0 +1,72 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.events;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.gerrit.server.events.Event;
+import com.google.gerrit.server.events.EventTypes;
+
+import org.junit.Test;
+
+public class EventTypesTest {
+  public static class TestEvent extends Event {
+    public TestEvent() {
+      super("test-event");
+    }
+  }
+
+  public static class TestEvent2 extends Event {
+    public TestEvent2() {
+      super("test-event"); // Intentionally same as in TestEvent
+    }
+  }
+
+  public static class AnotherTestEvent extends Event {
+    public AnotherTestEvent() {
+      super("another-test-event");
+    }
+  }
+
+  @Test
+  public void testEventTypeRegistration() {
+    EventTypes.registerClass(new TestEvent());
+    EventTypes.registerClass(new AnotherTestEvent());
+    assertThat(EventTypes.getClass("test-event")).isEqualTo(TestEvent.class);
+    assertThat(EventTypes.getClass("another-test-event"))
+      .isEqualTo(AnotherTestEvent.class);
+
+    try {
+      EventTypes.registerClass(new TestEvent());
+      fail("Expected IllegalArgumentException");
+    } catch (IllegalArgumentException e) {
+      assertThat(EventTypes.getClass("test-event")).isEqualTo(TestEvent.class);
+    }
+
+    try {
+      EventTypes.registerClass(new TestEvent2());
+      fail("Expected IllegalArgumentException");
+    } catch (IllegalArgumentException e) {
+      assertThat(EventTypes.getClass("test-event")).isEqualTo(TestEvent.class);
+    }
+  }
+
+  @Test
+  public void testGetClassForNonExistingType() {
+    Class<?> clazz = EventTypes.getClass("does-not-exist-event");
+    assertThat(clazz).isNull();
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/BasicSerializationTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/BasicSerializationTest.java
index d9f86bd..dbc8f02 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/BasicSerializationTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/BasicSerializationTest.java
@@ -137,8 +137,9 @@
   private static void assertOutput(final byte[] expect,
       final ByteArrayOutputStream out) {
     final byte[] buf = out.toByteArray();
-    for (int i = 0; i < expect.length; i++)
+    for (int i = 0; i < expect.length; i++) {
       assertEquals(expect[i], buf[i]);
+    }
   }
 
   private static InputStream r(final byte[] buf) {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeAccountByEmailCache.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeAccountByEmailCache.java
new file mode 100644
index 0000000..c3bfe1e
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeAccountByEmailCache.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.testutil;
+
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.SetMultimap;
+import com.google.common.collect.Sets;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.account.AccountByEmailCache;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/** Fake implementation of {@link AccountByEmailCache} for testing. */
+public class FakeAccountByEmailCache implements AccountByEmailCache {
+  private final SetMultimap<String, Account.Id> byEmail;
+  private final Set<Account.Id> anyEmail;
+
+  public FakeAccountByEmailCache() {
+    byEmail = HashMultimap.create();
+    anyEmail = new HashSet<>();
+  }
+
+  @Override
+  public synchronized Set<Account.Id> get(String email) {
+    return Collections.unmodifiableSet(
+        Sets.union(byEmail.get(email), anyEmail));
+  }
+
+  @Override
+  public synchronized void evict(String email) {
+    // Do nothing.
+  }
+
+  public synchronized void put(String email, Account.Id id) {
+    byEmail.put(email, id);
+  }
+
+  public synchronized void putAny(Account.Id id) {
+    anyEmail.add(id);
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/TempFileUtil.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/TempFileUtil.java
index 2f3453b..72c2b5a 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/TempFileUtil.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/TempFileUtil.java
@@ -22,7 +22,7 @@
 public class TempFileUtil {
   private static List<File> allDirsCreated = new ArrayList<>();
 
-  public synchronized static File createTempDirectory() throws IOException {
+  public static synchronized File createTempDirectory() throws IOException {
     File tmp = File.createTempFile("gerrit_test_", "").getCanonicalFile();
     if (!tmp.delete() || !tmp.mkdir()) {
       throw new IOException("Cannot create " + tmp.getPath());
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java
index 11ba665..675634e 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java
@@ -69,8 +69,13 @@
 
   public static PatchSet newPatchSet(PatchSet.Id id, ObjectId revision,
       Account.Id userId) {
+    return newPatchSet(id, revision.name(), userId);
+  }
+
+  public static PatchSet newPatchSet(PatchSet.Id id, String revision,
+      Account.Id userId) {
     PatchSet ps = new PatchSet(id);
-    ps.setRevision(new RevId(revision.name()));
+    ps.setRevision(new RevId(revision));
     ps.setUploader(userId);
     ps.setCreatedOn(TimeUtil.nowTs());
     return ps;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
index f7d4f8f..56441fba 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
@@ -253,7 +253,7 @@
   }
 
   /** Split a command line into a string array. */
-  static public String[] split(String commandLine) {
+  public static String[] split(String commandLine) {
     final List<String> list = new ArrayList<>();
     boolean inquote = false;
     boolean inDblQuote = false;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java
index d95b887..8f26bba 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java
@@ -709,7 +709,7 @@
     print(help.toString());
   }
 
-  private static abstract class Function {
+  private abstract static class Function {
     final String name;
 
     Function(final String name) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
index f5ab20a..889d3fb 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
@@ -16,12 +16,13 @@
 
 import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
 
-import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.EventListener;
+import com.google.gerrit.common.EventSource;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.events.Event;
+import com.google.gerrit.server.events.EventTypes;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.git.WorkQueue.CancelableRunnable;
 import com.google.gerrit.sshd.BaseCommand;
@@ -51,7 +52,7 @@
   private IdentifiedUser currentUser;
 
   @Inject
-  private ChangeHooks hooks;
+  private EventSource source;
 
   @Inject
   @StreamCommandExecutor
@@ -64,10 +65,17 @@
   private final Gson gson = new Gson();
 
   /** Special event to notify clients they missed other events. */
-  private final Object droppedOutputEvent = new Object() {
-    @SuppressWarnings("unused")
-    final String type = "dropped-output";
-  };
+  private static final class DroppedOutputEvent extends Event {
+    public DroppedOutputEvent() {
+      super("dropped-output");
+    }
+  }
+
+  private static final DroppedOutputEvent droppedOutputEvent = new DroppedOutputEvent();
+
+  static {
+    EventTypes.registerClass(droppedOutputEvent);
+  }
 
   private final EventListener listener = new EventListener() {
     @Override
@@ -124,12 +132,12 @@
     }
 
     stdout = toPrintWriter(out);
-    hooks.addEventListener(listener, currentUser);
+    source.addEventListener(listener, currentUser);
   }
 
   @Override
   protected void onExit(final int rc) {
-    hooks.removeEventListener(listener);
+    source.removeEventListener(listener);
 
     synchronized (taskLock) {
       done = true;
@@ -140,7 +148,7 @@
 
   @Override
   public void destroy() {
-    hooks.removeEventListener(listener);
+    source.removeEventListener(listener);
 
     final boolean exit;
     synchronized (taskLock) {
@@ -188,7 +196,7 @@
         // destroy() above, or it closed the stream and is no longer
         // accepting output. Either way terminate this instance.
         //
-        hooks.removeEventListener(listener);
+        source.removeEventListener(listener);
         flush();
         onExit(0);
         return;
diff --git a/plugins/replication b/plugins/replication
index 38c2a6b..3973b31 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 38c2a6b62ecc6fea9b41b1388c56f15ae760f3bb
+Subproject commit 3973b31f3c86cc5401ef2dfac5ac82f25d79e432