Merge "Add "Start review" workflow using work_in_progress"
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index da30264..a8aedc1 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -2166,14 +2166,23 @@
 [[mark-private]]
 === Mark Private
 --
-'PUT /changes/link:#change-id[\{change-id\}]/private'
+'POST /changes/link:#change-id[\{change-id\}]/private'
 --
 
-Marks the change to be private. Note users can only mark own changes as private.
+Marks the change to be private. Changes may only be marked private by the
+owner or site administrators.
+
+A message can be specified in the request body inside a
+link:#private-input[PrivateInput] entity.
 
 .Request
 ----
-  PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/private HTTP/1.0
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/private HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "message": "After this security fix has been released we can make it public now."
+  }
 ----
 
 .Response
@@ -2192,9 +2201,17 @@
 Marks the change to be non-private. Note users can only unmark own private
 changes.
 
+A message can be specified in the request body inside a
+link:#private-input[PrivateInput] entity.
+
 .Request
 ----
   DELETE /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/private HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "message": "This is a security fix that must not be public."
+  }
 ----
 
 .Response
@@ -2204,6 +2221,20 @@
 
 If the change was already not private, the response is "`409 Conflict`".
 
+Please note that some proxies prohibit request bodies for DELETE
+requests. In this case, if you want to set a message options, use a
+POST request:
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/private.delete HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "message": "This is a security fix that must not be public."
+  }
+----
+
 [[ignore]]
 === Ignore
 --
@@ -6221,6 +6252,17 @@
 identify the accounts that should be should be notified.
 |=======================
 
+[[private-input]]
+=== PrivateInput
+The `PrivateInput` entity contains information for changing the private
+flag on a change.
+
+[options="header",cols="1,^1,5"]
+|=======================
+|Field Name||Description
+|`message` |optional|Message describing why the private flag was changed.
+|=======================
+
 [[problem-info]]
 === ProblemInfo
 The `ProblemInfo` entity contains a description of a potential consistency problem
diff --git a/WORKSPACE b/WORKSPACE
index f136155..40e4000 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -6,9 +6,9 @@
 
 http_archive(
     name = "io_bazel_rules_closure",
-    strip_prefix = "rules_closure-0.4.1",
-    sha256 = "ba5e2e10cdc4027702f96e9bdc536c6595decafa94847d08ae28c6cb48225124",
-    url = "http://bazel-mirror.storage.googleapis.com/github.com/bazelbuild/rules_closure/archive/0.4.1.tar.gz",
+    sha256 = "af1f5a31b8306faed9d09a38c8e2c1d6afc4c4a2dada3b5de11cceae8c7f4596",
+    strip_prefix = "rules_closure-f68d4b5a55c04ee50a3196590dce1ca8e7dbf438",
+    url = "https://bazel-mirror.storage.googleapis.com/github.com/bazelbuild/rules_closure/archive/f68d4b5a55c04ee50a3196590dce1ca8e7dbf438.tar.gz",  # 2017-05-05
 )
 
 # File is specific to Polymer and copied from the Closure Github -- should be
@@ -24,18 +24,9 @@
 
 # Prevent redundant loading of dependencies.
 closure_repositories(
-    omit_aopalliance=True,
-    omit_args4j=True,
-    omit_jsr305=True,
-    omit_gson=True,
-    omit_guava=True,
-    omit_guice=True,
-    omit_soy=True,
-    omit_icu4j=True,
-    omit_asm=True,
-    omit_asm_analysis=True,
-    omit_asm_commons=True,
-    omit_asm_util=True,
+    omit_aopalliance = True,
+    omit_args4j = True,
+    omit_javax_inject = True,
 )
 
 ANTLR_VERS = "3.5.2"
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 a4a3c04..5d0f0ba 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
@@ -191,29 +191,55 @@
     String changeId = result.getChangeId();
     assertThat(gApi.changes().id(changeId).get().isPrivate).isNull();
 
-    gApi.changes().id(changeId).setPrivate(true);
+    gApi.changes().id(changeId).setPrivate(true, null);
     ChangeInfo info = gApi.changes().id(changeId).get();
     assertThat(info.isPrivate).isTrue();
     assertThat(Iterables.getLast(info.messages).message).isEqualTo("Set private");
     assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_PRIVATE);
 
-    gApi.changes().id(changeId).setPrivate(false);
+    gApi.changes().id(changeId).setPrivate(false, null);
     info = gApi.changes().id(changeId).get();
     assertThat(info.isPrivate).isNull();
     assertThat(Iterables.getLast(info.messages).message).isEqualTo("Unset private");
     assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_UNSET_PRIVATE);
+
+    String msg = "This is a security fix that must not be public.";
+    gApi.changes().id(changeId).setPrivate(true, msg);
+    info = gApi.changes().id(changeId).get();
+    assertThat(info.isPrivate).isTrue();
+    assertThat(Iterables.getLast(info.messages).message).isEqualTo("Set private\n\n" + msg);
+    assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_PRIVATE);
+
+    msg = "After this security fix has been released we can make it public now.";
+    gApi.changes().id(changeId).setPrivate(false, msg);
+    info = gApi.changes().id(changeId).get();
+    assertThat(info.isPrivate).isNull();
+    assertThat(Iterables.getLast(info.messages).message).isEqualTo("Unset private\n\n" + msg);
+    assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_UNSET_PRIVATE);
   }
 
   @Test
