Merge "Sort email comment groups"
diff --git a/BUILD b/BUILD
index 7ec32be..5fd38c8 100644
--- a/BUILD
+++ b/BUILD
@@ -20,3 +20,7 @@
 pkg_war(name = 'gerrit')
 pkg_war(name = 'headless', ui = None)
 pkg_war(name = 'release', ui = 'ui_optdbg_r', context = ['//plugins:core'])
+pkg_war(
+  name = "polygerrit",
+  ui = "polygerrit"
+)
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index f063b88..834bcae 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -38,15 +38,18 @@
   bazel build gerrit
 ----
 
-[NOTE]
-PolyGerrit UI not yet working.
-
 The output executable WAR will be placed in:
 
 ----
   bazel-bin/gerrit.war
 ----
 
+to run,
+
+----
+  $(bazel info output_base)/external/local_jdk/bin/java \
+     -jar bazel-bin/release.war daemon -d ../gerrit_testsite
+----
 
 === Headless Mode
 
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index e5073b2..a258b32 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -1767,13 +1767,19 @@
   HTTP/1.1 204 No Content
 ----
 
-[[delete-draft-change]]
-=== Delete Draft Change
+[[delete-change]]
+=== Delete Change
 --
 'DELETE /changes/link:#change-id[\{change-id\}]'
 --
 
-Deletes a draft change.
+Deletes a change.
+
+New or abandoned changes can only be deleted by administrators. The deletion of
+merged changes isn't supported at the moment. Draft changes can only be deleted
+by their owner or other users who have the permissions to view and delete
+drafts. If the draft workflow is disabled, only administrators with those
+permissions may delete draft changes.
 
 .Request
 ----
diff --git a/WORKSPACE b/WORKSPACE
index f01ad3f..ac50f4a 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -132,7 +132,7 @@
   sha256 = '80d63c117736ae2fb9837b7a39576f3f0c5bd19cd75127886550c77b4c478f87',
 )
 