-  public void setPrivateByOtherUser() throws Exception {
+  public void administratorCanSetUserChangePrivate() throws Exception {
     TestRepository<InMemoryRepository> userRepo = cloneProject(project, user);
     PushOneCommit.Result result =
         pushFactory.create(db, user.getIdent(), userRepo).to("refs/for/master");
 
-    assertThat(gApi.changes().id(result.getChangeId()).get().isPrivate).isNull();
+    String changeId = result.getChangeId();
+    assertThat(gApi.changes().id(changeId).get().isPrivate).isNull();
+
+    gApi.changes().id(changeId).setPrivate(true, null);
+    setApiUser(user);
+    ChangeInfo info = gApi.changes().id(changeId).get();
+    assertThat(info.isPrivate).isTrue();
+  }
+
+  @Test
+  public void cannotSetOtherUsersChangePrivate() throws Exception {
+    PushOneCommit.Result result = createChange();
+    setApiUser(user);
     exception.expect(AuthException.class);
     exception.expectMessage("not allowed to mark private");
-    gApi.changes().id(result.getChangeId()).setPrivate(true);
+    gApi.changes().id(result.getChangeId()).setPrivate(true, null);
   }
 
   @Test
@@ -223,7 +249,7 @@
         pushFactory.create(db, user.getIdent(), userRepo).to("refs/for/master");
 
     setApiUser(user);
-    gApi.changes().id(result.getChangeId()).setPrivate(true);
+    gApi.changes().id(result.getChangeId()).setPrivate(true, null);
     // Owner can always access its private changes.
     assertThat(gApi.changes().id(result.getChangeId()).get().isPrivate).isTrue();
 
@@ -246,7 +272,7 @@
   @Test
   public void privateChangeOfOtherUserCanBeAccessedWithPermission() throws Exception {
     PushOneCommit.Result result = createChange();
-    gApi.changes().id(result.getChangeId()).setPrivate(true);
+    gApi.changes().id(result.getChangeId()).setPrivate(true, null);
 
     allow(Permission.VIEW_PRIVATE_CHANGES, REGISTERED_USERS, "refs/*");
     setApiUser(user);
@@ -596,6 +622,50 @@
   }
 
   @Test
+  public void rebaseNotAllowedWithoutPushPermission() throws Exception {
+    // Create two changes both with the same parent
+    PushOneCommit.Result r = createChange();
+    testRepo.reset("HEAD~1");
+    PushOneCommit.Result r2 = createChange();
+
+    // Approve and submit the first change
+    RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+    revision.review(ReviewInput.approve());
+    revision.submit();
+
+    grant(Permission.REBASE, project, "refs/heads/master", false, REGISTERED_USERS);
+    block(Permission.PUSH, REGISTERED_USERS, "refs/for/*");
+
+    // Rebase the second
+    String changeId = r2.getChangeId();
+    setApiUser(user);
+    exception.expect(AuthException.class);
+    exception.expectMessage("rebase not permitted");
+    gApi.changes().id(changeId).rebase();
+  }
+
+  @Test
+  public void rebaseNotAllowedForOwnerWithoutPushPermission() throws Exception {
+    // Create two changes both with the same parent
+    PushOneCommit.Result r = createChange();
+    testRepo.reset("HEAD~1");
+    PushOneCommit.Result r2 = createChange();
+
+    // Approve and submit the first change
+    RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+    revision.review(ReviewInput.approve());
+    revision.submit();
+
+    block(Permission.PUSH, REGISTERED_USERS, "refs/for/*");
+
+    // Rebase the second
+    String changeId = r2.getChangeId();
+    exception.expect(AuthException.class);
+    exception.expectMessage("rebase not permitted");
+    gApi.changes().id(changeId).rebase();
+  }
+
+  @Test
   public void publish() throws Exception {
     PushOneCommit.Result r = createChange("refs/drafts/master");
     assertThat(info(r.getChangeId()).status).isEqualTo(ChangeStatus.DRAFT);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
index 0abdde6..3b868dd 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -518,7 +518,7 @@
           lsRemoteCommand.call().stream().map(Ref::getName).collect(toList());
       assertWithMessage("Precondition violated").that(initialRefNames).contains(change3RefName);
 
-      gApi.changes().id(c3.getId().get()).setPrivate(true);
+      gApi.changes().id(c3.getId().get()).setPrivate(true, null);
 
       List<String> refNames = lsRemoteCommand.call().stream().map(Ref::getName).collect(toList());
       assertThat(refNames).doesNotContain(change3RefName);
@@ -538,7 +538,7 @@
           lsRemoteCommand.call().stream().map(Ref::getName).collect(toList());
       assertWithMessage("Precondition violated").that(initialRefNames).contains(change3RefName);
 
-      gApi.changes().id(c3.getId().get()).setPrivate(true);
+      gApi.changes().id(c3.getId().get()).setPrivate(true, null);
 
       List<String> refNames = lsRemoteCommand.call().stream().map(Ref::getName).collect(toList());
       assertThat(refNames).contains(change3RefName);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
index 04151e9..0ac263f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
@@ -30,9 +30,8 @@
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
 import java.io.InputStream;
+import java.nio.file.Files;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -547,10 +546,10 @@
     try (BinaryResult request = submitPreview(change1.getChangeId(), "tgz")) {
       assertThat(request.getContentType()).isEqualTo("application/x-gzip");
       tempfile = File.createTempFile("test", null);
-      request.writeTo(new FileOutputStream(tempfile));
+      request.writeTo(Files.newOutputStream(tempfile.toPath()));
     }
 
-    InputStream is = new GZIPInputStream(new FileInputStream(tempfile));
+    InputStream is = new GZIPInputStream(Files.newInputStream(tempfile.toPath()));
 
     List<String> untarredFiles = new ArrayList<>();
     try (TarArchiveInputStream tarInputStream =
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 8d7a452..3dabced 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
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.api.changes;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -85,7 +86,7 @@
 
   void move(MoveInput in) throws RestApiException;
 
-  void setPrivate(boolean value) throws RestApiException;
+  void setPrivate(boolean value, @Nullable String message) throws RestApiException;
 
   void setWorkInProgress(String message) throws RestApiException;
 
@@ -335,7 +336,7 @@
     }
 
     @Override
-    public void setPrivate(boolean value) {
+    public void setPrivate(boolean value, @Nullable String message) {
       throw new NotImplementedException();
     }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java
index 74668c1..234df60 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java
@@ -41,6 +41,11 @@
       @Override
       public void onSuccess(JavaScriptObject in) {
         UiResult result = asUiResult(in);
+        if (result == null) {
+          Gerrit.display(target);
+          return;
+        }
+
         if (result.alert() != null) {
           Window.alert(result.alert());
         }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
index f985f31..915867d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
@@ -122,11 +122,11 @@
   }
 
   public static void markPrivate(int id, AsyncCallback<JavaScriptObject> cb) {
-    change(id).view("private").put(cb);
+    change(id).view("private").post(PrivateInput.create(), cb);
   }
 
   public static void unmarkPrivate(int id, AsyncCallback<JavaScriptObject> cb) {
-    change(id).view("private").delete(cb);
+    change(id).view("private.delete").post(PrivateInput.create(), cb);
   }
 
   public static RestApi comments(int id) {
@@ -327,6 +327,16 @@
     protected CherryPickInput() {}
   }
 
+  private static class PrivateInput extends JavaScriptObject {
+    static PrivateInput create() {
+      return (PrivateInput) createObject();
+    }
+
+    final native void setMessage(String m) /*-{ this.message = m; }-*/;
+
+    protected PrivateInput() {}
+  }
+
   private static class RebaseInput extends JavaScriptObject {
     final native void setBase(String b) /*-{ this.base = b; }-*/;
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RecompileGwtUiFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RecompileGwtUiFilter.java
index 651d718..90aedbe 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RecompileGwtUiFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RecompileGwtUiFilter.java
@@ -16,9 +16,10 @@
 
 import com.google.gwtexpui.linker.server.UserAgentRule;
 import java.io.File;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.Enumeration;
 import java.util.HashSet;
@@ -102,7 +103,7 @@
         mkdir(rawtmp.getParentFile());
         rawtmp.deleteOnExit();
 
-        try (FileOutputStream rawout = new FileOutputStream(rawtmp);
+        try (OutputStream rawout = Files.newOutputStream(rawtmp.toPath());
             InputStream in = zf.getInputStream(ze)) {
           final byte[] buf = new byte[4096];
           int n;
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 a7af056..b22ba49 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
@@ -20,9 +20,9 @@
 
 import java.io.File;
 import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
@@ -293,7 +293,7 @@
   private static void extractJar(ZipFile zf, ZipEntry ze, SortedMap<String, URL> jars)
       throws IOException {
     File tmp = createTempFile(safeName(ze), ".jar");
-    try (FileOutputStream out = new FileOutputStream(tmp);
+    try (OutputStream out = Files.newOutputStream(tmp.toPath());
         InputStream in = zf.getInputStream(ze)) {
       byte[] buf = new byte[4096];
       int n;
@@ -414,7 +414,7 @@
     if (src != null) {
       try (InputStream in = src.getLocation().openStream()) {
         final File tmp = createTempFile("gerrit_", ".zip");
-        try (FileOutputStream out = new FileOutputStream(tmp)) {
+        try (OutputStream out = Files.newOutputStream(tmp.toPath())) {
           final byte[] buf = new byte[4096];
           int n;
           while ((n = in.read(buf, 0, buf.length)) > 0) {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtobufImport.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtobufImport.java
index fb524a3..d970856 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtobufImport.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtobufImport.java
@@ -39,10 +39,10 @@
 import com.google.protobuf.UnknownFieldSet;
 import java.io.BufferedInputStream;
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.InputStream;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
+import java.nio.file.Files;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -96,7 +96,7 @@
       }
 
       Parser<UnknownFieldSet> parser = UnknownFieldSet.getDefaultInstance().getParserForType();
-      try (InputStream in = new BufferedInputStream(new FileInputStream(file))) {
+      try (InputStream in = new BufferedInputStream(Files.newInputStream(file.toPath()))) {
         UnknownFieldSet msg;
         while ((msg = parser.parseDelimitedFrom(in)) != null) {
           Map.Entry<Integer, UnknownFieldSet.Field> e =
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/rules/PrologCompiler.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/rules/PrologCompiler.java
index 0ea0f1a..4ad7701 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/rules/PrologCompiler.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/rules/PrologCompiler.java
@@ -24,10 +24,9 @@
 import com.googlecode.prolog_cafe.compiler.Compiler;
 import com.googlecode.prolog_cafe.exceptions.CompileException;
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.URL;
 import java.net.URLClassLoader;
@@ -134,7 +133,7 @@
     // Any leak of tmp caused by this method failing will be cleaned
     // up by our caller when tempDir is recursively deleted.
     File tmp = File.createTempFile("rules", ".pl", tempDir);
-    try (FileOutputStream out = new FileOutputStream(tmp)) {
+    try (OutputStream out = Files.newOutputStream(tmp.toPath())) {
       git.open(blobId).copyTo(out);
     }
     return tmp;
@@ -230,7 +229,7 @@
         jarAdd.setTime(now);
         out.putNextEntry(jarAdd);
         if (f.isFile()) {
-          try (FileInputStream in = new FileInputStream(f)) {
+          try (InputStream in = Files.newInputStream(f.toPath())) {
             while (true) {
               int nRead = in.read(buffer, 0, buffer.length);
               if (nRead <= 0) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
index 08f879f..410dc5c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
@@ -101,7 +101,7 @@
     }
   }
 
-  // Generate a candidate list at 3x the size of what the user wants to see to
+  // Generate a candidate list at 2x the size of what the user wants to see to
   // give the ranking algorithm a good set of candidates it can work with
   private static final int CANDIDATE_LIST_MULTIPLIER = 2;
 
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 5338e89..f4ea3b0 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
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.api.changes.AbandonInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerResult;
@@ -66,16 +67,17 @@
 import com.google.gerrit.server.change.Move;
 import com.google.gerrit.server.change.Mute;
 import com.google.gerrit.server.change.PostHashtags;
+import com.google.gerrit.server.change.PostPrivate;
 import com.google.gerrit.server.change.PostReviewers;
 import com.google.gerrit.server.change.PublishDraftPatchSet;
 import com.google.gerrit.server.change.PutAssignee;
-import com.google.gerrit.server.change.PutPrivate;
 import com.google.gerrit.server.change.PutTopic;
 import com.google.gerrit.server.change.Rebase;
 import com.google.gerrit.server.change.Restore;
 import com.google.gerrit.server.change.Revert;
 import com.google.gerrit.server.change.Reviewers;
 import com.google.gerrit.server.change.Revisions;
+import com.google.gerrit.server.change.SetPrivateOp;
 import com.google.gerrit.server.change.SetReadyForReview;
 import com.google.gerrit.server.change.SetWorkInProgress;
 import com.google.gerrit.server.change.SubmittedTogether;
@@ -129,7 +131,7 @@
   private final Check check;
   private final Index index;
   private final Move move;
-  private final PutPrivate putPrivate;
+  private final PostPrivate postPrivate;
   private final DeletePrivate deletePrivate;
   private final Ignore ignore;
   private final Unignore unignore;
@@ -172,7 +174,7 @@
       Check check,
       Index index,
       Move move,
-      PutPrivate putPrivate,
+      PostPrivate postPrivate,
       DeletePrivate deletePrivate,
       Ignore ignore,
       Unignore unignore,
@@ -213,7 +215,7 @@
     this.check = check;
     this.index = index;
     this.move = move;
-    this.putPrivate = putPrivate;
+    this.postPrivate = postPrivate;
     this.deletePrivate = deletePrivate;
     this.ignore = ignore;
     this.unignore = unignore;
@@ -302,12 +304,13 @@
   }
 
   @Override
-  public void setPrivate(boolean value) throws RestApiException {
+  public void setPrivate(boolean value, @Nullable String message) throws RestApiException {
     try {
+      SetPrivateOp.Input input = new SetPrivateOp.Input(message);
       if (value) {
-        putPrivate.apply(change, null);
+        postPrivate.apply(change, input);
       } else {
-        deletePrivate.apply(change, null);
+        deletePrivate.apply(change, input);
       }
     } catch (Exception e) {
       throw asRestApiException("Cannot change private status", e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeletePrivate.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeletePrivate.java
index a951d66..7819a29 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeletePrivate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeletePrivate.java
@@ -33,10 +33,7 @@
 
 @Singleton
 public class DeletePrivate
-    extends RetryingRestModifyView<ChangeResource, DeletePrivate.Input, Response<String>>
-    implements UiAction<ChangeResource> {
-  public static class Input {}
-
+    extends RetryingRestModifyView<ChangeResource, SetPrivateOp.Input, Response<String>> {
   private final ChangeMessagesUtil cmUtil;
   private final Provider<ReviewDb> dbProvider;
 
@@ -49,7 +46,7 @@
 
   @Override
   protected Response<String> applyImpl(
-      BatchUpdate.Factory updateFactory, ChangeResource rsrc, DeletePrivate.Input input)
+      BatchUpdate.Factory updateFactory, ChangeResource rsrc, SetPrivateOp.Input input)
       throws RestApiException, UpdateException {
     if (!rsrc.isUserOwner()) {
       throw new AuthException("not allowed to unmark private");
@@ -60,7 +57,7 @@
     }
 
     ChangeControl control = rsrc.getControl();
-    SetPrivateOp op = new SetPrivateOp(cmUtil, false);
+    SetPrivateOp op = new SetPrivateOp(cmUtil, false, input);
     try (BatchUpdate u =
         updateFactory.create(
             dbProvider.get(),
@@ -73,11 +70,20 @@
     return Response.none();
   }
 
-  @Override
-  public Description getDescription(ChangeResource rsrc) {
-    return new UiAction.Description()
-        .setLabel("Unmark private")
-        .setTitle("Unmark change as private")
-        .setVisible(rsrc.getChange().isPrivate() && rsrc.isUserOwner());
+  public static class DeletePrivateByPost extends DeletePrivate
+      implements UiAction<ChangeResource> {
+    @Inject
+    DeletePrivateByPost(
+        Provider<ReviewDb> dbProvider, RetryHelper retryHelper, ChangeMessagesUtil cmUtil) {
+      super(dbProvider, retryHelper, cmUtil);
+    }
+
+    @Override
+    public Description getDescription(ChangeResource rsrc) {
+      return new UiAction.Description()
+          .setLabel("Unmark private")
+          .setTitle("Unmark change as private")
+          .setVisible(rsrc.getChange().isPrivate() && rsrc.isUserOwner());
+    }
   }
 }
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 5ddf9e9..2315f48 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
@@ -28,6 +28,7 @@
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.RestApiModule;
 import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.change.DeletePrivate.DeletePrivateByPost;
 import com.google.gerrit.server.change.Reviewed.DeleteReviewed;
 import com.google.gerrit.server.change.Reviewed.PutReviewed;
 
@@ -85,7 +86,8 @@
     post(CHANGE_KIND, "index").to(Index.class);
     post(CHANGE_KIND, "rebuild.notedb").to(Rebuild.class);
     post(CHANGE_KIND, "move").to(Move.class);
-    put(CHANGE_KIND, "private").to(PutPrivate.class);
+    post(CHANGE_KIND, "private").to(PostPrivate.class);
+    post(CHANGE_KIND, "private.delete").to(DeletePrivateByPost.class);
     delete(CHANGE_KIND, "private").to(DeletePrivate.class);
     put(CHANGE_KIND, "ignore").to(Ignore.class);
     put(CHANGE_KIND, "unignore").to(Unignore.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutPrivate.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostPrivate.java
similarity index 72%
rename from gerrit-server/src/main/java/com/google/gerrit/server/change/PutPrivate.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/change/PostPrivate.java
index bd2bf05..a1e673f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutPrivate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostPrivate.java
@@ -22,6 +22,8 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.RetryHelper;
@@ -32,26 +34,30 @@
 import com.google.inject.Singleton;
 
 @Singleton
-public class PutPrivate
-    extends RetryingRestModifyView<ChangeResource, PutPrivate.Input, Response<String>>
+public class PostPrivate
+    extends RetryingRestModifyView<ChangeResource, SetPrivateOp.Input, Response<String>>
     implements UiAction<ChangeResource> {
-  public static class Input {}
-
   private final ChangeMessagesUtil cmUtil;
   private final Provider<ReviewDb> dbProvider;
+  private final PermissionBackend permissionBackend;
 
   @Inject
-  PutPrivate(Provider<ReviewDb> dbProvider, RetryHelper retryHelper, ChangeMessagesUtil cmUtil) {
+  PostPrivate(
+      Provider<ReviewDb> dbProvider,
+      RetryHelper retryHelper,
+      ChangeMessagesUtil cmUtil,
+      PermissionBackend permissionBackend) {
     super(retryHelper);
     this.dbProvider = dbProvider;
     this.cmUtil = cmUtil;
+    this.permissionBackend = permissionBackend;
   }
 
   @Override
-  protected Response<String> applyImpl(
-      BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input)
+  public Response<String> applyImpl(
+      BatchUpdate.Factory updateFactory, ChangeResource rsrc, SetPrivateOp.Input input)
       throws RestApiException, UpdateException {
-    if (!rsrc.isUserOwner()) {
+    if (!canSetPrivate(rsrc)) {
       throw new AuthException("not allowed to mark private");
     }
 
@@ -60,7 +66,7 @@
     }
 
     ChangeControl control = rsrc.getControl();
-    SetPrivateOp op = new SetPrivateOp(cmUtil, true);
+    SetPrivateOp op = new SetPrivateOp(cmUtil, true, input);
     try (BatchUpdate u =
         updateFactory.create(
             dbProvider.get(),
@@ -82,6 +88,11 @@
         .setVisible(
             !change.isPrivate()
                 && change.getStatus() != Change.Status.MERGED
-                && rsrc.isUserOwner());
+                && canSetPrivate(rsrc));
+  }
+
+  private boolean canSetPrivate(ChangeResource rsrc) {
+    PermissionBackend.WithUser user = permissionBackend.user(rsrc.getUser());
+    return rsrc.isUserOwner() || user.testOrFalse(GlobalPermission.ADMINISTRATE_SERVER);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetPrivateOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetPrivateOp.java
index 1cebcc2..7008eca 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetPrivateOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetPrivateOp.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.common.base.Strings;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -23,13 +24,25 @@
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gwtorm.server.OrmException;
 
-class SetPrivateOp implements BatchUpdateOp {
+public class SetPrivateOp implements BatchUpdateOp {
+  public static class Input {
+    String message;
+
+    public Input() {}
+
+    public Input(String message) {
+      this.message = message;
+    }
+  }
+
   private final ChangeMessagesUtil cmUtil;
   private final boolean isPrivate;
+  private final Input input;
 
-  SetPrivateOp(ChangeMessagesUtil cmUtil, boolean isPrivate) {
+  SetPrivateOp(ChangeMessagesUtil cmUtil, boolean isPrivate, Input input) {
     this.cmUtil = cmUtil;
     this.isPrivate = isPrivate;
+    this.input = input;
   }
 
   @Override
@@ -48,10 +61,18 @@
 
   private void addMessage(ChangeContext ctx, ChangeUpdate update) throws OrmException {
     Change c = ctx.getChange();
+    StringBuilder buf = new StringBuilder(c.isPrivate() ? "Set private" : "Unset private");
+
+    String m = Strings.nullToEmpty(input == null ? null : input.message).trim();
+    if (!m.isEmpty()) {
+      buf.append("\n\n");
+      buf.append(m);
+    }
+
     ChangeMessage cmsg =
         ChangeMessagesUtil.newMessage(
             ctx,
-            c.isPrivate() ? "Set private" : "Unset private",
+            buf.toString(),
             c.isPrivate()
                 ? ChangeMessagesUtil.TAG_SET_PRIVATE
                 : ChangeMessagesUtil.TAG_UNSET_PRIVATE);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/WorkInProgressOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/WorkInProgressOp.java
index 7f6e543..21e5dfa 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/WorkInProgressOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/WorkInProgressOp.java
@@ -61,7 +61,7 @@
     StringBuilder buf =
         new StringBuilder(c.isWorkInProgress() ? "Set Work In Progress" : "Set Ready For Review");
 
-    String m = Strings.nullToEmpty(in.message).trim();
+    String m = Strings.nullToEmpty(in == null ? null : in.message).trim();
     if (!m.isEmpty()) {
       buf.append("\n\n");
       buf.append(m);
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 8fabe44..16c63c8 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
@@ -286,6 +286,7 @@
   /** Can this user rebase this change? */
   private boolean canRebase(ReviewDb db) throws OrmException {
     return (isOwner() || getRefControl().canSubmit(isOwner()) || getRefControl().canRebase())
+        && getRefControl().canUpload()
         && !isPatchSetLocked(db);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index ca719c8..611a1a4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -986,7 +986,8 @@
     return reviewer(who, false);
   }
 
-  private Predicate<ChangeData> reviewerDefaultField(String who) throws QueryParseException, OrmException {
+  private Predicate<ChangeData> reviewerDefaultField(String who)
+      throws QueryParseException, OrmException {
     return reviewer(who, true);
   }
 
@@ -1232,8 +1233,9 @@
     return args.getIdentifiedUser().getAccountId();
   }
 
-  public Predicate<ChangeData> reviewerByState(String who, ReviewerStateInternal state,
-      boolean forDefaultField) throws QueryParseException, OrmException {
+  public Predicate<ChangeData> reviewerByState(
+      String who, ReviewerStateInternal state, boolean forDefaultField)
+      throws QueryParseException, OrmException {
     Predicate<ChangeData> reviewerByEmailPredicate = null;
     if (args.index.getSchema().hasField(ChangeField.REVIEWER_BY_EMAIL)) {
       Address address = Address.tryParse(who);
@@ -1246,10 +1248,12 @@
     try {
       Set<Account.Id> accounts = parseAccount(who);
       if (!forDefaultField || accounts.size() <= MAX_ACCOUNTS_PER_DEFAULT_FIELD) {
-        reviewerPredicate = Predicate.or(
-            accounts.stream()
-                .map(id -> ReviewerPredicate.forState(args, id, state))
-                .collect(toList()));
+        reviewerPredicate =
+            Predicate.or(
+                accounts
+                    .stream()
+                    .map(id -> ReviewerPredicate.forState(args, id, state))
+                    .collect(toList()));
       }
     } catch (QueryParseException e) {
       // Propagate this exception only if we can't use 'who' to query by email
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 6601266..ee894c9 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
@@ -98,6 +98,7 @@
   @Inject protected AllProjectsName allProjects;
 
   protected LifecycleManager lifecycle;
+  protected Injector injector;
   protected ReviewDb db;
   protected AccountInfo currentUserInfo;
   protected CurrentUser user;
@@ -107,11 +108,14 @@
   @Before
   public void setUpInjector() throws Exception {
     lifecycle = new LifecycleManager();
-    Injector injector = createInjector();
+    injector = createInjector();
     lifecycle.add(injector);
     injector.injectMembers(this);
     lifecycle.start();
+    setUpDatabase();
+  }
 
+  protected void setUpDatabase() throws Exception {
     db = schemaFactory.open();
     schemaCreator.create(db);
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 6df03cd..0b01362 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -399,7 +399,7 @@
     assertQuery("is:open", change2, change1);
     assertQuery("is:private");
 
-    gApi.changes().id(change1.getChangeId()).setPrivate(true);
+    gApi.changes().id(change1.getChangeId()).setPrivate(true, null);
 
     // Change1 is not private, but should be still visible to its owner.
     assertQuery("is:open", change1, change2);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
index a0e5ee0..4b8309a 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
@@ -98,6 +98,7 @@
   @Inject protected GroupCache groupCache;
 
   protected LifecycleManager lifecycle;
+  protected Injector injector;
   protected ReviewDb db;
   protected AccountInfo currentUserInfo;
   protected CurrentUser user;
@@ -107,11 +108,14 @@
   @Before
   public void setUpInjector() throws Exception {
     lifecycle = new LifecycleManager();
-    Injector injector = createInjector();
+    injector = createInjector();
     lifecycle.add(injector);
     injector.injectMembers(this);
     lifecycle.start();
+    setUpDatabase();
+  }
 
+  protected void setUpDatabase() throws Exception {
     db = schemaFactory.open();
     schemaCreator.create(db);
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java b/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java
index 21c8764..3d4a1a0 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java
@@ -54,10 +54,11 @@
 
 import com.google.common.io.ByteStreams;
 import java.io.File;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.net.URL;
+import java.nio.file.Files;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -119,7 +120,7 @@
       try (InputStream in = url.openStream()) {
         hook = File.createTempFile("hook_", ".sh");
         cleanup.add(hook);
-        try (FileOutputStream out = new FileOutputStream(hook)) {
+        try (OutputStream out = Files.newOutputStream(hook.toPath())) {
           ByteStreams.copy(in, out);
         }
       }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
index 5a47cb0..820052c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
@@ -25,12 +25,11 @@
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.nio.file.Files;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 
@@ -54,6 +53,7 @@
 
   @Inject private PluginLoader loader;
 
+  @SuppressWarnings("resource")
   @Override
   protected void run() throws UnloggedFailure {
     if (!loader.isRemoteAdminEnabled()) {
@@ -80,8 +80,8 @@
       data = in;
     } else if (new File(source).isFile() && source.equals(new File(source).getAbsolutePath())) {
       try {
-        data = new FileInputStream(new File(source));
-      } catch (FileNotFoundException e) {
+        data = Files.newInputStream(new File(source).toPath());
+      } catch (IOException e) {
         throw die("cannot read " + source);
       }
     } else {
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/UnzippedDistribution.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/UnzippedDistribution.java
index b5a1dae..ec92fba 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/UnzippedDistribution.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/UnzippedDistribution.java
@@ -20,10 +20,10 @@
 import com.google.gerrit.pgm.init.PluginsDistribution;
 import com.google.inject.Singleton;
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.file.Files;
 import java.util.ArrayList;
 import java.util.List;
 import javax.servlet.ServletContext;
@@ -45,7 +45,7 @@
       for (File p : list) {
         String pluginJarName = p.getName();
         String pluginName = pluginJarName.substring(0, pluginJarName.length() - JAR.length());
-        try (InputStream in = new FileInputStream(p)) {
+        try (InputStream in = Files.newInputStream(p.toPath())) {
           processor.process(pluginName, in);
         }
       }
diff --git a/lib/asciidoctor/java/AsciiDoctor.java b/lib/asciidoctor/java/AsciiDoctor.java
index d765cc1..219cc24 100644
--- a/lib/asciidoctor/java/AsciiDoctor.java
+++ b/lib/asciidoctor/java/AsciiDoctor.java
@@ -15,12 +15,12 @@
 import com.google.common.io.ByteStreams;
 import java.io.BufferedReader;
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
 import java.io.FileReader;
 import java.io.FilenameFilter;
 import java.io.IOException;
+import java.io.InputStream;
 import java.nio.file.Files;
+import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -162,7 +162,7 @@
     if (bazel) {
       renderFiles(inputFiles, null);
     } else {
-      try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(zipFile))) {
+      try (ZipOutputStream zip = new ZipOutputStream(Files.newOutputStream(Paths.get(zipFile)))) {
         renderFiles(inputFiles, zip);
 
         File[] cssFiles =
@@ -199,7 +199,7 @@
 
   public static void zipFile(File file, String name, ZipOutputStream zip) throws IOException {
     zip.putNextEntry(new ZipEntry(name));
-    try (FileInputStream input = new FileInputStream(file)) {
+    try (InputStream input = Files.newInputStream(file.toPath())) {
       ByteStreams.copy(input, zip);
     }
     zip.closeEntry();
diff --git a/lib/asciidoctor/java/DocIndexer.java b/lib/asciidoctor/java/DocIndexer.java
index 395f9fe..fbb7f94 100644
--- a/lib/asciidoctor/java/DocIndexer.java
+++ b/lib/asciidoctor/java/DocIndexer.java
@@ -18,13 +18,13 @@
 import java.io.BufferedReader;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
 import java.io.FileReader;
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.io.UnsupportedEncodingException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.jar.JarEntry;
@@ -81,7 +81,7 @@
       return;
     }
 
-    try (JarOutputStream jar = new JarOutputStream(new FileOutputStream(outFile))) {
+    try (JarOutputStream jar = new JarOutputStream(Files.newOutputStream(Paths.get(outFile)))) {
       byte[] compressedIndex = zip(index());
       JarEntry entry = new JarEntry(String.format("%s/%s", Constants.PACKAGE, Constants.INDEX_ZIP));
       entry.setSize(compressedIndex.length);
@@ -106,7 +106,7 @@
 
         String title;
         try (BufferedReader titleReader =
-            new BufferedReader(new InputStreamReader(new FileInputStream(file), UTF_8))) {
+            new BufferedReader(new InputStreamReader(Files.newInputStream(file.toPath()), UTF_8))) {
           title = titleReader.readLine();
           if (title != null && title.startsWith("[[")) {
             // Generally the first line of the txt is the title. In a few cases the
diff --git a/lib/prolog/java/BuckPrologCompiler.java b/lib/prolog/java/BuckPrologCompiler.java
index d3f41c0..cc3e39e 100644
--- a/lib/prolog/java/BuckPrologCompiler.java
+++ b/lib/prolog/java/BuckPrologCompiler.java
@@ -15,9 +15,9 @@
 import com.googlecode.prolog_cafe.compiler.Compiler;
 import com.googlecode.prolog_cafe.exceptions.CompileException;
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
 import java.util.jar.JarEntry;
 import java.util.jar.JarOutputStream;
 
@@ -46,7 +46,7 @@
   private static void jar(File jar, File classes) throws IOException {
     File tmp = File.createTempFile("prolog", ".jar", tmpdir);
     try {
-      try (JarOutputStream out = new JarOutputStream(new FileOutputStream(tmp))) {
+      try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(tmp.toPath()))) {
         add(out, classes, "");
       }
       if (!tmp.renameTo(jar)) {
@@ -70,7 +70,7 @@
       }
 
       JarEntry e = new JarEntry(prefix + name);
-      try (FileInputStream in = new FileInputStream(f)) {
+      try (InputStream in = Files.newInputStream(f.toPath())) {
         e.setTime(f.lastModified());
         out.putNextEntry(e);
         byte[] buf = new byte[16 << 10];
diff --git a/polygerrit-ui/.eslintrc.json b/polygerrit-ui/app/.eslintrc.json
similarity index 100%
rename from polygerrit-ui/.eslintrc.json
rename to polygerrit-ui/app/.eslintrc.json
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index 7c12fa2..4e99272 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -34,10 +34,9 @@
     name = "closure_lib",
     srcs = ["gr-app.js"],
     convention = "GOOGLE",
-    language = "ECMASCRIPT6",
-    suppress = [
-        "JSC_BAD_JSDOC_ANNOTATION",
-    ],
+    # TODO(davido): Clean up these issues: http://paste.openstack.org/show/608548
+    # and remove this supression
+    suppress = ["JSC_UNUSED_LOCAL_ASSIGNMENT"],
     deps = [
         "//lib/polymer_externs:polymer_closure",
         "@io_bazel_rules_closure//closure/library",
@@ -52,6 +51,7 @@
     defs = [
         "--polymer_pass",
         "--jscomp_off=duplicate",
+        "--force_inject_library=es6_runtime",
     ],
     language = "ECMASCRIPT5",
     deps = [":closure_lib"],
@@ -157,3 +157,18 @@
         "manual",
     ],
 )
+
+sh_test(
+    name = "lint_test",
+    size = "large",
+    srcs = ["lint_test.sh"],
+    data = [
+        ":pg_code",
+        ".eslintrc.json",
+    ],
+    # Should not run sandboxed.
+    tags = [
+        "local",
+        "manual",
+    ],
+)
diff --git a/polygerrit-ui/app/lint_test.sh b/polygerrit-ui/app/lint_test.sh
new file mode 100755
index 0000000..7ee74d8
--- /dev/null
+++ b/polygerrit-ui/app/lint_test.sh
@@ -0,0 +1,22 @@
+#!/bin/sh
+
+set -ex
+
+eslint_bin=$(which npm)
+if [[ -z "$eslint_bin" ]]; then
+    echo "NPM must be on the path."
+    exit 1
+fi
+
+eslint_bin=$(which eslint)
+eslint_config=$(npm list -g | grep -c eslint-config-google)
+eslint_plugin=$(npm list -g | grep -c eslint-plugin-html)
+if [[ -z "$eslint_bin" ]] || [[ eslint_config -eq "0" ]] || [[ eslint_plugin -eq "0" ]]; then
+    echo "You must install ESLint and its dependencies from NPM."
+    echo "> npm install -g eslint eslint-config-google eslint-plugin-html"
+    echo "For more information, view the README:"
+    echo "https://gerrit.googlesource.com/gerrit/+/master/polygerrit-ui/#Style-guide"
+    exit 1
+fi
+
+${eslint_bin} --ignore-pattern 'bower_components/' --ignore-pattern 'gr-linked-text' --ignore-pattern 'scripts/vendor' --ext .html,.js .
diff --git a/tools/bzl/maven_jar.bzl b/tools/bzl/maven_jar.bzl
index 2dbeae7..55bfae1 100644
--- a/tools/bzl/maven_jar.bzl
+++ b/tools/bzl/maven_jar.bzl
@@ -64,12 +64,13 @@
       formatted_deps += "    ],"
   return formatted_deps
 
-def _generate_build_file(ctx, binjar, srcjar):
+def _generate_build_files(ctx, binjar, srcjar):
+  header = "# DO NOT EDIT: automatically generated BUILD file for maven_jar rule %s" % ctx.name
   srcjar_attr = ""
   if srcjar:
     srcjar_attr = 'srcjar = "%s",' % srcjar
   contents = """
-# DO NOT EDIT: automatically generated BUILD file for maven_jar rule {rule_name}
+{header}
 package(default_visibility = ['//visibility:public'])
 java_import(
     name = 'jar',
@@ -86,10 +87,10 @@
     {exports}
 )
 \n""".format(srcjar_attr = srcjar_attr,
-              rule_name = ctx.name,
-              binjar = binjar,
-              deps = _format_deps("deps", ctx.attr.deps),
-              exports = _format_deps("exports", ctx.attr.exports))
+             header = header,
+             binjar = binjar,
+             deps = _format_deps("deps", ctx.attr.deps),
+             exports = _format_deps("exports", ctx.attr.exports))
   if srcjar:
     contents += """
 java_import(
@@ -99,6 +100,18 @@
 """.format(srcjar = srcjar)
   ctx.file('%s/BUILD' % ctx.path("jar"), contents, False)
 
+  # Compatibility layer for java_import_external from rules_closure
+  contents = """
+{header}
+package(default_visibility = ['//visibility:public'])
+
+alias(
+    name = "{rule_name}",
+    actual = "@{rule_name}//jar",
+)
+\n""".format(rule_name = ctx.name, header = header)
+  ctx.file("BUILD", contents, False)
+
 def _maven_jar_impl(ctx):
   """rule to download a Maven archive."""
   coordinates = _create_coordinates(ctx.attr.artifact)
@@ -142,7 +155,7 @@
     if out.return_code:
       fail("failed %s: %s" % (args, out.stderr))
 
-  _generate_build_file(ctx, binjar, srcjar)
+  _generate_build_files(ctx, binjar, srcjar)
 
 maven_jar = repository_rule(
     attrs = {