-JGIT_VERS = '4.5.0.201609210915-r'
+load('//lib/jgit:jgit.bzl', 'JGIT_VERS')
 
 maven_jar(
   name = 'jgit',
@@ -481,12 +481,6 @@
 )
 
 maven_jar(
-  name = 'lucene_misc',
-  artifact = 'org.apache.lucene:lucene-misc:' + LUCENE_VERS,
-  sha1 = '37bbe5a2fb429499dfbe75d750d1778881fff45d',
-)
-
-maven_jar(
   name = 'lucene_sandbox',
   artifact = 'org.apache.lucene:lucene-sandbox:' + LUCENE_VERS,
   sha1 = '30a91f120706ba66732d5a974b56c6971b3c8a16',
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestAccount.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestAccount.java
index c1a815a..63e0fa7 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestAccount.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestAccount.java
@@ -14,7 +14,8 @@
 
 package com.google.gerrit.acceptance;
 
-import com.google.common.collect.FluentIterable;
+import static java.util.stream.Collectors.toList;
+
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.mail.Address;
 
@@ -24,22 +25,22 @@
 
 import java.io.ByteArrayOutputStream;
 import java.util.Arrays;
+import java.util.List;
 
 public class TestAccount {
-  public static FluentIterable<Account.Id> ids(
-      Iterable<TestAccount> accounts) {
-    return FluentIterable.from(accounts).transform(a -> a.id);
+  public static List<Account.Id> ids(List<TestAccount> accounts) {
+    return accounts.stream().map(a -> a.id).collect(toList());
   }
 
-  public static FluentIterable<Account.Id> ids(TestAccount... accounts) {
+  public static List<Account.Id> ids(TestAccount... accounts) {
     return ids(Arrays.asList(accounts));
   }
 
-  public static FluentIterable<String> names(Iterable<TestAccount> accounts) {
-    return FluentIterable.from(accounts).transform(a -> a.fullName);
+  public static List<String> names(List<TestAccount> accounts) {
+    return accounts.stream().map(a -> a.fullName).collect(toList());
   }
 
-  public static FluentIterable<String> names(TestAccount... accounts) {
+  public static List<String> names(TestAccount... accounts) {
     return names(Arrays.asList(accounts));
   }
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
index d995be6..8af4098 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -68,6 +68,7 @@
 import com.google.gerrit.extensions.common.MergePatchSetInput;
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
@@ -398,7 +399,7 @@
   }
 
   @Test
-  public void delete() throws Exception {
+  public void deleteDraftChange() throws Exception {
     PushOneCommit.Result r = createChange("refs/drafts/master");
     assertThat(query(r.getChangeId())).hasSize(1);
     assertThat(info(r.getChangeId()).status).isEqualTo(ChangeStatus.DRAFT);
@@ -409,6 +410,110 @@
   }
 
   @Test
+  public void deleteNewChangeAsAdmin() throws Exception {
+    PushOneCommit.Result changeResult = createChange();
+    String changeId = changeResult.getChangeId();
+
+    gApi.changes()
+        .id(changeId)
+        .delete();
+
+    assertThat(query(changeId)).isEmpty();
+  }
+
+  @Test
+  @TestProjectInput(cloneAs = "user")
+  public void deleteNewChangeAsNormalUser() throws Exception {
+    PushOneCommit.Result changeResult =
+        pushFactory.create(db, user.getIdent(), testRepo)
+            .to("refs/for/master");
+    String changeId = changeResult.getChangeId();
+    Change.Id id = changeResult.getChange().getId();
+
+    setApiUser(user);
+    exception.expect(AuthException.class);
+    exception.expectMessage(String.format(
+        "Deleting change %s is not permitted", id));
+    gApi.changes()
+        .id(changeId)
+        .delete();
+  }
+
+  @Test
+  @TestProjectInput(cloneAs = "user")
+  public void deleteNewChangeOfAnotherUserAsAdmin() throws Exception {
+    PushOneCommit.Result changeResult =
+        pushFactory.create(db, user.getIdent(), testRepo)
+            .to("refs/for/master");
+    changeResult.assertOkStatus();
+    String changeId = changeResult.getChangeId();
+
+    setApiUser(admin);
+    gApi.changes()
+        .id(changeId)
+        .delete();
+
+    assertThat(query(changeId)).isEmpty();
+  }
+
+  @Test
+  @TestProjectInput(cloneAs = "user")
+  public void deleteAbandonedChangeAsNormalUser() throws Exception {
+    PushOneCommit.Result changeResult =
+        pushFactory.create(db, user.getIdent(), testRepo)
+        .to("refs/for/master");
+    String changeId = changeResult.getChangeId();
+    Change.Id id = changeResult.getChange().getId();
+
+    setApiUser(user);
+    gApi.changes()
+        .id(changeId)
+        .abandon();
+
+    exception.expect(AuthException.class);
+    exception.expectMessage(String.format(
+        "Deleting change %s is not permitted", id));
+    gApi.changes()
+        .id(changeId)
+        .delete();
+  }
+
+  @Test
+  @TestProjectInput(cloneAs = "user")
+  public void deleteAbandonedChangeOfAnotherUserAsAdmin() throws Exception {
+    PushOneCommit.Result changeResult =
+        pushFactory.create(db, user.getIdent(), testRepo)
+        .to("refs/for/master");
+    String changeId = changeResult.getChangeId();
+
+    gApi.changes()
+        .id(changeId)
+        .abandon();
+
+    gApi.changes()
+        .id(changeId)
+        .delete();
+
+    assertThat(query(changeId)).isEmpty();
+  }
+
+  @Test
+  public void deleteMergedChange() throws Exception {
+    PushOneCommit.Result changeResult = createChange();
+    String changeId = changeResult.getChangeId();
+    Change.Id id = changeResult.getChange().getId();
+
+    merge(changeResult);
+
+    exception.expect(MethodNotAllowedException.class);
+    exception.expectMessage(String.format(
+        "Deleting merged change %s is not allowed", id));
+    gApi.changes()
+        .id(changeId)
+        .delete();
+  }
+
+  @Test
   public void rebaseUpToDateChange() throws Exception {
     PushOneCommit.Result r = createChange();
     exception.expect(ResourceConflictException.class);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java
index 78c0cda..f98e588 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -17,10 +17,9 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.api.group.GroupAssert.assertGroupInfo;
 import static com.google.gerrit.acceptance.rest.account.AccountAssert.assertAccountInfos;
+import static java.util.stream.Collectors.toList;
 
-import com.google.common.collect.FluentIterable;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.Ordering;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.TestAccount;
@@ -397,10 +396,10 @@
 
   @Test
   public void testListAllGroups() throws Exception {
-    List<String> expectedGroups = FluentIterable
-          .from(groupCache.all())
-          .transform(AccountGroup::getName)
-          .toSortedList(Ordering.natural());
+    List<String> expectedGroups = groupCache.all().stream()
+          .map(a -> a.getName())
+          .sorted()
+          .collect(toList());
     assertThat(expectedGroups.size()).isAtLeast(2);
     assertThat(gApi.groups().list().getAsMap().keySet())
         .containsExactlyElementsIn(expectedGroups).inOrder();
@@ -504,7 +503,7 @@
       throws Exception {
     assertMembers(
         gApi.groups().id(group).members(),
-        TestAccount.names(expectedMembers).toArray(String.class));
+        TestAccount.names(expectedMembers).stream().toArray(String[]::new));
     assertAccountInfos(
         Arrays.asList(expectedMembers),
         gApi.groups().id(group).members());
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java
index 2a32abe..eb6e433 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java
@@ -22,20 +22,35 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.LabelInfo;
+import com.google.gerrit.extensions.common.RevisionInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.notedb.PatchSetState;
 import com.google.gerrit.testutil.ConfigSuite;
+import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.Config;
 import org.junit.Test;
 
 import java.util.Collection;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.stream.Collectors;
 
 public class DraftChangeIT extends AbstractDaemonTest {
   @ConfigSuite.Config
@@ -43,20 +58,8 @@
     return allowDraftsDisabledConfig();
   }
 
-  @Test
-  public void deleteChange() throws Exception {
-    PushOneCommit.Result result = createChange();
-    result.assertOkStatus();
-    String changeId = result.getChangeId();
-    String triplet = project.get() + "~master~" + changeId;
-    ChangeInfo c = get(triplet);
-    assertThat(c.id).isEqualTo(triplet);
-    assertThat(c.status).isEqualTo(ChangeStatus.NEW);
-    RestResponse response = deleteChange(changeId, adminRestSession);
-    assertThat(response.getEntityContent())
-        .isEqualTo("Change is not a draft: " + c._number);
-    response.assertConflict();
-  }
+  @Inject
+  private BatchUpdate.Factory updateFactory;
 
   @Test
   public void deleteDraftChange() throws Exception {
@@ -75,6 +78,104 @@
   }
 
   @Test
+  public void deleteDraftChangeOfAnotherUser() throws Exception {
+    assume().that(isAllowDrafts()).isTrue();
+    PushOneCommit.Result changeResult = createDraftChange();
+    changeResult.assertOkStatus();
+    String changeId = changeResult.getChangeId();
+    Change.Id id = changeResult.getChange().getId();
+
+    // The user needs to be able to see the draft change (which reviewers can).
+    gApi.changes()
+        .id(changeId)
+        .addReviewer(user.fullName);
+
+    setApiUser(user);
+    exception.expect(AuthException.class);
+    exception.expectMessage(String.format(
+        "Deleting change %s is not permitted", id));
+    gApi.changes()
+        .id(changeId)
+        .delete();
+  }
+
+  @Test
+  @TestProjectInput(cloneAs = "user")
+  public void deleteDraftChangeWhenDraftsNotAllowedAsNormalUser()
+      throws Exception {
+    assume().that(isAllowDrafts()).isFalse();
+
+    setApiUser(user);
+    // We can't create a draft change while the draft workflow is disabled.
+    // For this reason, we create a normal change and modify the database.
+    PushOneCommit.Result changeResult =
+        pushFactory.create(db, user.getIdent(), testRepo)
+            .to("refs/for/master");
+    Change.Id id = changeResult.getChange().getId();
+    markChangeAsDraft(id);
+    setDraftStatusOfPatchSetsOfChange(id, true);
+
+    String changeId = changeResult.getChangeId();
+    exception.expect(MethodNotAllowedException.class);
+    exception.expectMessage("Draft workflow is disabled");
+    gApi.changes()
+        .id(changeId)
+        .delete();
+  }
+
+  @Test
+  @TestProjectInput(cloneAs = "user")
+  public void deleteDraftChangeWhenDraftsNotAllowedAsAdmin() throws Exception {
+    assume().that(isAllowDrafts()).isFalse();
+
+    setApiUser(user);
+    // We can't create a draft change while the draft workflow is disabled.
+    // For this reason, we create a normal change and modify the database.
+    PushOneCommit.Result changeResult =
+        pushFactory.create(db, user.getIdent(), testRepo)
+        .to("refs/for/master");
+    Change.Id id = changeResult.getChange().getId();
+    markChangeAsDraft(id);
+    setDraftStatusOfPatchSetsOfChange(id, true);
+
+    String changeId = changeResult.getChangeId();
+
+    // Grant those permissions to admins.
+    grant(Permission.VIEW_DRAFTS, project, "refs/*");
+    grant(Permission.DELETE_DRAFTS, project, "refs/*");
+
+    try {
+      setApiUser(admin);
+      gApi.changes()
+          .id(changeId)
+          .delete();
+    } finally {
+      removePermission(Permission.DELETE_DRAFTS, project, "refs/*");
+      removePermission(Permission.VIEW_DRAFTS, project, "refs/*");
+    }
+
+    setApiUser(user);
+    assertThat(query(changeId)).isEmpty();
+  }
+
+  @Test
+  public void deleteDraftChangeWithNonDraftPatchSet() throws Exception {
+    assume().that(isAllowDrafts()).isTrue();
+
+    PushOneCommit.Result changeResult = createDraftChange();
+    Change.Id id = changeResult.getChange().getId();
+    setDraftStatusOfPatchSetsOfChange(id, false);
+
+    String changeId = changeResult.getChangeId();
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage(String.format(
+        "Cannot delete draft change %s: patch set 1 is not a draft", id));
+    gApi.changes()
+        .id(changeId)
+        .delete();
+  }
+
+  @Test
   public void publishDraftChange() throws Exception {
     assume().that(isAllowDrafts()).isTrue();
     PushOneCommit.Result result = createDraftChange();
@@ -160,4 +261,90 @@
         + patchSet.getRevision().get()
         + "/publish");
   }
+
+  private void markChangeAsDraft(Change.Id id) throws Exception {
+    try (BatchUpdate batchUpdate = updateFactory
+        .create(db, project, atrScope.get().getUser(), TimeUtil.nowTs())) {
+      batchUpdate.addOp(id, new MarkChangeAsDraftUpdateOp());
+      batchUpdate.execute();
+    }
+
+    ChangeStatus changeStatus = gApi.changes()
+        .id(id.get())
+        .get()
+        .status;
+    assertThat(changeStatus).isEqualTo(ChangeStatus.DRAFT);
+  }
+
+  private void setDraftStatusOfPatchSetsOfChange(Change.Id id,
+      boolean draftStatus) throws Exception {
+    try (BatchUpdate batchUpdate = updateFactory
+        .create(db, project, atrScope.get().getUser(), TimeUtil.nowTs())) {
+      batchUpdate.addOp(id, new DraftStatusOfPatchSetsUpdateOp(draftStatus));
+      batchUpdate.execute();
+    }
+
+    Boolean expectedDraftStatus = draftStatus ? Boolean.TRUE : null;
+    List<Boolean> patchSetDraftStatuses = getPatchSetDraftStatuses(id);
+    patchSetDraftStatuses.forEach(status ->
+        assertThat(status).isEqualTo(expectedDraftStatus));
+  }
+
+  private List<Boolean> getPatchSetDraftStatuses(Change.Id id)
+      throws Exception {
+    Collection<RevisionInfo> revisionInfos = gApi.changes()
+        .id(id.get())
+        .get(EnumSet.of(ListChangesOption.ALL_REVISIONS))
+        .revisions
+        .values();
+    return revisionInfos.stream()
+        .map(revisionInfo -> revisionInfo.draft)
+        .collect(Collectors.toList());
+  }
+
+  private class MarkChangeAsDraftUpdateOp extends BatchUpdate.Op {
+    @Override
+    public boolean updateChange(BatchUpdate.ChangeContext ctx)
+        throws Exception {
+      Change change = ctx.getChange();
+
+      // Change status in database.
+      change.setStatus(Change.Status.DRAFT);
+
+      // Change status in NoteDb.
+      PatchSet.Id currentPatchSetId = change.currentPatchSetId();
+      ctx.getUpdate(currentPatchSetId).setStatus(Change.Status.DRAFT);
+
+      return true;
+    }
+  }
+
+  private class DraftStatusOfPatchSetsUpdateOp extends BatchUpdate.Op {
+    private final boolean draftStatus;
+
+    DraftStatusOfPatchSetsUpdateOp(boolean draftStatus) {
+      this.draftStatus = draftStatus;
+    }
+
+    @Override
+    public boolean updateChange(BatchUpdate.ChangeContext ctx)
+        throws Exception {
+      Collection<PatchSet> patchSets = psUtil.byChange(db, ctx.getNotes());
+
+      // Change status in database.
+      patchSets.forEach(patchSet -> patchSet.setDraft(draftStatus));
+      db.patchSets().update(patchSets);
+
+      // Change status in NoteDb.
+      PatchSetState patchSetState = draftStatus ? PatchSetState.DRAFT
+          : PatchSetState.PUBLISHED;
+      patchSets.stream()
+          .map(PatchSet::getId)
+          .map(ctx::getUpdate)
+          .forEach(changeUpdate ->
+              changeUpdate.setPatchSetState(patchSetState));
+
+      return true;
+    }
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
index ce7e8c9..a252aeb 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
@@ -66,7 +66,6 @@
     @GerritConfig(name = "gerrit.allProjects", value = "Root"),
     @GerritConfig(name = "gerrit.allUsers", value = "Users"),
     @GerritConfig(name = "gerrit.enableGwtUi", value = "true"),
-    @GerritConfig(name = "gerrit.enablePolyGerrit", value = "true"),
     @GerritConfig(name = "gerrit.reportBugText", value = "REPORT BUG"),
     @GerritConfig(name = "gerrit.reportBugUrl", value = "https://example.com/report"),
 
diff --git a/gerrit-extension-api/BUILD b/gerrit-extension-api/BUILD
index b66617a..d242495 100644
--- a/gerrit-extension-api/BUILD
+++ b/gerrit-extension-api/BUILD
@@ -1,3 +1,4 @@
+load('//lib/jgit:jgit.bzl', 'JGIT_DOC_URL')
 load('//tools/bzl:gwt.bzl', 'gwt_module')
 
 SRC = 'src/main/java/com/google/gerrit/extensions/'
@@ -52,4 +53,5 @@
   title = 'Gerrit Review Extension API Documentation',
   libs = [':api'],
   pkgs = ['com.google.gerrit.extensions'],
+  external_docs = [JGIT_DOC_URL],
 )
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index bd5dd69..629ad97 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -112,7 +112,7 @@
   void publish() throws RestApiException;
 
   /**
-   * Deletes a draft change.
+   * Deletes a change.
    */
   void delete() throws RestApiException;
 
diff --git a/gerrit-gwtui-common/BUCK b/gerrit-gwtui-common/BUCK
index d4d97a6..729b7e7 100644
--- a/gerrit-gwtui-common/BUCK
+++ b/gerrit-gwtui-common/BUCK
@@ -8,7 +8,6 @@
 ]
 DEPS = ['//lib/gwt:user']
 SRC = 'src/main/java/com/google/gerrit/'
-DIFFY = glob(['src/main/resources/com/google/gerrit/client/diffy*.png'])
 
 gwt_module(
   name = 'client',
@@ -36,9 +35,9 @@
   visibility = ['PUBLIC'],
 )
 
-prebuilt_jar(
+java_library(
   name = 'diffy_logo',
-  binary_jar = ':diffy_image_files_ln',
+  resources = glob(['src/main/resources/com/google/gerrit/client/diffy*.png']),
   deps = [
     '//lib:LICENSE-diffy',
     '//lib:LICENSE-CC-BY3.0-unported',
@@ -46,17 +45,6 @@
   visibility = ['PUBLIC'],
 )
 
-genrule(
-  name = 'diffy_image_files_ln',
-  cmd = 'ln -s $(location :diffy_image_files) $OUT',
-  out = 'diffy_images.jar',
-)
-
-java_library(
-  name = 'diffy_image_files',
-  resources = DIFFY,
-)
-
 java_test(
   name = 'client_tests',
   srcs = glob(['src/test/java/**/*.java']),
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.java
index 63de389..99f3b9f2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.java
@@ -51,6 +51,6 @@
   String abandoned();
 
   String deleteChangeEdit();
-  String deleteDraftChange();
+  String deleteChange();
   String deleteDraftRevision();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.properties
index 5b4f18f..dd4760d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.properties
@@ -34,5 +34,5 @@
 deleteChangeEdit = Delete Change Edit?\n\
   \n\
   All changes made in the edit revision will be lost.
-deleteDraftChange = Delete Draft Change?
+deleteChange = Delete Change?
 deleteDraftRevision = Delete Draft Revision?
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 436e0c3..fa3855e 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
@@ -495,15 +495,13 @@
   }
 
   private void initChangeAction(ChangeInfo info) {
-    if (info.status() == Status.DRAFT) {
-      NativeMap<ActionInfo> actions = info.hasActions()
-          ? info.actions()
-          : NativeMap.<ActionInfo> create();
-      actions.copyKeysIntoChildren("id");
-      if (actions.containsKey("/")) {
-        deleteChange.setVisible(true);
-        deleteChange.setTitle(actions.get("/").title());
-      }
+    NativeMap<ActionInfo> actions = info.hasActions()
+        ? info.actions()
+        : NativeMap.create();
+    actions.copyKeysIntoChildren("id");
+    if (actions.containsKey("/")) {
+      deleteChange.setVisible(true);
+      deleteChange.setTitle(actions.get("/").title());
     }
   }
 
@@ -679,7 +677,7 @@
 
   @UiHandler("deleteChange")
   void onDeleteChange(@SuppressWarnings("unused") ClickEvent e) {
-    if (Window.confirm(Resources.C.deleteDraftChange())) {
+    if (Window.confirm(Resources.C.deleteChange())) {
       DraftActions.delete(changeId, publish, deleteRevision, deleteChange);
     }
   }
diff --git a/gerrit-pgm/BUCK b/gerrit-pgm/BUCK
index 5f2ef43..d5abf99 100644
--- a/gerrit-pgm/BUCK
+++ b/gerrit-pgm/BUCK
@@ -130,7 +130,6 @@
   '//lib:gwtorm',
   '//lib:protobuf',
   '//lib:servlet-api-3_1',
-  '//lib/auto:auto-value',
   '//lib/prolog:cafeteria',
   '//lib/prolog:compiler',
   '//lib/prolog:runtime',
diff --git a/gerrit-pgm/BUILD b/gerrit-pgm/BUILD
index 8e3cbcf..4f2b609 100644
--- a/gerrit-pgm/BUILD
+++ b/gerrit-pgm/BUILD
@@ -120,7 +120,6 @@
   '//lib:gwtorm',
   '//lib:protobuf',
   '//lib:servlet-api-3_1-without-neverlink',
-  '//lib/auto:auto-value',
   '//lib/prolog:cafeteria',
   '//lib/prolog:compiler',
   '//lib/prolog:runtime',
@@ -142,6 +141,7 @@
   resources = glob([RSRCS + '*']),
   deps = DEPS + REST_PGM_DEPS + [ # We want all these deps to be provided_deps
     '//gerrit-launcher:launcher',
+    '//lib/auto:auto-value',
   ],
   visibility = ['//visibility:public'],
 )
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
index eec700a..7d6dc32 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
@@ -29,7 +29,7 @@
 import com.google.gerrit.pgm.init.api.Section;
 import com.google.gerrit.pgm.init.api.Section.Factory;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.mail.send.OutgoingEmail;
+import com.google.gerrit.server.mail.EmailModule;
 import com.google.inject.Binding;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
@@ -156,7 +156,7 @@
 
   private void extractMailExample(String orig) throws Exception {
     Path ex = site.mail_dir.resolve(orig + ".example");
-    extract(ex, OutgoingEmail.class, orig);
+    extract(ex, EmailModule.class, orig);
     chmod(0444, ex);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java
index c7a2241..3d966d2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 
@@ -31,8 +32,8 @@
   @Nullable
   AccountGroup get(AccountGroup.UUID uuid);
 
-  /** @return sorted iteration of groups. */
-  Iterable<AccountGroup> all();
+  /** @return sorted list of groups. */
+  ImmutableList<AccountGroup> all();
 
   /** Notify the cache that a new group was constructed. */
   void onCreateGroup(AccountGroup.NameKey newGroupName);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
index a2f4b65..4f5cc2b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
@@ -16,6 +16,7 @@
 
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupName;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -32,7 +33,6 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
 import java.util.concurrent.ExecutionException;
@@ -151,12 +151,12 @@
   }
 
   @Override
-  public Iterable<AccountGroup> all() {
+  public ImmutableList<AccountGroup> all() {
     try (ReviewDb db = schema.open()) {
-      return Collections.unmodifiableList(db.accountGroups().all().toList());
+      return ImmutableList.copyOf(db.accountGroups().all());
     } catch (OrmException e) {
       log.warn("Cannot list internal groups", e);
-      return Collections.emptyList();
+      return ImmutableList.of();
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java
index 84660ec..2028654 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java
@@ -28,7 +28,6 @@
 import org.eclipse.jgit.lib.ObjectId;
 
 import java.util.Collection;
-import java.util.stream.StreamSupport;
 
 /** Implementation of GroupBackend for the internal group system. */
 @Singleton
@@ -68,7 +67,7 @@
   @Override
   public Collection<GroupReference> suggest(final String name,
       final ProjectControl project) {
-    return StreamSupport.stream(groupCache.all().spliterator(), false)
+    return groupCache.all().stream()
         .filter(group ->
             // startsWithIgnoreCase && isVisible
             group.getName().regionMatches(true, 0, name, 0, name.length())
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index 31c70d9..a265160 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -45,7 +45,7 @@
 import com.google.gerrit.server.change.Check;
 import com.google.gerrit.server.change.CreateMergePatchSet;
 import com.google.gerrit.server.change.DeleteAssignee;
-import com.google.gerrit.server.change.DeleteDraftChange;
+import com.google.gerrit.server.change.DeleteChange;
 import com.google.gerrit.server.change.GetAssignee;
 import com.google.gerrit.server.change.GetHashtags;
 import com.google.gerrit.server.change.GetPastAssignees;
@@ -98,7 +98,7 @@
   private final Provider<SubmittedTogether> submittedTogether;
   private final PublishDraftPatchSet.CurrentRevision
     publishDraftChange;
-  private final DeleteDraftChange deleteDraftChange;
+  private final DeleteChange deleteChange;
   private final GetTopic getTopic;
   private final PutTopic putTopic;
   private final PostReviewers postReviewers;
@@ -129,7 +129,7 @@
       CreateMergePatchSet updateByMerge,
       Provider<SubmittedTogether> submittedTogether,
       PublishDraftPatchSet.CurrentRevision publishDraftChange,
-      DeleteDraftChange deleteDraftChange,
+      DeleteChange deleteChange,
       GetTopic getTopic,
       PutTopic putTopic,
       PostReviewers postReviewers,
@@ -159,7 +159,7 @@
     this.updateByMerge = updateByMerge;
     this.submittedTogether = submittedTogether;
     this.publishDraftChange = publishDraftChange;
-    this.deleteDraftChange = deleteDraftChange;
+    this.deleteChange = deleteChange;
     this.getTopic = getTopic;
     this.putTopic = putTopic;
     this.postReviewers = postReviewers;
@@ -324,7 +324,7 @@
   @Override
   public void delete() throws RestApiException {
     try {
-      deleteDraftChange.apply(change, null);
+      deleteChange.apply(change, null);
     } catch (UpdateException e) {
       throw new RestApiException("Cannot delete change", e);
     }
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 30ed82f..20e5b9d 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
@@ -658,7 +658,7 @@
     public boolean updateChange(ChangeContext ctx)
         throws OrmException, PatchSetInfoNotAvailableException {
       // Delete dangling key references.
-      ReviewDb db = DeleteDraftChangeOp.unwrap(ctx.getDb());
+      ReviewDb db = DeleteChangeOp.unwrap(ctx.getDb());
       accountPatchReviewStore.get().clearReviewed(psId);
       db.changeMessages().delete(
           db.changeMessages().byChange(psId.getParentKey()));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChange.java
similarity index 75%
rename from gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChange.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChange.java
index a125272..18d7074 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChange.java
@@ -22,10 +22,11 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.change.DeleteDraftChange.Input;
+import com.google.gerrit.server.change.DeleteChange.Input;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.UpdateException;
+import com.google.gerrit.server.project.ChangeControl;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -34,25 +35,25 @@
 import org.eclipse.jgit.lib.Config;
 
 @Singleton
-public class DeleteDraftChange implements
+public class DeleteChange implements
     RestModifyView<ChangeResource, Input>, UiAction<ChangeResource> {
   public static class Input {
   }
 
   private final Provider<ReviewDb> db;
   private final BatchUpdate.Factory updateFactory;
-  private final Provider<DeleteDraftChangeOp> opProvider;
+  private final Provider<DeleteChangeOp> opProvider;
   private final boolean allowDrafts;
 
   @Inject
-  public DeleteDraftChange(Provider<ReviewDb> db,
+  public DeleteChange(Provider<ReviewDb> db,
       BatchUpdate.Factory updateFactory,
-      Provider<DeleteDraftChangeOp> opProvider,
+      Provider<DeleteChangeOp> opProvider,
       @GerritServerConfig Config cfg) {
     this.db = db;
     this.updateFactory = updateFactory;
     this.opProvider = opProvider;
-    this.allowDrafts = DeleteDraftChangeOp.allowDrafts(cfg);
+    this.allowDrafts = DeleteChangeOp.allowDrafts(cfg);
   }
 
   @Override
@@ -71,14 +72,21 @@
   @Override
   public UiAction.Description getDescription(ChangeResource rsrc) {
     try {
+      Change.Status status = rsrc.getChange().getStatus();
+      ChangeControl changeControl = rsrc.getControl();
+      boolean visible = isActionAllowed(changeControl, status)
+          && changeControl.canDelete(db.get(), status);
       return new UiAction.Description()
         .setLabel("Delete")
-        .setTitle("Delete draft change " + rsrc.getId())
-        .setVisible(allowDrafts
-            && rsrc.getChange().getStatus() == Status.DRAFT
-            && rsrc.getControl().canDeleteDraft(db.get()));
+        .setTitle("Delete change " + rsrc.getId())
+        .setVisible(visible);
     } catch (OrmException e) {
       throw new IllegalStateException(e);
     }
   }
+
+  private boolean isActionAllowed(ChangeControl changeControl,
+      Status status) {
+    return status != Status.DRAFT || allowDrafts || changeControl.isAdmin();
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChangeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeOp.java
similarity index 82%
rename from gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChangeOp.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeOp.java
index 4ed6a25..0e77a50 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChangeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeOp.java
@@ -44,7 +44,7 @@
 import java.io.IOException;
 import java.util.Collection;
 
-class DeleteDraftChangeOp extends BatchUpdate.Op {
+class DeleteChangeOp extends BatchUpdate.Op {
   static boolean allowDrafts(Config cfg) {
     return cfg.getBoolean("change", "allowDrafts", true);
   }
@@ -68,7 +68,7 @@
   private Change.Id id;
 
   @Inject
-  DeleteDraftChangeOp(PatchSetUtil psUtil,
+  DeleteChangeOp(PatchSetUtil psUtil,
       StarredChangesUtil starredChangesUtil,
       DynamicItem<AccountPatchReviewStore> accountPatchReviewStore,
       @GerritServerConfig Config cfg) {
@@ -82,16 +82,18 @@
   public boolean updateChange(ChangeContext ctx) throws RestApiException,
       OrmException, IOException, NoSuchChangeException {
     checkState(ctx.getOrder() == BatchUpdate.Order.DB_BEFORE_REPO,
-        "must use DeleteDraftChangeOp with DB_BEFORE_REPO");
-    checkState(id == null, "cannot reuse DeleteDraftChangeOp");
+        "must use DeleteChangeOp with DB_BEFORE_REPO");
+    checkState(id == null, "cannot reuse DeleteChangeOp");
 
     id = ctx.getChange().getId();
     Collection<PatchSet> patchSets = psUtil.byChange(ctx.getDb(),
         ctx.getNotes());
 
     ensureDeletable(ctx, id, patchSets);
-    deleteChangeElementsFromDb(ctx, id);
+    // Cleaning up is only possible as long as the change and its elements are
+    // still part of the database.
     cleanUpReferences(ctx, id, patchSets);
+    deleteChangeElementsFromDb(ctx, id);
 
     ctx.deleteChange();
     return true;
@@ -100,19 +102,25 @@
   private void ensureDeletable(ChangeContext ctx, Change.Id id,
       Collection<PatchSet> patchSets) throws ResourceConflictException,
       MethodNotAllowedException, OrmException, AuthException {
-    if (ctx.getChange().getStatus() != Change.Status.DRAFT) {
-      throw new ResourceConflictException("Change is not a draft: " + id);
+    Change.Status status = ctx.getChange().getStatus();
+    if (status == Change.Status.MERGED) {
+      throw new MethodNotAllowedException("Deleting merged change " + id
+          + " is not allowed");
     }
-    if (!allowDrafts) {
-      throw new MethodNotAllowedException("Draft workflow is disabled");
+
+    if (!ctx.getControl().canDelete(ctx.getDb(), status)) {
+      throw new AuthException("Deleting change " + id + " is not permitted");
     }
-    if (!ctx.getControl().canDeleteDraft(ctx.getDb())) {
-      throw new AuthException("Not permitted to delete this draft change");
-    }
-    for (PatchSet ps : patchSets) {
-      if (!ps.isDraft()) {
-        throw new ResourceConflictException("Cannot delete draft change " + id
-            + ": patch set " + ps.getPatchSetId() + " is not a draft");
+
+    if (status == Change.Status.DRAFT) {
+      if (!allowDrafts && !ctx.getControl().isAdmin()) {
+        throw new MethodNotAllowedException("Draft workflow is disabled");
+      }
+      for (PatchSet ps : patchSets) {
+        if (!ps.isDraft()) {
+          throw new ResourceConflictException("Cannot delete draft change " + id
+              + ": patch set " + ps.getPatchSetId() + " is not a draft");
+        }
       }
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
index 1cd8726..e473e39 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
@@ -59,7 +59,7 @@
   private final BatchUpdate.Factory updateFactory;
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final PatchSetUtil psUtil;
-  private final Provider<DeleteDraftChangeOp> deleteChangeOpProvider;
+  private final Provider<DeleteChangeOp> deleteChangeOpProvider;
   private final DynamicItem<AccountPatchReviewStore> accountPatchReviewStore;
   private final boolean allowDrafts;
 
@@ -68,7 +68,7 @@
       BatchUpdate.Factory updateFactory,
       PatchSetInfoFactory patchSetInfoFactory,
       PatchSetUtil psUtil,
-      Provider<DeleteDraftChangeOp> deleteChangeOpProvider,
+      Provider<DeleteChangeOp> deleteChangeOpProvider,
       DynamicItem<AccountPatchReviewStore> accountPatchReviewStore,
       @GerritServerConfig Config cfg) {
     this.db = db;
@@ -97,7 +97,7 @@
 
     private Collection<PatchSet> patchSetsBeforeDeletion;
     private PatchSet patchSet;
-    private DeleteDraftChangeOp deleteChangeOp;
+    private DeleteChangeOp deleteChangeOp;
 
     private Op(PatchSet.Id psId) {
       this.psId = psId;
@@ -116,7 +116,7 @@
       if (!allowDrafts) {
         throw new MethodNotAllowedException("Draft workflow is disabled");
       }
-      if (!ctx.getControl().canDeleteDraft(ctx.getDb())) {
+      if (!ctx.getControl().canDelete(ctx.getDb(), Change.Status.DRAFT)) {
         throw new AuthException("Not permitted to delete this draft patch set");
       }
 
@@ -146,8 +146,8 @@
       psUtil.delete(ctx.getDb(), ctx.getUpdate(patchSet.getId()), patchSet);
 
       accountPatchReviewStore.get().clearReviewed(psId);
-      // Use the unwrap from DeleteDraftChangeOp to handle BatchUpdateReviewDb.
-      ReviewDb db = DeleteDraftChangeOp.unwrap(ctx.getDb());
+      // Use the unwrap from DeleteChangeOp to handle BatchUpdateReviewDb.
+      ReviewDb db = DeleteChangeOp.unwrap(ctx.getDb());
       db.changeMessages().delete(db.changeMessages().byPatchSet(psId));
       db.patchComments().delete(db.patchComments().byPatchSet(psId));
       db.patchSetApprovals().delete(db.patchSetApprovals().byPatchSet(psId));
@@ -195,7 +195,7 @@
             rsrc.getPatchSet().getPatchSetId()))
         .setVisible(allowDrafts
             && rsrc.getPatchSet().isDraft()
-            && rsrc.getControl().canDeleteDraft(db.get())
+            && rsrc.getControl().canDelete(db.get(), Change.Status.DRAFT)
             && psCount > 1);
     } catch (OrmException e) {
       throw new IllegalStateException(e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
index bb76084..9ff9833 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
@@ -68,7 +68,7 @@
     post(CHANGE_KIND, "check").to(Check.class);
     put(CHANGE_KIND, "topic").to(PutTopic.class);
     delete(CHANGE_KIND, "topic").to(PutTopic.class);
-    delete(CHANGE_KIND).to(DeleteDraftChange.class);
+    delete(CHANGE_KIND).to(DeleteChange.class);
     post(CHANGE_KIND, "abandon").to(Abandon.class);
     post(CHANGE_KIND, "hashtags").to(PostHashtags.class);
     post(CHANGE_KIND, "publish").to(PublishDraftPatchSet.CurrentRevision.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritOptions.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritOptions.java
index c181f79..ab4b463 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritOptions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritOptions.java
@@ -33,7 +33,7 @@
       boolean forcePolyGerritDev) {
     this.slave = slave;
     this.enablePolyGerrit = forcePolyGerritDev
-        || cfg.getBoolean("gerrit", null, "enablePolyGerrit", false);
+        || cfg.getBoolean("gerrit", null, "enablePolyGerrit", true);
     this.enableGwtUi = cfg.getBoolean("gerrit", null, "enableGwtUi", true);
     this.forcePolyGerritDev = forcePolyGerritDev;
     this.headless = headless || (!enableGwtUi && !enablePolyGerrit);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java
index d23cac4..85c3d15 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java
@@ -45,6 +45,7 @@
 import org.kohsuke.args4j.Option;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.HashMap;
@@ -314,11 +315,11 @@
     return groups;
   }
 
-  private List<AccountGroup> filterGroups(final Iterable<AccountGroup> groups) {
-    final List<AccountGroup> filteredGroups = new ArrayList<>();
-    final boolean isAdmin =
+  private List<AccountGroup> filterGroups(Collection<AccountGroup> groups) {
+    List<AccountGroup> filteredGroups = new ArrayList<>(groups.size());
+    boolean isAdmin =
         identifiedUser.get().getCapabilities().canAdministrateServer();
-    for (final AccountGroup group : groups) {
+    for (AccountGroup group : groups) {
       if (!Strings.isNullOrEmpty(matchSubstring)) {
         if (!group.getName().toLowerCase(Locale.US)
             .contains(matchSubstring.toLowerCase(Locale.US))) {
@@ -326,7 +327,7 @@
         }
       }
       if (!isAdmin) {
-        final GroupControl c = groupControlFactory.controlFor(group);
+        GroupControl c = groupControlFactory.controlFor(group);
         if (!c.isVisible()) {
           continue;
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RepoSequence.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RepoSequence.java
index 8172906..e4a6f7c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RepoSequence.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RepoSequence.java
@@ -83,7 +83,8 @@
         .withStopStrategy(StopStrategies.stopAfterDelay(30, TimeUnit.SECONDS));
   }
 
-  private static Retryer<RefUpdate.Result> RETRYER = retryerBuilder().build();
+  private static final Retryer<RefUpdate.Result> RETRYER =
+      retryerBuilder().build();
 
   private final GitRepositoryManager repoManager;
   private final Project.NameKey projectName;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
index 5cc461f..c4d8dcd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
@@ -261,10 +261,23 @@
         && isVisible(db);
   }
 
-  /** Can this user delete this draft change or any draft patch set of this change? */
-  public boolean canDeleteDraft(final ReviewDb db) throws OrmException {
-    return (isOwner() || getRefControl().canDeleteDrafts())
-        && isVisible(db);
+  /** Can this user delete this change or any patch set of this change? */
+  public boolean canDelete(ReviewDb db, Change.Status status)
+      throws OrmException {
+    if (!isVisible(db)) {
+      return false;
+    }
+
+    switch (status) {
+      case DRAFT:
+        return (isOwner() || getRefControl().canDeleteDrafts());
+      case NEW:
+      case ABANDONED:
+        return isAdmin();
+      case MERGED:
+      default:
+        return false;
+    }
   }
 
   /** Can this user rebase this change? */
@@ -377,6 +390,10 @@
     return false;
   }
 
+  public boolean isAdmin() {
+    return getUser().getCapabilities().canAdministrateServer();
+  }
+
   /** @return true if the user is allowed to remove this reviewer. */
   public boolean canRemoveReviewer(PatchSetApproval approval) {
     return canRemoveReviewer(approval.getAccountId(), approval.getValue());
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 eaec021..3387f06 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
@@ -17,11 +17,10 @@
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.server.ApprovalsUtil.sortApprovals;
+import static java.util.stream.Collectors.toList;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.base.MoreObjects;
-import com.google.common.base.Predicate;
-import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ImmutableMultimap;
@@ -94,6 +93,8 @@
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
 
 public class ChangeData {
   private static final int BATCH_SIZE = 50;
@@ -108,12 +109,8 @@
   }
 
   public static Map<Change.Id, ChangeData> asMap(List<ChangeData> changes) {
-    Map<Change.Id, ChangeData> result =
-        Maps.newHashMapWithExpectedSize(changes.size());
-    for (ChangeData cd : changes) {
-      result.put(cd.getId(), cd);
-    }
-    return result;
+    return changes.stream().collect(
+        Collectors.toMap(ChangeData::getId, cd -> cd));
   }
 
   public static void ensureChangeLoaded(Iterable<ChangeData> changes)
@@ -898,14 +895,14 @@
    * @throws OrmException an error occurred reading the database.
    */
   public Collection<PatchSet> visiblePatchSets() throws OrmException {
-    Predicate<PatchSet> predicate = ps -> {
+    Predicate<? super PatchSet> predicate = ps -> {
       try {
         return changeControl().isPatchVisible(ps, db);
       } catch (OrmException e) {
         return false;
       }
     };
-    return FluentIterable.from(patchSets()).filter(predicate).toList();
+    return patchSets().stream().filter(predicate).collect(toList());
   }
 
   public void setPatchSets(Collection<PatchSet> patchSets) {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
index 5686dc3..1ff5aac 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
@@ -36,6 +36,8 @@
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.server.schema.SchemaCreator;
+import com.google.gerrit.server.util.ManualRequestContext;
+import com.google.gerrit.server.util.OneOffRequestContext;
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.testutil.ConfigSuite;
@@ -97,6 +99,9 @@
   @Inject
   protected ThreadLocalRequestContext requestContext;
 
+  @Inject
+  protected OneOffRequestContext oneOffRequestContext;
+
   protected LifecycleManager lifecycle;
   protected ReviewDb db;
   protected AccountInfo currentUserInfo;
@@ -405,18 +410,20 @@
 
   private Account.Id createAccount(String username, String fullName,
       String email, boolean active) throws Exception {
-    Account.Id id =
-        accountManager.authenticate(AuthRequest.forUser(username)).getAccountId();
-    if (email != null) {
-      accountManager.link(id, AuthRequest.forEmail(email));
+    try (ManualRequestContext ctx = oneOffRequestContext.open()) {
+      Account.Id id =
+          accountManager.authenticate(AuthRequest.forUser(username)).getAccountId();
+      if (email != null) {
+        accountManager.link(id, AuthRequest.forEmail(email));
+      }
+      Account a = db.accounts().get(id);
+      a.setFullName(fullName);
+      a.setPreferredEmail(email);
+      a.setActive(active);
+      db.accounts().update(ImmutableList.of(a));
+      accountCache.evict(id);
+      return id;
     }
-    Account a = db.accounts().get(id);
-    a.setFullName(fullName);
-    a.setPreferredEmail(email);
-    a.setActive(active);
-    db.accounts().update(ImmutableList.of(a));
-    accountCache.evict(id);
-    return id;
   }
 
   private void addEmails(AccountInfo account, String... emails)
diff --git a/lib/JGIT_VERSION b/lib/JGIT_VERSION
index b7f7c84..569cf59 100644
--- a/lib/JGIT_VERSION
+++ b/lib/JGIT_VERSION
@@ -1,6 +1,4 @@
+include_defs('//lib/jgit/jgit.bzl')
 include_defs('//lib/maven.defs')
 
 REPO = MAVEN_CENTRAL # Leave here even if set to MAVEN_CENTRAL.
-VERS = '4.5.0.201609210915-r'
-DOC_VERS = VERS # Set to VERS unless using a snapshot
-JGIT_DOC_URL="http://download.eclipse.org/jgit/site/" + DOC_VERS + "/apidocs"
diff --git a/lib/jgit/jgit.bzl b/lib/jgit/jgit.bzl
new file mode 100644
index 0000000..996e44d
--- /dev/null
+++ b/lib/jgit/jgit.bzl
@@ -0,0 +1,3 @@
+JGIT_VERS = '4.5.0.201609210915-r'
+DOC_VERS = JGIT_VERS # Set to VERS unless using a snapshot
+JGIT_DOC_URL="http://download.eclipse.org/jgit/site/" + DOC_VERS + "/apidocs"
diff --git a/lib/jgit/org.eclipse.jgit.archive/BUCK b/lib/jgit/org.eclipse.jgit.archive/BUCK
index 7c967b3..02f99c6 100644
--- a/lib/jgit/org.eclipse.jgit.archive/BUCK
+++ b/lib/jgit/org.eclipse.jgit.archive/BUCK
@@ -3,7 +3,7 @@
 
 maven_jar(
   name = 'jgit-archive',
-  id = 'org.eclipse.jgit:org.eclipse.jgit.archive:' + VERS,
+  id = 'org.eclipse.jgit:org.eclipse.jgit.archive:' + JGIT_VERS,
   sha1 = '2db2e7666672a31fa41b7e1dadcba51df6d30954',
   license = 'jgit',
   repository = REPO,
diff --git a/lib/jgit/org.eclipse.jgit.http.server/BUCK b/lib/jgit/org.eclipse.jgit.http.server/BUCK
index 06865cb..5dd3777 100644
--- a/lib/jgit/org.eclipse.jgit.http.server/BUCK
+++ b/lib/jgit/org.eclipse.jgit.http.server/BUCK
@@ -3,7 +3,7 @@
 
 maven_jar(
   name = 'jgit-servlet',
-  id = 'org.eclipse.jgit:org.eclipse.jgit.http.server:' + VERS,
+  id = 'org.eclipse.jgit:org.eclipse.jgit.http.server:' + JGIT_VERS,
   sha1 = '6e36638888918d9941dddec7e2abe1f162cc74d9',
   license = 'jgit',
   repository = REPO,
diff --git a/lib/jgit/org.eclipse.jgit.junit/BUCK b/lib/jgit/org.eclipse.jgit.junit/BUCK
index 77b637a..e5cd5c0 100644
--- a/lib/jgit/org.eclipse.jgit.junit/BUCK
+++ b/lib/jgit/org.eclipse.jgit.junit/BUCK
@@ -3,7 +3,7 @@
 
 maven_jar(
   name = 'junit',
-  id = 'org.eclipse.jgit:org.eclipse.jgit.junit:' + VERS,
+  id = 'org.eclipse.jgit:org.eclipse.jgit.junit:' + JGIT_VERS,
   sha1 = 'e8fb1d81f588c3174a9730bdecdbde9faa04140a',
   license = 'DO_NOT_DISTRIBUTE',
   repository = REPO,
diff --git a/lib/jgit/org.eclipse.jgit/BUCK b/lib/jgit/org.eclipse.jgit/BUCK
index 458703c..74338de 100644
--- a/lib/jgit/org.eclipse.jgit/BUCK
+++ b/lib/jgit/org.eclipse.jgit/BUCK
@@ -3,7 +3,7 @@
 
 maven_jar(
   name = 'jgit',
-  id = 'org.eclipse.jgit:org.eclipse.jgit:' + VERS,
+  id = 'org.eclipse.jgit:org.eclipse.jgit:' + JGIT_VERS,
   bin_sha1 = '3e3d0b73dcf4ad649f37758ea8502d92f3d299de',
   src_sha1 = 'fc352952db91a4046e4b832145eb2dc8afce8db1',
   license = 'jgit',
diff --git a/plugins/replication b/plugins/replication
index 3212bcd..bc37211 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 3212bcd4f2c0dc791a99af97ee98df70746f2306
+Subproject commit bc37211df3cc7b7516974142d78232197c49ce29
diff --git a/tools/bzl/javadoc.bzl b/tools/bzl/javadoc.bzl
index c479e2c..dcaa611 100644
--- a/tools/bzl/javadoc.bzl
+++ b/tools/bzl/javadoc.bzl
@@ -26,6 +26,7 @@
   transitive_jar_paths = [j.path for j in transitive_jar_set]
   dir = ctx.outputs.zip.path + ".dir"
   source = ctx.outputs.zip.path + ".source"
+  external_docs = ["http://docs.oracle.com/javase/8/docs/api"] + ctx.attr.external_docs
   cmd = [
       "rm -rf %s" % source,
       "mkdir %s" % source,
@@ -41,7 +42,7 @@
         "-notimestamp",
         "-quiet",
         "-windowtitle '%s'" % ctx.attr.title,
-        "-link", "http://docs.oracle.com/javase/8/docs/api",
+        " ".join(['-link %s' % url for url in external_docs]),
         "-sourcepath %s" % source,
         "-subpackages ",
         ":".join(ctx.attr.pkgs),
@@ -61,6 +62,7 @@
       "libs": attr.label_list(allow_files = False),
       "pkgs": attr.string_list(),
       "title": attr.string(),
+      "external_docs": attr.string_list(),
       "_javadoc": attr.label(
         default = Label("@local_jdk//:bin/javadoc"),
         single_file = True,
diff --git a/tools/bzl/pkg_war.bzl b/tools/bzl/pkg_war.bzl
index aa7d07f..a9d3e2e 100644
--- a/tools/bzl/pkg_war.bzl
+++ b/tools/bzl/pkg_war.bzl
@@ -53,10 +53,11 @@
   ]
 
 def _make_war(input_dir, output):
-  return ''.join([
-    '(root=$(pwd) && ',
-    'cd %s && ' % input_dir,
-    'zip -9qr ${root}/%s .)' % (output.path),
+  return '(%s)' % ' && '.join([
+    'root=$(pwd)',
+    'cd %s' % input_dir,
+    "find . -exec touch -t 198001010000 '{}' ';' 2> /dev/null",
+    'zip -9qr ${root}/%s .' % (output.path),
   ])
 
 def _war_impl(ctx):
@@ -126,8 +127,11 @@
 
 def pkg_war(name, ui = 'ui_optdbg', context = [], **kwargs):
   ui_deps = []
-  if ui:
+  if ui == 'polygerrit' or ui == 'ui_optdbg' or ui == 'ui_optdbg_r':
+    ui_deps.append('//polygerrit-ui/app:polygerrit_ui')
+  if ui != 'polygerrit':
     ui_deps.append('//gerrit-gwtui:%s' % ui)
+
   _pkg_war(
     name = name,
     libs = LIBS,
diff --git a/tools/bzl/setup-intellij.sh b/tools/bzl/setup-intellij.sh
new file mode 100644
index 0000000..57df8c8
--- /dev/null
+++ b/tools/bzl/setup-intellij.sh
@@ -0,0 +1,72 @@
+#!/bin/sh
+
+# This script sets up a 'bazel_external' libraries for maven jars and Auto classes.
+#
+# To use:
+#
+# * Start IntelliJ
+# * Go to "project structure" (Ctrl-Alt-Shift-S),
+# * Go to "Libraries",
+# * Right click "bazel_external"
+# * Select "Add to Module"
+# * Select all modules, click OK
+# * Click "Apply"
+
+mkdir -p .idea/libraries/
+dest=.idea/libraries/bazel_external.xml
+
+cat <<EOF > $dest
+ <component name="libraryTable">
+  <library name="bazel_external">
+    <CLASSES>
+EOF
+
+for jar in $(bazel query --nohost_deps --output=location 'kind(file,deps(kind(java_import,deps(//...))))' | grep 'source file .*jar$' | sed 's|/BUILD:[0-9]*:[0-9]*: source file [^:]*:|/|' ); do
+cat <<EOF  >> $dest
+      <root url="jar://$jar!/" />
+EOF
+done
+
+cat <<EOF >> $dest
+    </CLASSES>
+    <JAVADOC />
+    <SOURCES />
+  </library>
+</component>
+EOF
+
+dest=.idea/libraries/bazel_autogen.xml
+cat <<EOF > $dest
+<component name="libraryTable">
+  <library name="bazel_autogen">
+    <CLASSES />
+    <JAVADOC />
+    <SOURCES>
+EOF
+
+not_found=""
+
+for dep in $(bazel query 'rdeps(//...,//lib/auto:auto-value,1) - //lib/auto:auto-value'); do
+    root=$(echo $dep | sed 's|//\(.*\):\(.*\)|bazel-bin/\1/_javac/\2/lib\2_sourcegenfiles|g')
+    if [[ ! -d $root ]]; then
+        # for some reason, tests don't have the "lib" prefix.
+        root=$(echo $dep | sed 's|//\(.*\):\(.*\)|bazel-bin/\1/_javac/\2/\2_sourcegenfiles|g')
+    fi
+    if [[ ! -d $root ]]; then
+        not_found="$not_found $root"
+    fi
+cat <<EOF  >> $dest
+      <root url="file://\$PROJECT_DIR\$/$root" />
+EOF
+done
+
+cat <<EOF >> $dest
+    </SOURCES>
+  </library>
+</component>
+EOF
+
+
+if [[ -n "$not_found" ]]; then
+    echo "some generated roots were missing. Did you run 'bazel build' yet?"
+fi