Merge "Show revert change and revert submission options instead of 2 buttons"
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index f320d2a..6da455d 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -6827,6 +6827,11 @@
 The source to merge from, e.g. a complete or abbreviated commit SHA-1,
 a complete reference name, a short reference name under `refs/heads`, `refs/tags`,
 or `refs/remotes` namespace, etc.
+|`source_branch`  |optional|
+A branch from which `source` is reachable. If specified,
+`source` is checked for visibility and reachability against only this
+branch. This speeds up the operation, especially for large repos with
+many branches.
 |`strategy`     |optional|
 The strategy of the merge, can be `recursive`, `resolve`,
 `simple-two-way-in-core`, `ours` or `theirs`, default will use project settings.
diff --git a/java/com/google/gerrit/extensions/common/MergeInput.java b/java/com/google/gerrit/extensions/common/MergeInput.java
index c16a551..c3cfcee 100644
--- a/java/com/google/gerrit/extensions/common/MergeInput.java
+++ b/java/com/google/gerrit/extensions/common/MergeInput.java
@@ -24,6 +24,12 @@
   public String source;
 
   /**
+   * If specified, visibility of the {@code source} commit will only be checked against {@code
+   * source_branch}, rather than all visible branches.
+   */
+  public String sourceBranch;
+
+  /**
    * {@code strategy} name of the merge strategy.
    *
    * @see org.eclipse.jgit.merge.MergeStrategy
diff --git a/java/com/google/gerrit/extensions/validators/CommentValidationContext.java b/java/com/google/gerrit/extensions/validators/CommentValidationContext.java
new file mode 100644
index 0000000..1cb00e3
--- /dev/null
+++ b/java/com/google/gerrit/extensions/validators/CommentValidationContext.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.validators;
+
+import com.google.auto.value.AutoValue;
+
+/**
+ * Holds a comment validators context in order to pass it to a validation plugin.
+ *
+ * <p>This is used to provided additional context around that comment that can be used by the
+ * validator to determine what validations should be run. For example, a comment validator may only
+ * want to validate a comment if it's on a change in the project foo.
+ *
+ * @see CommentValidator
+ */
+@AutoValue
+public abstract class CommentValidationContext {
+
+  /** Returns the change id the comment is being added to. */
+  public abstract int getChangeId();
+
+  /** Returns the project the comment is being added to. */
+  public abstract String getProject();
+
+  public static Builder builder() {
+    return new AutoValue_CommentValidationContext.Builder();
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+    public abstract Builder changeId(int value);
+
+    public abstract Builder project(String value);
+
+    public abstract CommentValidationContext build();
+  }
+}
diff --git a/java/com/google/gerrit/extensions/validators/CommentValidator.java b/java/com/google/gerrit/extensions/validators/CommentValidator.java
index cfefdef..ba73e46 100644
--- a/java/com/google/gerrit/extensions/validators/CommentValidator.java
+++ b/java/com/google/gerrit/extensions/validators/CommentValidator.java
@@ -30,5 +30,5 @@
    * @return An empty list if all comments are valid, or else a list of validation failures.
    */
   ImmutableList<CommentValidationFailure> validateComments(
-      ImmutableList<CommentForValidation> comments);
+      CommentValidationContext ctx, ImmutableList<CommentForValidation> comments);
 }
diff --git a/java/com/google/gerrit/git/RefUpdateUtil.java b/java/com/google/gerrit/git/RefUpdateUtil.java
index fa7b98f..bd88962 100644
--- a/java/com/google/gerrit/git/RefUpdateUtil.java
+++ b/java/com/google/gerrit/git/RefUpdateUtil.java
@@ -150,6 +150,7 @@
   public static void deleteChecked(Repository repo, String refName) throws IOException {
     RefUpdate ru = repo.updateRef(refName);
     ru.setForceUpdate(true);
+    ru.setCheckConflicting(false);
     switch (ru.delete()) {
       case FORCED:
         // Ref was deleted.
diff --git a/java/com/google/gerrit/pgm/init/InitModule.java b/java/com/google/gerrit/pgm/init/InitModule.java
index f2fc001..b658675 100644
--- a/java/com/google/gerrit/pgm/init/InitModule.java
+++ b/java/com/google/gerrit/pgm/init/InitModule.java
@@ -34,8 +34,6 @@
   @Override
   protected void configure() {
     bind(SitePaths.class);
-    bind(Libraries.class);
-    bind(LibraryDownloader.class);
     factory(Section.Factory.class);
     factory(VersionedAuthorizedKeysOnInit.Factory.class);
 
diff --git a/java/com/google/gerrit/pgm/init/Libraries.java b/java/com/google/gerrit/pgm/init/Libraries.java
deleted file mode 100644
index c599e99..0000000
--- a/java/com/google/gerrit/pgm/init/Libraries.java
+++ /dev/null
@@ -1,141 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.pgm.init;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.gerrit.pgm.init.api.LibraryDownload;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.lang.reflect.Field;
-import java.lang.reflect.Modifier;
-import java.util.List;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.Config;
-
-/** Standard {@link LibraryDownloader} instances derived from configuration. */
-@Singleton
-class Libraries {
-  private static final String RESOURCE_FILE = "com/google/gerrit/pgm/init/libraries.config";
-
-  private final Provider<LibraryDownloader> downloadProvider;
-  private final List<String> skippedDownloads;
-  private final boolean skipAllDownloads;
-
-  /* final */ LibraryDownloader db2Driver;
-  /* final */ LibraryDownloader db2DriverLicense;
-  /* final */ LibraryDownloader hanaDriver;
-  /* final */ LibraryDownloader mariadbDriver;
-  /* final */ LibraryDownloader mysqlDriver;
-  /* final */ LibraryDownloader oracleDriver;
-
-  @Inject
-  Libraries(
-      final Provider<LibraryDownloader> downloadProvider,
-      @LibraryDownload List<String> skippedDownloads,
-      @LibraryDownload Boolean skipAllDownloads) {
-    this.downloadProvider = downloadProvider;
-    this.skippedDownloads = skippedDownloads;
-    this.skipAllDownloads = skipAllDownloads;
-    init();
-  }
-
-  private void init() {
-    final Config cfg = new Config();
-    try {
-      cfg.fromText(read(RESOURCE_FILE));
-    } catch (IOException | ConfigInvalidException e) {
-      throw new RuntimeException(e.getMessage(), e);
-    }
-
-    for (Field f : Libraries.class.getDeclaredFields()) {
-      if ((f.getModifiers() & Modifier.STATIC) == 0 && f.getType() == LibraryDownloader.class) {
-        try {
-          f.set(this, downloadProvider.get());
-        } catch (IllegalArgumentException | IllegalAccessException e) {
-          throw new IllegalStateException("Cannot initialize " + f.getName());
-        }
-      }
-    }
-
-    for (Field f : Libraries.class.getDeclaredFields()) {
-      if ((f.getModifiers() & Modifier.STATIC) == 0 && f.getType() == LibraryDownloader.class) {
-        try {
-          init(f, cfg);
-        } catch (IllegalArgumentException
-            | IllegalAccessException
-            | NoSuchFieldException
-            | SecurityException e) {
-          throw new IllegalStateException("Cannot configure " + f.getName());
-        }
-      }
-    }
-  }
-
-  private void init(Field field, Config cfg)
-      throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
-          SecurityException {
-    String n = field.getName();
-    LibraryDownloader dl = (LibraryDownloader) field.get(this);
-    dl.setName(get(cfg, n, "name"));
-    dl.setJarUrl(get(cfg, n, "url"));
-    dl.setSHA1(getOptional(cfg, n, "sha1"));
-    dl.setRemove(get(cfg, n, "remove"));
-    for (String d : cfg.getStringList("library", n, "needs")) {
-      dl.addNeeds((LibraryDownloader) getClass().getDeclaredField(d).get(this));
-    }
-    dl.setSkipDownload(skipAllDownloads || skippedDownloads.contains(n));
-  }
-
-  private static String getOptional(Config cfg, String name, String key) {
-    return doGet(cfg, name, key, false);
-  }
-
-  private static String get(Config cfg, String name, String key) {
-    return doGet(cfg, name, key, true);
-  }
-
-  private static String doGet(Config cfg, String name, String key, boolean required) {
-    String val = cfg.getString("library", name, key);
-    if ((val == null || val.isEmpty()) && required) {
-      throw new IllegalStateException(
-          "Variable library." + name + "." + key + " is required within " + RESOURCE_FILE);
-    }
-    return val;
-  }
-
-  private static String read(String p) throws IOException {
-    try (InputStream in = Libraries.class.getClassLoader().getResourceAsStream(p)) {
-      if (in == null) {
-        throw new FileNotFoundException("Cannot load resource " + p);
-      }
-      try (Reader r = new InputStreamReader(in, UTF_8)) {
-        final StringBuilder buf = new StringBuilder();
-        final char[] tmp = new char[512];
-        int n;
-        while (0 < (n = r.read(tmp))) {
-          buf.append(tmp, 0, n);
-        }
-        return buf.toString();
-      }
-    }
-  }
-}
diff --git a/java/com/google/gerrit/pgm/init/LibraryDownloader.java b/java/com/google/gerrit/pgm/init/LibraryDownloader.java
deleted file mode 100644
index 0b31ee2..0000000
--- a/java/com/google/gerrit/pgm/init/LibraryDownloader.java
+++ /dev/null
@@ -1,316 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.pgm.init;
-
-import com.google.common.hash.Funnels;
-import com.google.common.hash.Hasher;
-import com.google.common.hash.Hashing;
-import com.google.common.io.ByteStreams;
-import com.google.gerrit.common.Die;
-import com.google.gerrit.common.IoUtil;
-import com.google.gerrit.pgm.init.api.ConsoleUI;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.inject.Inject;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.HttpURLConnection;
-import java.net.Proxy;
-import java.net.ProxySelector;
-import java.net.URISyntaxException;
-import java.net.URL;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.List;
-import org.eclipse.jgit.util.HttpSupport;
-
-/** Get optional or required 3rd party library files into $site_path/lib. */
-class LibraryDownloader {
-  private final ConsoleUI ui;
-  private final Path lib_dir;
-  private final StaleLibraryRemover remover;
-
-  private boolean required;
-  private String name;
-  private String jarUrl;
-  private String sha1;
-  private String remove;
-  private List<LibraryDownloader> needs;
-  private LibraryDownloader neededBy;
-  private Path dst;
-  private boolean download; // download or copy
-  private boolean exists;
-  private boolean skipDownload;
-
-  @Inject
-  LibraryDownloader(ConsoleUI ui, SitePaths site, StaleLibraryRemover remover) {
-    this.ui = ui;
-    this.lib_dir = site.lib_dir;
-    this.remover = remover;
-    this.needs = new ArrayList<>(2);
-  }
-
-  void setName(String name) {
-    this.name = name;
-  }
-
-  void setJarUrl(String url) {
-    this.jarUrl = url;
-    download = jarUrl.startsWith("http");
-  }
-
-  void setSHA1(String sha1) {
-    this.sha1 = sha1;
-  }
-
-  void setRemove(String remove) {
-    this.remove = remove;
-  }
-
-  void addNeeds(LibraryDownloader lib) {
-    needs.add(lib);
-  }
-
-  void setSkipDownload(boolean skipDownload) {
-    this.skipDownload = skipDownload;
-  }
-
-  void downloadRequired() {
-    setRequired(true);
-    download();
-  }
-
-  void downloadOptional() {
-    required = false;
-    download();
-  }
-
-  private void setRequired(boolean r) {
-    required = r;
-    for (LibraryDownloader d : needs) {
-      d.setRequired(r);
-    }
-  }
-
-  private void download() {
-    if (skipDownload) {
-      return;
-    }
-
-    if (jarUrl == null || !jarUrl.contains("/")) {
-      throw new IllegalStateException("Invalid JarUrl for " + name);
-    }
-
-    final String jarName = jarUrl.substring(jarUrl.lastIndexOf('/') + 1);
-    if (jarName.contains("/") || jarName.contains("\\")) {
-      throw new IllegalStateException("Invalid JarUrl: " + jarUrl);
-    }
-
-    if (name == null) {
-      name = jarName;
-    }
-
-    dst = lib_dir.resolve(jarName);
-    if (Files.exists(dst)) {
-      exists = true;
-    } else if (shouldGet()) {
-      doGet();
-    }
-
-    if (exists) {
-      for (LibraryDownloader d : needs) {
-        d.neededBy = this;
-        d.downloadRequired();
-      }
-    }
-  }
-
-  private boolean shouldGet() {
-    if (ui.isBatch()) {
-      return required;
-    }
-    final StringBuilder msg = new StringBuilder();
-    msg.append("\n");
-    msg.append("Gerrit Code Review is not shipped with %s\n");
-    if (neededBy != null) {
-      msg.append(String.format("** This library is required by %s. **\n", neededBy.name));
-    } else if (required) {
-      msg.append("**  This library is required for your configuration. **\n");
-    } else {
-      msg.append("  If available, Gerrit can take advantage of features\n");
-      msg.append("  in the library, but will also function without it.\n");
-    }
-    msg.append(String.format("%s and install it now", download ? "Download" : "Copy"));
-    return ui.yesno(true, msg.toString(), name);
-  }
-
-  private void doGet() {
-    if (!Files.exists(lib_dir)) {
-      try {
-        Files.createDirectories(lib_dir);
-      } catch (IOException e) {
-        throw new Die("Cannot create " + lib_dir, e);
-      }
-    }
-
-    try {
-      remover.remove(remove);
-      if (download) {
-        doGetByHttp();
-      } else {
-        doGetByLocalCopy();
-      }
-      verifyFileChecksum();
-    } catch (IOException err) {
-      try {
-        Files.delete(dst);
-      } catch (IOException e) {
-        // Delete failed; leave alone.
-      }
-
-      if (ui.isBatch()) {
-        throw new Die("error: Cannot get " + jarUrl, err);
-      }
-
-      System.err.println();
-      System.err.println();
-      System.err.println("error: " + err.getMessage());
-      System.err.println("Please download:");
-      System.err.println();
-      System.err.println("  " + jarUrl);
-      System.err.println();
-      System.err.println("and save as:");
-      System.err.println();
-      System.err.println("  " + dst.toAbsolutePath());
-      System.err.println();
-      System.err.flush();
-
-      ui.waitForUser();
-
-      if (Files.exists(dst)) {
-        verifyFileChecksum();
-
-      } else if (!ui.yesno(!required, "Continue without this library")) {
-        throw new Die("aborted by user");
-      }
-    }
-
-    if (Files.exists(dst)) {
-      exists = true;
-      IoUtil.loadJARs(dst);
-    }
-  }
-
-  private void doGetByLocalCopy() throws IOException {
-    System.err.print("Copying " + jarUrl + " ...");
-    Path p = url2file(jarUrl);
-    if (!Files.exists(p)) {
-      StringBuilder msg =
-          new StringBuilder()
-              .append("\n")
-              .append("Can not find the %s at this location: %s\n")
-              .append("Please provide alternative URL");
-      p = url2file(ui.readString(null, msg.toString(), name, jarUrl));
-    }
-    Files.copy(p, dst);
-  }
-
-  private static Path url2file(String urlString) throws IOException {
-    final URL url = new URL(urlString);
-    try {
-      return Paths.get(url.toURI());
-    } catch (URISyntaxException e) {
-      return Paths.get(url.getPath());
-    }
-  }
-
-  private void doGetByHttp() throws IOException {
-    System.err.print("Downloading " + jarUrl + " ...");
-    System.err.flush();
-    try (InputStream in = openHttpStream(jarUrl);
-        OutputStream out = Files.newOutputStream(dst)) {
-      ByteStreams.copy(in, out);
-      System.err.println(" OK");
-      System.err.flush();
-    } catch (IOException err) {
-      deleteDst();
-      System.err.println(" !! FAIL !!");
-      System.err.println(err);
-      System.err.flush();
-      throw err;
-    }
-  }
-
-  private static InputStream openHttpStream(String urlStr) throws IOException {
-    ProxySelector proxySelector = ProxySelector.getDefault();
-    URL url = new URL(urlStr);
-    Proxy proxy = HttpSupport.proxyFor(proxySelector, url);
-    HttpURLConnection c = (HttpURLConnection) url.openConnection(proxy);
-
-    switch (HttpSupport.response(c)) {
-      case HttpURLConnection.HTTP_OK:
-        return c.getInputStream();
-
-      case HttpURLConnection.HTTP_NOT_FOUND:
-        throw new FileNotFoundException(url.toString());
-
-      default:
-        throw new IOException(
-            url.toString() + ": " + HttpSupport.response(c) + " " + c.getResponseMessage());
-    }
-  }
-
-  @SuppressWarnings("deprecation") // Use Hashing.sha1 for compatibility.
-  private void verifyFileChecksum() {
-    if (sha1 == null) {
-      System.err.println();
-      System.err.flush();
-      return;
-    }
-    Hasher h = Hashing.sha1().newHasher();
-    try (InputStream in = Files.newInputStream(dst);
-        OutputStream out = Funnels.asOutputStream(h)) {
-      ByteStreams.copy(in, out);
-    } catch (IOException e) {
-      deleteDst();
-      throw new Die("cannot checksum " + dst, e);
-    }
-    if (sha1.equals(h.hash().toString())) {
-      System.err.println("Checksum " + dst.getFileName() + " OK");
-      System.err.flush();
-    } else if (ui.isBatch()) {
-      deleteDst();
-      throw new Die(dst + " SHA-1 checksum does not match");
-
-    } else if (!ui.yesno(
-        null /* force an answer */,
-        "error: SHA-1 checksum does not match\nUse %s anyway", //
-        dst.getFileName())) {
-      deleteDst();
-      throw new Die("aborted by user");
-    }
-  }
-
-  private void deleteDst() {
-    try {
-      Files.delete(dst);
-    } catch (IOException e) {
-      System.err.println(" Failed to clean up lib: " + dst);
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/PublishCommentUtil.java b/java/com/google/gerrit/server/PublishCommentUtil.java
index 09042ab..3d34d6b 100644
--- a/java/com/google/gerrit/server/PublishCommentUtil.java
+++ b/java/com/google/gerrit/server/PublishCommentUtil.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.validators.CommentForValidation;
+import com.google.gerrit.extensions.validators.CommentValidationContext;
 import com.google.gerrit.extensions.validators.CommentValidationFailure;
 import com.google.gerrit.extensions.validators.CommentValidator;
 import com.google.gerrit.server.notedb.ChangeNotes;
@@ -118,16 +119,18 @@
   /**
    * Helper to run the specified set of {@link CommentValidator}-s on the specified comments.
    *
-   * @return See {@link CommentValidator#validateComments(ImmutableList)}.
+   * @return See {@link CommentValidator#validateComments(CommentValidationContext,ImmutableList)}.
    */
   public static ImmutableList<CommentValidationFailure> findInvalidComments(
+      CommentValidationContext ctx,
       PluginSetContext<CommentValidator> commentValidators,
       ImmutableList<CommentForValidation> commentsForValidation) {
     ImmutableList.Builder<CommentValidationFailure> commentValidationFailures =
         new ImmutableList.Builder<>();
     commentValidators.runEach(
         validator ->
-            commentValidationFailures.addAll(validator.validateComments(commentsForValidation)));
+            commentValidationFailures.addAll(
+                validator.validateComments(ctx, commentsForValidation)));
     return commentValidationFailures.build();
   }
 }
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java b/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
index 2b068aa..8f7e360 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
@@ -47,6 +47,10 @@
 import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.lib.Config;
 
+/**
+ * Creates persistent caches depending on gerrit.config parameters. If the cache.directory property
+ * is unset, it will fall back to in-memory caches.
+ */
 @Singleton
 class H2CacheFactory implements PersistentCacheFactory, LifecycleListener {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
diff --git a/java/com/google/gerrit/server/change/FileInfoJson.java b/java/com/google/gerrit/server/change/FileInfoJson.java
index a823975..aca4fb0 100644
--- a/java/com/google/gerrit/server/change/FileInfoJson.java
+++ b/java/com/google/gerrit/server/change/FileInfoJson.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.extensions.common.FileInfo;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListEntry;
@@ -30,6 +31,8 @@
 import com.google.inject.Singleton;
 import java.util.Map;
 import java.util.TreeMap;
+import java.util.concurrent.ExecutionException;
+import org.eclipse.jgit.errors.NoMergeBaseException;
 import org.eclipse.jgit.lib.ObjectId;
 
 @Singleton
@@ -42,31 +45,44 @@
   }
 
   public Map<String, FileInfo> toFileInfoMap(Change change, PatchSet patchSet)
-      throws PatchListNotAvailableException {
+      throws ResourceConflictException, PatchListNotAvailableException {
     return toFileInfoMap(change, patchSet.commitId(), null);
   }
 
   public Map<String, FileInfo> toFileInfoMap(
       Change change, ObjectId objectId, @Nullable PatchSet base)
-      throws PatchListNotAvailableException {
+      throws ResourceConflictException, PatchListNotAvailableException {
     ObjectId a = base != null ? base.commitId() : null;
     return toFileInfoMap(change, PatchListKey.againstCommit(a, objectId, Whitespace.IGNORE_NONE));
   }
 
   public Map<String, FileInfo> toFileInfoMap(Change change, ObjectId objectId, int parent)
-      throws PatchListNotAvailableException {
+      throws ResourceConflictException, PatchListNotAvailableException {
     return toFileInfoMap(
         change, PatchListKey.againstParentNum(parent + 1, objectId, Whitespace.IGNORE_NONE));
   }
 
   private Map<String, FileInfo> toFileInfoMap(Change change, PatchListKey key)
-      throws PatchListNotAvailableException {
+      throws ResourceConflictException, PatchListNotAvailableException {
     return toFileInfoMap(change.getProject(), key);
   }
 
   public Map<String, FileInfo> toFileInfoMap(Project.NameKey project, PatchListKey key)
-      throws PatchListNotAvailableException {
-    PatchList list = patchListCache.get(key, project);
+      throws ResourceConflictException, PatchListNotAvailableException {
+    PatchList list;
+    try {
+      list = patchListCache.get(key, project);
+    } catch (PatchListNotAvailableException e) {
+      Throwable cause = e.getCause();
+      if (cause instanceof ExecutionException) {
+        cause = cause.getCause();
+      }
+      if (cause instanceof NoMergeBaseException) {
+        throw new ResourceConflictException(
+            String.format("Cannot create auto merge commit: %s", e.getMessage()), e);
+      }
+      throw e;
+    }
 
     Map<String, FileInfo> files = new TreeMap<>();
     for (PatchListEntry e : list.getPatches()) {
diff --git a/java/com/google/gerrit/server/change/RevisionJson.java b/java/com/google/gerrit/server/change/RevisionJson.java
index fbd14c4..a4994fd 100644
--- a/java/com/google/gerrit/server/change/RevisionJson.java
+++ b/java/com/google/gerrit/server/change/RevisionJson.java
@@ -49,6 +49,7 @@
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.Extension;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GpgException;
@@ -311,9 +312,13 @@
     }
 
     if (has(ALL_FILES) || (out.isCurrent && has(CURRENT_FILES))) {
-      out.files = fileInfoJson.toFileInfoMap(c, in);
-      out.files.remove(Patch.COMMIT_MSG);
-      out.files.remove(Patch.MERGE_LIST);
+      try {
+        out.files = fileInfoJson.toFileInfoMap(c, in);
+        out.files.remove(Patch.COMMIT_MSG);
+        out.files.remove(Patch.MERGE_LIST);
+      } catch (ResourceConflictException e) {
+        logger.atWarning().withCause(e).log("creating file list failed");
+      }
     }
 
     if (out.isCurrent && has(CURRENT_ACTIONS) && userProvider.get().isIdentifiedUser()) {
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index edf8864..7df3902 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -128,6 +128,7 @@
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.git.receive.ReceiveCommitsModule;
+import com.google.gerrit.server.git.validators.CommentLimitsValidator;
 import com.google.gerrit.server.git.validators.CommitValidationListener;
 import com.google.gerrit.server.git.validators.MergeValidationListener;
 import com.google.gerrit.server.git.validators.MergeValidators;
@@ -408,6 +409,10 @@
     factory(UploadValidators.Factory.class);
     DynamicSet.setOf(binder(), UploadValidationListener.class);
 
+    bind(CommentValidator.class)
+        .annotatedWith(Exports.named(CommentLimitsValidator.class.getSimpleName()))
+        .to(CommentLimitsValidator.class);
+
     DynamicMap.mapOf(binder(), DynamicOptions.DynamicBean.class);
     DynamicMap.mapOf(binder(), ChangeQueryBuilder.ChangeOperatorFactory.class);
     DynamicMap.mapOf(binder(), ChangeQueryBuilder.ChangeHasOperandFactory.class);
diff --git a/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
index c05a47d..128388d 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditModifier.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -53,6 +53,7 @@
 import java.util.List;
 import java.util.Optional;
 import java.util.TimeZone;
+import org.eclipse.jgit.dircache.InvalidPathException;
 import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.NullProgressMonitor;
@@ -250,13 +251,14 @@
    * @param filePath the path of the file whose contents should be modified
    * @param newContent the new file content
    * @throws AuthException if the user isn't authenticated or not allowed to use change edits
+   * @throws BadRequestException if the user provided bad input (e.g. invalid file paths)
    * @throws InvalidChangeOperationException if the file already had the specified content
    * @throws PermissionBackendException
    * @throws ResourceConflictException if the project state does not permit the operation
    */
   public void modifyFile(
       Repository repository, ChangeNotes notes, String filePath, RawInput newContent)
-      throws AuthException, InvalidChangeOperationException, IOException,
+      throws AuthException, BadRequestException, InvalidChangeOperationException, IOException,
           PermissionBackendException, ResourceConflictException {
     modifyTree(repository, notes, new ChangeFileContentModification(filePath, newContent));
   }
@@ -269,12 +271,13 @@
    * @param notes the {@link ChangeNotes} of the change whose change edit should be modified
    * @param file path of the file which should be deleted
    * @throws AuthException if the user isn't authenticated or not allowed to use change edits
+   * @throws BadRequestException if the user provided bad input (e.g. invalid file paths)
    * @throws InvalidChangeOperationException if the file does not exist
    * @throws PermissionBackendException
    * @throws ResourceConflictException if the project state does not permit the operation
    */
   public void deleteFile(Repository repository, ChangeNotes notes, String file)
-      throws AuthException, InvalidChangeOperationException, IOException,
+      throws AuthException, BadRequestException, InvalidChangeOperationException, IOException,
           PermissionBackendException, ResourceConflictException {
     modifyTree(repository, notes, new DeleteFileModification(file));
   }
@@ -288,6 +291,7 @@
    * @param currentFilePath the current path/name of the file
    * @param newFilePath the desired path/name of the file
    * @throws AuthException if the user isn't authenticated or not allowed to use change edits
+   * @throws BadRequestException if the user provided bad input (e.g. invalid file paths)
    * @throws InvalidChangeOperationException if the file was already renamed to the specified new
    *     name
    * @throws PermissionBackendException
@@ -295,7 +299,7 @@
    */
   public void renameFile(
       Repository repository, ChangeNotes notes, String currentFilePath, String newFilePath)
-      throws AuthException, InvalidChangeOperationException, IOException,
+      throws AuthException, BadRequestException, InvalidChangeOperationException, IOException,
           PermissionBackendException, ResourceConflictException {
     modifyTree(repository, notes, new RenameFileModification(currentFilePath, newFilePath));
   }
@@ -313,14 +317,14 @@
    * @throws PermissionBackendException
    */
   public void restoreFile(Repository repository, ChangeNotes notes, String file)
-      throws AuthException, InvalidChangeOperationException, IOException,
+      throws AuthException, BadRequestException, InvalidChangeOperationException, IOException,
           PermissionBackendException, ResourceConflictException {
     modifyTree(repository, notes, new RestoreFileModification(file));
   }
 
   private void modifyTree(
       Repository repository, ChangeNotes notes, TreeModification treeModification)
-      throws AuthException, IOException, InvalidChangeOperationException,
+      throws AuthException, BadRequestException, IOException, InvalidChangeOperationException,
           PermissionBackendException, ResourceConflictException {
     assertCanEdit(notes);
 
@@ -370,8 +374,8 @@
       ChangeNotes notes,
       PatchSet patchSet,
       List<TreeModification> treeModifications)
-      throws AuthException, IOException, InvalidChangeOperationException, MergeConflictException,
-          PermissionBackendException, ResourceConflictException {
+      throws AuthException, BadRequestException, IOException, InvalidChangeOperationException,
+          MergeConflictException, PermissionBackendException, ResourceConflictException {
     assertCanEdit(notes);
 
     Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(notes);
@@ -486,10 +490,15 @@
 
   private static ObjectId createNewTree(
       Repository repository, RevCommit baseCommit, List<TreeModification> treeModifications)
-      throws IOException, InvalidChangeOperationException {
-    TreeCreator treeCreator = new TreeCreator(baseCommit);
-    treeCreator.addTreeModifications(treeModifications);
-    ObjectId newTreeId = treeCreator.createNewTreeAndGetId(repository);
+      throws BadRequestException, IOException, InvalidChangeOperationException {
+    ObjectId newTreeId;
+    try {
+      TreeCreator treeCreator = new TreeCreator(baseCommit);
+      treeCreator.addTreeModifications(treeModifications);
+      newTreeId = treeCreator.createNewTreeAndGetId(repository);
+    } catch (InvalidPathException e) {
+      throw new BadRequestException(e.getMessage());
+    }
 
     if (ObjectId.isEqual(newTreeId, baseCommit.getTree())) {
       throw new InvalidChangeOperationException("no changes were made");
diff --git a/java/com/google/gerrit/server/git/receive/BUILD b/java/com/google/gerrit/server/git/receive/BUILD
index 7402a37..766a835 100644
--- a/java/com/google/gerrit/server/git/receive/BUILD
+++ b/java/com/google/gerrit/server/git/receive/BUILD
@@ -34,7 +34,7 @@
 
 java_library(
     name = "ref_cache",
-    srcs = glob(["ReceivePackRefCache.java"]),
+    srcs = ["ReceivePackRefCache.java"],
     visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/entities",
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 3d531b2..d8aa054 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -93,6 +93,7 @@
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.extensions.validators.CommentForValidation;
 import com.google.gerrit.extensions.validators.CommentForValidation.CommentType;
+import com.google.gerrit.extensions.validators.CommentValidationContext;
 import com.google.gerrit.extensions.validators.CommentValidationFailure;
 import com.google.gerrit.extensions.validators.CommentValidator;
 import com.google.gerrit.server.ApprovalsUtil;
@@ -2014,8 +2015,13 @@
                                 : CommentType.FILE_COMMENT,
                             comment.message))
                 .collect(toImmutableList());
+        CommentValidationContext ctx =
+            CommentValidationContext.builder()
+                .changeId(change.getChangeId())
+                .project(change.getProject().get())
+                .build();
         ImmutableList<CommentValidationFailure> commentValidationFailures =
-            PublishCommentUtil.findInvalidComments(commentValidators, draftsForValidation);
+            PublishCommentUtil.findInvalidComments(ctx, commentValidators, draftsForValidation);
         magicBranch.setCommentsValid(commentValidationFailures.isEmpty());
         commentValidationFailures.forEach(
             failure ->
@@ -3337,7 +3343,8 @@
                     }
 
                     logger.atFine().log(
-                        "Auto-closing %d changes with existing patch sets and %d with new patch sets",
+                        "Auto-closing %d changes with existing patch sets and %d with new patch"
+                            + " sets",
                         existingPatchSets, newPatchSets);
                     bu.execute();
                   } catch (IOException | StorageException | PermissionBackendException e) {
diff --git a/java/com/google/gerrit/server/git/validators/CommentLimitsValidator.java b/java/com/google/gerrit/server/git/validators/CommentLimitsValidator.java
new file mode 100644
index 0000000..3a8bcac
--- /dev/null
+++ b/java/com/google/gerrit/server/git/validators/CommentLimitsValidator.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git.validators;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.extensions.validators.CommentForValidation;
+import com.google.gerrit.extensions.validators.CommentValidationContext;
+import com.google.gerrit.extensions.validators.CommentValidationFailure;
+import com.google.gerrit.extensions.validators.CommentValidator;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+import org.eclipse.jgit.lib.Config;
+
+/** Limits the size of comments to prevent large comments from causing issues. */
+public class CommentLimitsValidator implements CommentValidator {
+  private final int maxCommentLength;
+
+  @Inject
+  CommentLimitsValidator(@GerritServerConfig Config serverConfig) {
+    maxCommentLength = serverConfig.getInt("change", null, "maxCommentLength", 16 << 10);
+  }
+
+  @Override
+  public ImmutableList<CommentValidationFailure> validateComments(
+      CommentValidationContext ctx, ImmutableList<CommentForValidation> comments) {
+    return comments.stream()
+        .filter(c -> c.getText().length() > maxCommentLength)
+        .map(
+            c ->
+                c.failValidation(
+                    String.format(
+                        "Comment too large (%d > %d)", c.getText().length(), maxCommentLength)))
+        .collect(ImmutableList.toImmutableList());
+  }
+}
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidators.java b/java/com/google/gerrit/server/git/validators/CommitValidators.java
index 90d6b66..4159ebb 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -394,7 +394,7 @@
 
     FileCountValidator(PatchListCache patchListCache, Config config) {
       this.patchListCache = patchListCache;
-      maxFileCount = config.getInt("change", null, "maxFiles", 50_000);
+      maxFileCount = config.getInt("change", null, "maxFiles", 100_000);
     }
 
     @Override
diff --git a/java/com/google/gerrit/server/group/GroupResolver.java b/java/com/google/gerrit/server/group/GroupResolver.java
index a54f465..1aa265b 100644
--- a/java/com/google/gerrit/server/group/GroupResolver.java
+++ b/java/com/google/gerrit/server/group/GroupResolver.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.group;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.entities.AccountGroup;
@@ -28,6 +29,8 @@
 
 @Singleton
 public class GroupResolver {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private final GroupBackend groupBackend;
   private final GroupCache groupCache;
   private final GroupControl.Factory groupControlFactory;
@@ -81,36 +84,48 @@
    * @return the group, null if no group is found for the given group ID
    */
   public GroupDescription.Basic parseId(String id) {
+    logger.atFine().log("Parsing group %s", id);
+
     AccountGroup.UUID uuid = AccountGroup.uuid(id);
     if (groupBackend.handles(uuid)) {
+      logger.atFine().log("Group UUID %s is handled by a group backend", uuid.get());
       GroupDescription.Basic d = groupBackend.get(uuid);
       if (d != null) {
+        logger.atFine().log("Found group %s", d.getName());
         return d;
       }
     }
 
     // Might be a numeric AccountGroup.Id. -> Internal group.
     if (id.matches("^[1-9][0-9]*$")) {
+      logger.atFine().log("Group ID %s is a numeric ID", id);
       try {
         AccountGroup.Id groupId = AccountGroup.Id.parse(id);
         Optional<InternalGroup> group = groupCache.get(groupId);
         if (group.isPresent()) {
+          logger.atFine().log(
+              "Found internal group %s (UUID = %s)",
+              group.get().getName(), group.get().getGroupUUID().get());
           return new InternalGroupDescription(group.get());
         }
       } catch (IllegalArgumentException e) {
         // Ignored
+        logger.atFine().withCause(e).log("Parsing numeric group ID %s failed", id);
       }
     }
 
     // Might be a group name, be nice and accept unique names.
+    logger.atFine().log("Try finding a group with name %s", id);
     GroupReference ref = GroupBackends.findExactSuggestion(groupBackend, id);
     if (ref != null) {
       GroupDescription.Basic d = groupBackend.get(ref.getUUID());
       if (d != null) {
+        logger.atFine().log("Found group %s", d.getName());
         return d;
       }
     }
 
+    logger.atFine().log("Group %s not found", id);
     return null;
   }
 }
diff --git a/java/com/google/gerrit/server/mail/receive/MailProcessor.java b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
index 71d8c15..e79696a 100644
--- a/java/com/google/gerrit/server/mail/receive/MailProcessor.java
+++ b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -34,6 +34,7 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.extensions.validators.CommentForValidation;
+import com.google.gerrit.extensions.validators.CommentValidationContext;
 import com.google.gerrit.extensions.validators.CommentValidationFailure;
 import com.google.gerrit.extensions.validators.CommentValidator;
 import com.google.gerrit.mail.HtmlParser;
@@ -287,8 +288,14 @@
                           MAIL_COMMENT_TYPE_TO_VALIDATION_TYPE.get(comment.getType()),
                           comment.getMessage()))
               .collect(ImmutableList.toImmutableList());
+      CommentValidationContext commentValidationCtx =
+          CommentValidationContext.builder()
+              .changeId(cd.change().getChangeId())
+              .project(cd.change().getProject().get())
+              .build();
       ImmutableList<CommentValidationFailure> commentValidationFailures =
-          PublishCommentUtil.findInvalidComments(commentValidators, parsedCommentsForValidation);
+          PublishCommentUtil.findInvalidComments(
+              commentValidationCtx, commentValidators, parsedCommentsForValidation);
       if (!commentValidationFailures.isEmpty()) {
         sendRejectionEmail(message, InboundEmailRejectionSender.Error.COMMENT_REJECTED);
         return;
diff --git a/java/com/google/gerrit/server/restapi/change/ChangeEdits.java b/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
index aa36b73..cbc1b79 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
@@ -138,7 +138,8 @@
 
     @Override
     public Response<?> apply(ChangeResource rsrc, IdString id, Input in)
-        throws IOException, AuthException, ResourceConflictException, PermissionBackendException {
+        throws IOException, AuthException, BadRequestException, ResourceConflictException,
+            PermissionBackendException {
       return deleteContent.apply(rsrc, id.get());
     }
   }
@@ -184,7 +185,8 @@
 
     @Override
     public Response<EditInfo> apply(ChangeResource rsrc)
-        throws AuthException, IOException, ResourceNotFoundException, PermissionBackendException {
+        throws AuthException, IOException, ResourceNotFoundException, ResourceConflictException,
+            PermissionBackendException {
       Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getNotes(), rsrc.getUser());
       if (!edit.isPresent()) {
         return Response.none();
@@ -239,7 +241,8 @@
 
     @Override
     public Response<?> apply(ChangeResource resource, Post.Input input)
-        throws AuthException, IOException, ResourceConflictException, PermissionBackendException {
+        throws AuthException, BadRequestException, IOException, ResourceConflictException,
+            PermissionBackendException {
       Project.NameKey project = resource.getProject();
       try (Repository repository = repositoryManager.openRepository(project)) {
         if (isRestoreFile(input)) {
@@ -325,12 +328,14 @@
 
     @Override
     public Response<?> apply(ChangeEditResource rsrc, Input input)
-        throws AuthException, ResourceConflictException, IOException, PermissionBackendException {
+        throws AuthException, BadRequestException, ResourceConflictException, IOException,
+            PermissionBackendException {
       return apply(rsrc.getChangeResource(), rsrc.getPath());
     }
 
     public Response<?> apply(ChangeResource rsrc, String filePath)
-        throws AuthException, IOException, ResourceConflictException, PermissionBackendException {
+        throws AuthException, BadRequestException, IOException, ResourceConflictException,
+            PermissionBackendException {
       try (Repository repository = repositoryManager.openRepository(rsrc.getProject())) {
         editModifier.deleteFile(repository, rsrc.getNotes(), filePath);
       } catch (InvalidChangeOperationException e) {
diff --git a/java/com/google/gerrit/server/restapi/change/CreateChange.java b/java/com/google/gerrit/server/restapi/change/CreateChange.java
index c0b28d2..537993a 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateChange.java
@@ -508,7 +508,13 @@
     }
 
     RevCommit sourceCommit = MergeUtil.resolveCommit(repo, rw, merge.source);
-    if (!commits.canRead(projectState, repo, sourceCommit)) {
+    if (merge.sourceBranch != null) {
+      Ref ref = repo.findRef(merge.sourceBranch);
+      logger.atFine().log("checking visibility for branch %s", merge.sourceBranch);
+      if (ref == null || !commits.canRead(projectState, repo, sourceCommit, ref)) {
+        throw new BadRequestException("do not have read permission for: " + merge.source);
+      }
+    } else if (!commits.canRead(projectState, repo, sourceCommit)) {
       throw new BadRequestException("do not have read permission for: " + merge.source);
     }
 
diff --git a/java/com/google/gerrit/server/restapi/change/GetPatch.java b/java/com/google/gerrit/server/restapi/change/GetPatch.java
index 31785be..66ccef3 100644
--- a/java/com/google/gerrit/server/restapi/change/GetPatch.java
+++ b/java/com/google/gerrit/server/restapi/change/GetPatch.java
@@ -136,7 +136,9 @@
       } finally {
         if (close) {
           rw.close();
-          bin.close();
+          if (bin != null) {
+            bin.close();
+          }
         }
       }
     } finally {
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 03c2fc4..324069d 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -77,6 +77,7 @@
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.extensions.validators.CommentForValidation;
+import com.google.gerrit.extensions.validators.CommentValidationContext;
 import com.google.gerrit.extensions.validators.CommentValidationFailure;
 import com.google.gerrit.extensions.validators.CommentValidator;
 import com.google.gerrit.json.OutputFormat;
@@ -999,16 +1000,22 @@
         }
       }
 
+      CommentValidationContext commentValidationCtx =
+          CommentValidationContext.builder()
+              .changeId(ctx.getChange().getChangeId())
+              .project(ctx.getChange().getProject().get())
+              .build();
       switch (in.drafts) {
         case PUBLISH:
         case PUBLISH_ALL_REVISIONS:
-          validateComments(Streams.concat(drafts.values().stream(), toPublish.stream()));
+          validateComments(
+              commentValidationCtx, Streams.concat(drafts.values().stream(), toPublish.stream()));
           publishCommentUtil.publish(ctx, ctx.getUpdate(psId), drafts.values(), in.tag);
           comments.addAll(drafts.values());
           break;
         case KEEP:
         default:
-          validateComments(toPublish.stream());
+          validateComments(commentValidationCtx, toPublish.stream());
           break;
       }
       ChangeUpdate changeUpdate = ctx.getUpdate(psId);
@@ -1017,7 +1024,8 @@
       return !toPublish.isEmpty();
     }
 
-    private void validateComments(Stream<Comment> comments) throws CommentsRejectedException {
+    private void validateComments(CommentValidationContext ctx, Stream<Comment> comments)
+        throws CommentsRejectedException {
       ImmutableList<CommentForValidation> draftsForValidation =
           comments
               .map(
@@ -1029,7 +1037,7 @@
                           comment.message))
               .collect(toImmutableList());
       ImmutableList<CommentValidationFailure> draftValidationFailures =
-          PublishCommentUtil.findInvalidComments(commentValidators, draftsForValidation);
+          PublishCommentUtil.findInvalidComments(ctx, commentValidators, draftsForValidation);
       if (!draftValidationFailures.isEmpty()) {
         throw new CommentsRejectedException(draftValidationFailures);
       }
@@ -1415,8 +1423,14 @@
         buf.append(String.format("\n\n(%d comments)", comments.size()));
       }
       if (!msg.isEmpty()) {
+        CommentValidationContext commentValidationCtx =
+            CommentValidationContext.builder()
+                .changeId(ctx.getChange().getChangeId())
+                .project(ctx.getChange().getProject().get())
+                .build();
         ImmutableList<CommentValidationFailure> messageValidationFailure =
             PublishCommentUtil.findInvalidComments(
+                commentValidationCtx,
                 commentValidators,
                 ImmutableList.of(
                     CommentForValidation.create(
diff --git a/java/com/google/gerrit/server/restapi/project/CommitsCollection.java b/java/com/google/gerrit/server/restapi/project/CommitsCollection.java
index 31fa8d3..757b650 100644
--- a/java/com/google/gerrit/server/restapi/project/CommitsCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/CommitsCollection.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.collect.ImmutableList.toImmutableList;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.registration.DynamicMap;
@@ -110,6 +111,14 @@
     return views;
   }
 
+  /**
+   * @return true if {@code commit} is visible to the caller and {@code commit} is reachable from
+   *     the given branch.
+   */
+  public boolean canRead(ProjectState state, Repository repo, RevCommit commit, Ref ref) {
+    return reachable.fromRefs(state.getNameKey(), repo, commit, ImmutableList.of(ref));
+  }
+
   /** @return true if {@code commit} is visible to the caller. */
   public boolean canRead(ProjectState state, Repository repo, RevCommit commit) throws IOException {
     Project.NameKey project = state.getNameKey();
diff --git a/java/com/google/gerrit/server/restapi/project/FilesInCommitCollection.java b/java/com/google/gerrit/server/restapi/project/FilesInCommitCollection.java
index 0ee8279..0d5ab88 100644
--- a/java/com/google/gerrit/server/restapi/project/FilesInCommitCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/FilesInCommitCollection.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.ChildCollection;
 import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
@@ -86,7 +87,7 @@
 
     @Override
     public Response<Map<String, FileInfo>> apply(CommitResource resource)
-        throws PatchListNotAvailableException {
+        throws ResourceConflictException, PatchListNotAvailableException {
       RevCommit commit = resource.getCommit();
       PatchListKey key;
 
diff --git a/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java b/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
index 7156c8d..524a05e 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
@@ -37,6 +38,7 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.validators.CommentForValidation;
 import com.google.gerrit.extensions.validators.CommentForValidation.CommentType;
+import com.google.gerrit.extensions.validators.CommentValidationContext;
 import com.google.gerrit.extensions.validators.CommentValidator;
 import com.google.gerrit.server.restapi.change.PostReview;
 import com.google.gerrit.server.update.CommentsRejectedException;
@@ -80,14 +82,17 @@
 
   @Test
   public void validateCommentsInInput_commentOK() throws Exception {
+    PushOneCommit.Result r = createChange();
     when(mockCommentValidator.validateComments(
+            CommentValidationContext.builder()
+                .changeId(r.getChange().getId().get())
+                .project(r.getChange().project().get())
+                .build(),
             ImmutableList.of(
                 CommentForValidation.create(
                     CommentForValidation.CommentType.FILE_COMMENT, COMMENT_TEXT))))
         .thenReturn(ImmutableList.of());
 
-    PushOneCommit.Result r = createChange();
-
     ReviewInput input = new ReviewInput();
     CommentInput comment = newComment(r.getChange().currentFilePaths().get(0));
     comment.updated = new Timestamp(0);
@@ -101,14 +106,17 @@
 
   @Test
   public void validateCommentsInInput_commentRejected() throws Exception {
+    PushOneCommit.Result r = createChange();
     CommentForValidation commentForValidation =
         CommentForValidation.create(CommentType.FILE_COMMENT, COMMENT_TEXT);
     when(mockCommentValidator.validateComments(
+            CommentValidationContext.builder()
+                .changeId(r.getChange().getId().get())
+                .project(r.getChange().project().get())
+                .build(),
             ImmutableList.of(CommentForValidation.create(CommentType.FILE_COMMENT, COMMENT_TEXT))))
         .thenReturn(ImmutableList.of(commentForValidation.failValidation("Oh no!")));
 
-    PushOneCommit.Result r = createChange();
-
     ReviewInput input = new ReviewInput();
     CommentInput comment = newComment(r.getChange().currentFilePaths().get(0));
     comment.updated = new Timestamp(0);
@@ -151,14 +159,17 @@
 
   @Test
   public void validateDrafts_draftOK() throws Exception {
+    PushOneCommit.Result r = createChange();
     when(mockCommentValidator.validateComments(
+            CommentValidationContext.builder()
+                .changeId(r.getChange().getId().get())
+                .project(r.getChange().project().get())
+                .build(),
             ImmutableList.of(
                 CommentForValidation.create(
                     CommentForValidation.CommentType.INLINE_COMMENT, COMMENT_TEXT))))
         .thenReturn(ImmutableList.of());
 
-    PushOneCommit.Result r = createChange();
-
     DraftInput draft =
         testCommentHelper.newDraft(
             r.getChange().currentFilePaths().get(0), Side.REVISION, 1, COMMENT_TEXT);
@@ -174,14 +185,18 @@
 
   @Test
   public void validateDrafts_draftRejected() throws Exception {
+    PushOneCommit.Result r = createChange();
     CommentForValidation commentForValidation =
         CommentForValidation.create(CommentType.INLINE_COMMENT, COMMENT_TEXT);
     when(mockCommentValidator.validateComments(
+            CommentValidationContext.builder()
+                .changeId(r.getChange().getId().get())
+                .project(r.getChange().project().get())
+                .build(),
             ImmutableList.of(
                 CommentForValidation.create(
                     CommentForValidation.CommentType.INLINE_COMMENT, COMMENT_TEXT))))
         .thenReturn(ImmutableList.of(commentForValidation.failValidation("Oh no!")));
-    PushOneCommit.Result r = createChange();
 
     DraftInput draft =
         testCommentHelper.newDraft(
@@ -218,7 +233,8 @@
     testCommentHelper.addDraft(r.getChangeId(), r.getCommit().getName(), draftFile);
     assertThat(testCommentHelper.getPublishedComments(r.getChangeId())).isEmpty();
 
-    when(mockCommentValidator.validateComments(capture.capture())).thenReturn(ImmutableList.of());
+    when(mockCommentValidator.validateComments(any(), capture.capture()))
+        .thenReturn(ImmutableList.of());
 
     ReviewInput input = new ReviewInput();
     input.drafts = DraftHandling.PUBLISH;
@@ -236,11 +252,15 @@
 
   @Test
   public void validateCommentsInChangeMessage_messageOK() throws Exception {
+    PushOneCommit.Result r = createChange();
     when(mockCommentValidator.validateComments(
+            CommentValidationContext.builder()
+                .changeId(r.getChange().getId().get())
+                .project(r.getChange().project().get())
+                .build(),
             ImmutableList.of(
                 CommentForValidation.create(CommentType.CHANGE_MESSAGE, COMMENT_TEXT))))
         .thenReturn(ImmutableList.of());
-    PushOneCommit.Result r = createChange();
 
     ReviewInput input = new ReviewInput().message(COMMENT_TEXT);
     int numMessages = gApi.changes().id(r.getChangeId()).get().messages.size();
@@ -253,13 +273,17 @@
 
   @Test
   public void validateCommentsInChangeMessage_messageRejected() throws Exception {
+    PushOneCommit.Result r = createChange();
     CommentForValidation commentForValidation =
         CommentForValidation.create(CommentType.CHANGE_MESSAGE, COMMENT_TEXT);
     when(mockCommentValidator.validateComments(
+            CommentValidationContext.builder()
+                .changeId(r.getChange().getId().get())
+                .project(r.getChange().project().get())
+                .build(),
             ImmutableList.of(
                 CommentForValidation.create(CommentType.CHANGE_MESSAGE, COMMENT_TEXT))))
         .thenReturn(ImmutableList.of(commentForValidation.failValidation("Oh no!")));
-    PushOneCommit.Result r = createChange();
 
     ReviewInput input = new ReviewInput().message(COMMENT_TEXT);
     assertThat(gApi.changes().id(r.getChangeId()).get().messages)
diff --git a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
index b0f183e..2883d8c 100644
--- a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
+++ b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
@@ -59,6 +59,7 @@
 import com.google.gerrit.extensions.common.EditInfo;
 import com.google.gerrit.extensions.common.FileInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.server.ChangeMessagesUtil;
@@ -436,6 +437,16 @@
   }
 
   @Test
+  public void renameExistingFileToInvalidPath() throws Exception {
+    createEmptyEditFor(changeId);
+    BadRequestException badRequest =
+        assertThrows(
+            BadRequestException.class,
+            () -> gApi.changes().id(changeId).edit().renameFile(FILE_NAME, "invalid/path/"));
+    assertThat(badRequest.getMessage()).isEqualTo("Invalid path: invalid/path/");
+  }
+
+  @Test
   public void createEditByDeletingExistingFileRest() throws Exception {
     adminRestSession.delete(urlEditFile(changeId, FILE_NAME)).assertNoContent();
     assertThat(getFileContentOfEdit(changeId, FILE_NAME)).isAbsent();
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index 10bae39..3f80dd1 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -619,6 +619,8 @@
 
     gApi.projects().name(project.get()).branch(mergeTarget).create(branchInput);
 
+    // To create a merge commit, create two changes from the same parent,
+    // and submit them one after the other.
     PushOneCommit.Result result1 =
         pushFactory
             .create(
@@ -668,6 +670,39 @@
     gApi.changes().create(in);
   }
 
+  @Test
+  public void createChangeWithSourceBranch() throws Exception {
+    changeInTwoBranches("branchA", "a.txt", "branchB", "b.txt");
+
+    // create a merge change from branchA to master in gerrit
+    ChangeInput in = new ChangeInput();
+    in.project = project.get();
+    in.branch = "branchA";
+    in.subject = "message";
+    in.status = ChangeStatus.NEW;
+    MergeInput mergeInput = new MergeInput();
+
+    String mergeRev = gApi.projects().name(project.get()).branch("branchB").get().revision;
+    mergeInput.source = mergeRev;
+    in.merge = mergeInput;
+
+    assertCreateSucceeds(in);
+
+    // Succeeds with a visible branch
+    in.merge.sourceBranch = "refs/heads/branchB";
+    gApi.changes().create(in);
+
+    // Make it invisible
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(READ).ref(in.merge.sourceBranch).group(REGISTERED_USERS))
+        .update();
+
+    // Now it fails.
+    assertThrows(BadRequestException.class, () -> gApi.changes().create(in));
+  }
+
   private ChangeInput newChangeInput(ChangeStatus status) {
     ChangeInput in = new ChangeInput();
     in.project = project.get();
diff --git a/javatests/com/google/gerrit/acceptance/server/git/receive/ReceiveCommitsCommentValidationIT.java b/javatests/com/google/gerrit/acceptance/server/git/receive/ReceiveCommitsCommentValidationIT.java
index 84bf759c..ccfe783 100644
--- a/javatests/com/google/gerrit/acceptance/server/git/receive/ReceiveCommitsCommentValidationIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/git/receive/ReceiveCommitsCommentValidationIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.server.git.receive;
 
 import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
@@ -24,12 +25,14 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.PushOneCommit.Result;
+import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.extensions.validators.CommentForValidation;
 import com.google.gerrit.extensions.validators.CommentForValidation.CommentType;
+import com.google.gerrit.extensions.validators.CommentValidationContext;
 import com.google.gerrit.extensions.validators.CommentValidator;
 import com.google.gerrit.testing.TestCommentHelper;
 import com.google.inject.Inject;
@@ -46,9 +49,12 @@
   @Inject private CommentValidator mockCommentValidator;
   @Inject private TestCommentHelper testCommentHelper;
 
+  private static final int MAX_COMMENT_LENGTH = 666;
+
   private static final String COMMENT_TEXT = "The comment text";
 
   @Captor private ArgumentCaptor<ImmutableList<CommentForValidation>> capture;
+  @Captor private ArgumentCaptor<CommentValidationContext> captureCtx;
 
   @Override
   public Module createModule() {
@@ -72,14 +78,18 @@
 
   @Test
   public void validateComments_commentOK() throws Exception {
+    PushOneCommit.Result result = createChange();
+    String changeId = result.getChangeId();
+    String revId = result.getCommit().getName();
     when(mockCommentValidator.validateComments(
+            CommentValidationContext.builder()
+                .changeId(result.getChange().getId().get())
+                .project(result.getChange().project().get())
+                .build(),
             ImmutableList.of(
                 CommentForValidation.create(
                     CommentForValidation.CommentType.FILE_COMMENT, COMMENT_TEXT))))
         .thenReturn(ImmutableList.of());
-    PushOneCommit.Result result = createChange();
-    String changeId = result.getChangeId();
-    String revId = result.getCommit().getName();
     DraftInput comment = testCommentHelper.newDraft(COMMENT_TEXT);
     testCommentHelper.addDraft(changeId, revId, comment);
     assertThat(testCommentHelper.getPublishedComments(result.getChangeId())).isEmpty();
@@ -93,14 +103,18 @@
   public void validateComments_commentRejected() throws Exception {
     CommentForValidation commentForValidation =
         CommentForValidation.create(CommentType.FILE_COMMENT, COMMENT_TEXT);
+    PushOneCommit.Result result = createChange();
+    String changeId = result.getChangeId();
+    String revId = result.getCommit().getName();
     when(mockCommentValidator.validateComments(
+            CommentValidationContext.builder()
+                .changeId(result.getChange().getId().get())
+                .project(result.getChange().project().get())
+                .build(),
             ImmutableList.of(
                 CommentForValidation.create(
                     CommentForValidation.CommentType.FILE_COMMENT, COMMENT_TEXT))))
         .thenReturn(ImmutableList.of(commentForValidation.failValidation("Oh no!")));
-    PushOneCommit.Result result = createChange();
-    String changeId = result.getChangeId();
-    String revId = result.getCommit().getName();
     DraftInput comment = testCommentHelper.newDraft(COMMENT_TEXT);
     testCommentHelper.addDraft(changeId, revId, comment);
     assertThat(testCommentHelper.getPublishedComments(result.getChangeId())).isEmpty();
@@ -112,7 +126,8 @@
 
   @Test
   public void validateComments_inlineVsFileComments_allOK() throws Exception {
-    when(mockCommentValidator.validateComments(capture.capture())).thenReturn(ImmutableList.of());
+    when(mockCommentValidator.validateComments(captureCtx.capture(), capture.capture()))
+        .thenReturn(ImmutableList.of());
     PushOneCommit.Result result = createChange();
     String changeId = result.getChangeId();
     String revId = result.getCommit().getName();
@@ -128,6 +143,9 @@
 
     assertThat(capture.getAllValues()).hasSize(1);
 
+    assertThat(captureCtx.getValue().getProject()).isEqualTo(result.getChange().project().get());
+    assertThat(captureCtx.getValue().getChangeId()).isEqualTo(result.getChange().getId().get());
+
     assertThat(capture.getAllValues().get(0))
         .containsExactly(
             CommentForValidation.create(
@@ -135,4 +153,23 @@
             CommentForValidation.create(
                 CommentForValidation.CommentType.FILE_COMMENT, draftFile.message));
   }
+
+  @Test
+  @GerritConfig(name = "change.maxCommentLength", value = "" + MAX_COMMENT_LENGTH)
+  public void validateComments_enforceLimits_commentTooLarge() throws Exception {
+    when(mockCommentValidator.validateComments(any(), any())).thenReturn(ImmutableList.of());
+    PushOneCommit.Result result = createChange();
+    String changeId = result.getChangeId();
+    int commentLength = MAX_COMMENT_LENGTH + 1;
+    DraftInput comment =
+        testCommentHelper.newDraft(new String(new char[commentLength]).replace("\0", "x"));
+    testCommentHelper.addDraft(changeId, result.getCommit().getName(), comment);
+
+    assertThat(testCommentHelper.getPublishedComments(result.getChangeId())).isEmpty();
+    Result amendResult = amendChange(changeId, "refs/for/master%publish-comments", admin, testRepo);
+    amendResult.assertOkStatus();
+    amendResult.assertMessage(
+        String.format("Comment too large (%d > %d)", commentLength, MAX_COMMENT_LENGTH));
+    assertThat(testCommentHelper.getPublishedComments(result.getChangeId())).isEmpty();
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
index 5531709..2409f52 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.extensions.validators.CommentForValidation;
+import com.google.gerrit.extensions.validators.CommentValidationContext;
 import com.google.gerrit.extensions.validators.CommentValidator;
 import com.google.gerrit.mail.MailMessage;
 import com.google.gerrit.mail.MailProcessingUtil;
@@ -70,7 +71,7 @@
   @BeforeClass
   public static void setUpMock() {
     // Let the mock comment validator accept all comments during test setup.
-    when(mockCommentValidator.validateComments(any())).thenReturn(ImmutableList.of());
+    when(mockCommentValidator.validateComments(any(), any())).thenReturn(ImmutableList.of());
   }
 
   @Before
@@ -274,7 +275,8 @@
         MailProcessingUtil.rfcDateformatter.format(
             ZonedDateTime.ofInstant(comments.get(0).updated.toInstant(), ZoneId.of("UTC")));
 
-    setupFailValidation(CommentForValidation.CommentType.CHANGE_MESSAGE);
+    setupFailValidation(
+        CommentForValidation.CommentType.CHANGE_MESSAGE, changeInfo.project, changeInfo._number);
 
     MailMessage.Builder b = messageBuilderWithDefaultFields();
     String txt = newPlaintextBody(getChangeUrl(changeInfo) + "/1", COMMENT_TEXT, null, null, null);
@@ -298,7 +300,8 @@
         MailProcessingUtil.rfcDateformatter.format(
             ZonedDateTime.ofInstant(comments.get(0).updated.toInstant(), ZoneId.of("UTC")));
 
-    setupFailValidation(CommentForValidation.CommentType.INLINE_COMMENT);
+    setupFailValidation(
+        CommentForValidation.CommentType.INLINE_COMMENT, changeInfo.project, changeInfo._number);
 
     MailMessage.Builder b = messageBuilderWithDefaultFields();
     String txt = newPlaintextBody(getChangeUrl(changeInfo) + "/1", null, COMMENT_TEXT, null, null);
@@ -322,7 +325,8 @@
         MailProcessingUtil.rfcDateformatter.format(
             ZonedDateTime.ofInstant(comments.get(0).updated.toInstant(), ZoneId.of("UTC")));
 
-    setupFailValidation(CommentForValidation.CommentType.FILE_COMMENT);
+    setupFailValidation(
+        CommentForValidation.CommentType.FILE_COMMENT, changeInfo.project, changeInfo._number);
 
     MailMessage.Builder b = messageBuilderWithDefaultFields();
     String txt = newPlaintextBody(getChangeUrl(changeInfo) + "/1", null, null, COMMENT_TEXT, null);
@@ -341,10 +345,12 @@
     return canonicalWebUrl.get() + "c/" + changeInfo.project + "/+/" + changeInfo._number;
   }
 
-  private void setupFailValidation(CommentForValidation.CommentType type) {
+  private void setupFailValidation(
+      CommentForValidation.CommentType type, String failProject, int failChange) {
     CommentForValidation commentForValidation = CommentForValidation.create(type, COMMENT_TEXT);
 
     when(mockCommentValidator.validateComments(
+            CommentValidationContext.builder().changeId(failChange).project(failProject).build(),
             ImmutableList.of(CommentForValidation.create(type, COMMENT_TEXT))))
         .thenReturn(ImmutableList.of(commentForValidation.failValidation("Oh no!")));
   }
diff --git a/javatests/com/google/gerrit/pgm/init/LibrariesTest.java b/javatests/com/google/gerrit/pgm/init/LibrariesTest.java
deleted file mode 100644
index 5aa4718..0000000
--- a/javatests/com/google/gerrit/pgm/init/LibrariesTest.java
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.pgm.init;
-
-import static org.junit.Assert.assertNotNull;
-import static org.mockito.Mockito.verifyZeroInteractions;
-
-import com.google.gerrit.pgm.init.api.ConsoleUI;
-import com.google.gerrit.server.config.SitePaths;
-import java.nio.file.Paths;
-import java.util.Collections;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnitRunner;
-
-@RunWith(MockitoJUnitRunner.class)
-public class LibrariesTest {
-  @Mock ConsoleUI ui;
-  @Mock StaleLibraryRemover remover;
-
-  @Test
-  public void create() throws Exception {
-    final SitePaths site = new SitePaths(Paths.get("."));
-
-    Libraries lib =
-        new Libraries(
-            () -> new LibraryDownloader(ui, site, remover), Collections.emptyList(), false);
-
-    assertNotNull(lib.mysqlDriver);
-    verifyZeroInteractions(ui);
-    verifyZeroInteractions(remover);
-  }
-}
diff --git a/lib/guava.bzl b/lib/guava.bzl
index 18a8355..86060d4 100644
--- a/lib/guava.bzl
+++ b/lib/guava.bzl
@@ -1,5 +1,5 @@
-GUAVA_VERSION = "28.1-jre"
+GUAVA_VERSION = "28.2-jre"
 
-GUAVA_BIN_SHA1 = "b0e91dcb6a44ffb6221b5027e12a5cb34b841145"
+GUAVA_BIN_SHA1 = "8ec9ed76528425762174f0011ce8f74ad845b756"
 
 GUAVA_DOC_URL = "https://google.github.io/guava/releases/" + GUAVA_VERSION + "/api/docs/"
diff --git a/lib/lucene/BUILD b/lib/lucene/BUILD
index 5ca9580..93eeccb 100644
--- a/lib/lucene/BUILD
+++ b/lib/lucene/BUILD
@@ -1,5 +1,4 @@
 load("@rules_java//java:defs.bzl", "java_binary", "java_import", "java_library")
-load("//tools/bzl:maven.bzl", "merge_maven_jars")
 
 package(default_visibility = ["//visibility:public"])
 
diff --git a/plugins/delete-project b/plugins/delete-project
index 39dd25c..7885a9f 160000
--- a/plugins/delete-project
+++ b/plugins/delete-project
@@ -1 +1 @@
-Subproject commit 39dd25c822be14a69282d38e5205f0ca4f2902c1
+Subproject commit 7885a9fa8d262b6127a7a3aa294108d6aac47a4d
diff --git a/plugins/gitiles b/plugins/gitiles
index 22b8e24..825ca06 160000
--- a/plugins/gitiles
+++ b/plugins/gitiles
@@ -1 +1 @@
-Subproject commit 22b8e242b5eaa9eae817b776bc862b096479ceaa
+Subproject commit 825ca06dddc9de89daa6b126dfc187fbeb25280c
diff --git a/plugins/webhooks b/plugins/webhooks
index 377fec1..570daca 160000
--- a/plugins/webhooks
+++ b/plugins/webhooks
@@ -1 +1 @@
-Subproject commit 377fec17d2c0fc71cdfb1d12f502cfa1eba1fbd7
+Subproject commit 570dacacc64f7c01e0bc0f1301aa1d5a218cfc1b
diff --git a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior.html b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior.html
index d3cd940..d03316a 100644
--- a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior.html
@@ -55,6 +55,9 @@
      * @return {boolean}
      */
     isColumnHidden(columnToCheck, columnsToDisplay) {
+      if ([columnsToDisplay, columnToCheck].some(arg => arg === undefined)) {
+        return false;
+      }
       return !columnsToDisplay.includes(columnToCheck);
     },
 
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
index 609a00d..1df2a41 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
@@ -245,31 +245,43 @@
       });
     });
 
-    test('_getAccountSuggestions empty', () => element
-        ._getAccountSuggestions('nonexistent').then(accounts => {
-          assert.equal(accounts.length, 0);
-        }));
+    test('_getAccountSuggestions empty', done => {
+      element
+          ._getAccountSuggestions('nonexistent').then(accounts => {
+            assert.equal(accounts.length, 0);
+            done();
+          });
+    });
 
-    test('_getAccountSuggestions non-empty', () => element
-        ._getAccountSuggestions('test-').then(accounts => {
-          assert.equal(accounts.length, 3);
-          assert.equal(accounts[0].name,
-              'test-account <test.account@example.com>');
-          assert.equal(accounts[1].name, 'test-admin <test.admin@example.com>');
-          assert.equal(accounts[2].name, 'test-git');
-        }));
+    test('_getAccountSuggestions non-empty', done => {
+      element
+          ._getAccountSuggestions('test-').then(accounts => {
+            assert.equal(accounts.length, 3);
+            assert.equal(accounts[0].name,
+                'test-account <test.account@example.com>');
+            assert.equal(accounts[1].name, 'test-admin <test.admin@example.com>');
+            assert.equal(accounts[2].name, 'test-git');
+            done();
+          });
+    });
 
-    test('_getGroupSuggestions empty', () => element
-        ._getGroupSuggestions('nonexistent').then(groups => {
-          assert.equal(groups.length, 0);
-        }));
+    test('_getGroupSuggestions empty', done => {
+      element
+          ._getGroupSuggestions('nonexistent').then(groups => {
+            assert.equal(groups.length, 0);
+            done();
+          });
+    });
 
-    test('_getGroupSuggestions non-empty', () => element
-        ._getGroupSuggestions('test').then(groups => {
-          assert.equal(groups.length, 2);
-          assert.equal(groups[0].name, 'test-admin');
-          assert.equal(groups[1].name, 'test/Administrator (admin)');
-        }));
+    test('_getGroupSuggestions non-empty', done => {
+      element
+          ._getGroupSuggestions('test').then(groups => {
+            assert.equal(groups.length, 2);
+            assert.equal(groups[0].name, 'test-admin');
+            assert.equal(groups[1].name, 'test/Administrator (admin)');
+            done();
+          });
+    });
 
     test('_computeHideItemClass returns string for admin', () => {
       const admin = true;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html
index 6f68525..7b79a7e3 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html
@@ -307,13 +307,18 @@
         });
       });
 
-      test('inherited submit type value is calculated correctly', () => element
-          ._loadRepo().then(() => {
-            const sel = element.$.submitTypeSelect;
-            assert.equal(sel.bindValue, 'INHERIT');
-            assert.equal(
-                sel.nativeSelect.options[0].text, 'Inherit (Merge if necessary)');
-          }));
+      test('inherited submit type value is calculated correctly', done => {
+        element
+            ._loadRepo().then(() => {
+              const sel = element.$.submitTypeSelect;
+              assert.equal(sel.bindValue, 'INHERIT');
+              assert.equal(
+                  sel.nativeSelect.options[0].text,
+                  'Inherit (Merge if necessary)'
+              );
+              done();
+            });
+      });
 
       test('fields update and save correctly', () => {
         const configInputObj = {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
index 2d6b440..8fd4214 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
@@ -187,8 +187,13 @@
       if (account) {
         this.showNumber = !!(preferences &&
             preferences.legacycid_in_change_table);
-        this.visibleChangeTableColumns = preferences.change_table.length > 0 ?
-          this.getVisibleColumns(preferences.change_table) : this.columnNames;
+        if (preferences.change_table &&
+            preferences.change_table.length > 0) {
+          this.visibleChangeTableColumns =
+            this.getVisibleColumns(preferences.change_table);
+        } else {
+          this.visibleChangeTableColumns = this.columnNames;
+        }
       } else {
         // Not logged in.
         this.showNumber = false;
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index e4ea218..9c1a527 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -1554,14 +1554,17 @@
               'navigateToChange').returns(Promise.resolve(true));
         });
 
-        test('change action', () => element
-            ._send('DELETE', payload, '/endpoint', false, cleanup)
-            .then(() => {
-              assert.isFalse(onShowError.called);
-              assert.isTrue(cleanup.calledOnce);
-              assert.isTrue(sendStub.calledWith(42, 'DELETE', '/endpoint',
-                  null, payload));
-            }));
+        test('change action', done => {
+          element
+              ._send('DELETE', payload, '/endpoint', false, cleanup)
+              .then(() => {
+                assert.isFalse(onShowError.called);
+                assert.isTrue(cleanup.calledOnce);
+                assert.isTrue(sendStub.calledWith(42, 'DELETE', '/endpoint',
+                    null, payload));
+                done();
+              });
+        });
 
         suite('show revert submission dialog', () => {
           setup(() => {
@@ -1645,14 +1648,17 @@
           });
         });
 
-        test('revision action', () => element
-            ._send('DELETE', payload, '/endpoint', true, cleanup)
-            .then(() => {
-              assert.isFalse(onShowError.called);
-              assert.isTrue(cleanup.calledOnce);
-              assert.isTrue(sendStub.calledWith(42, 'DELETE', '/endpoint',
-                  12, payload));
-            }));
+        test('revision action', done => {
+          element
+              ._send('DELETE', payload, '/endpoint', true, cleanup)
+              .then(() => {
+                assert.isFalse(onShowError.called);
+                assert.isTrue(cleanup.calledOnce);
+                assert.isTrue(sendStub.calledWith(42, 'DELETE', '/endpoint',
+                    12, payload));
+                done();
+              });
+        });
       });
 
       suite('failure modes', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index ec42824..471560d 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -194,8 +194,6 @@
         max-height: 36em;
         overflow: hidden;
       }
-      #relatedChanges {
-      }
       #relatedChanges.collapsed {
         margin-bottom: var(--spacing-l);
         max-height: var(--relation-chain-max-height, 2em);
@@ -391,10 +389,15 @@
               change="{{_change}}"
               on-toggle-star="_handleToggleStar"
               hidden$="[[!_loggedIn]]"></gr-change-star>
+
           <a aria-label$="[[_computeChangePermalinkAriaLabel(_change._number)]]"
               href$="[[_computeChangeUrl(_change)]]">[[_change._number]]</a>
           <span class="changeNumberColon">:&nbsp;</span>
           <span class="headerSubject">[[_change.subject]]</span>
+          <gr-copy-clipboard
+            hide-input
+            text="[[_computeCopyTextForTitle(_change)]]">
+          </gr-copy-clipboard>
         </div><!-- end headerTitle -->
         <div class="commitActions" hidden$="[[!_loggedIn]]">
           <gr-change-actions
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index 502cb27..058bce8 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -1597,6 +1597,17 @@
       return collapsed ? '\u25bc Show more' : '\u25b2 Show less';
     }
 
+    /**
+     * Returns the text to be copied when
+     * click the copy icon next to change subject
+     *
+     * @param {!Object} change
+     */
+    _computeCopyTextForTitle(change) {
+      return `${change._number}: ${change.subject}` +
+       ` | https://${location.host}${this._computeChangeUrl(change)}`;
+    }
+
     _toggleCommitCollapsed() {
       this._commitCollapsed = !this._commitCollapsed;
       if (this._commitCollapsed) {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index 7e776b5..9c23d0e 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -803,6 +803,24 @@
       assert.deepEqual(commit, {commit: 2});
     });
 
+    test('_computeCopyTextForTitle', () => {
+      const change = {
+        _number: 123,
+        subject: 'test subject',
+        revisions: {
+          rev1: {_number: 1},
+          rev3: {_number: 3},
+        },
+        current_revision: 'rev3',
+      };
+      sandbox.stub(Gerrit.Nav, 'getUrlForChange')
+          .returns('/change/123');
+      assert.equal(
+          element._computeCopyTextForTitle(change),
+          '123: test subject | https://localhost:8081/change/123'
+      );
+    });
+
     test('get latest revision', () => {
       let change = {
         revisions: {
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
index 9ef2461..5a7e58d 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
@@ -178,6 +178,8 @@
     _showAlert(text, opt_actionText, opt_actionCallback,
         opt_dismissOnNavigation) {
       if (this._alertElement) {
+        // do not override auth alerts
+        if (this._alertElement.type === 'AUTH') return;
         this._hideAlert();
       }
 
@@ -212,10 +214,14 @@
     }
 
     _showAuthErrorAlert(errorText, actionText) {
-      // TODO(viktard): close alert if it's not for auth error.
-      if (this._alertElement) { return; }
+      // hide any existing alert like `reload`
+      // as auth error should have the highest priority
+      if (this._alertElement) {
+        this._alertElement.hide();
+      }
 
       this._alertElement = this._createToastAlert();
+      this._alertElement.type = 'AUTH';
       this._alertElement.show(errorText, actionText,
           this._createLoginPopup.bind(this));
 
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
index a0a5731..72f5d5b 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
@@ -52,6 +52,7 @@
         sandbox.stub(window, 'fetch')
             .returns(Promise.resolve({ok: true, status: 204}));
         element = fixture('basic');
+        element._authService.clearCache();
       });
 
       test('does not show auth error on 403 by default', done => {
@@ -184,7 +185,8 @@
         );
         assert.equal(window.fetch.callCount, 1);
         flush(() => {
-          // auth check again
+          // here needs two flush as there are two chanined
+          // promises on server-error handler and flush only flushes one
           assert.equal(window.fetch.callCount, 2);
           flush(() => {
             // auth-error fired
@@ -238,6 +240,97 @@
         });
       });
 
+      test('auth toast should dismiss existing toast', done => {
+        // starts with authed state
+        element.$.restAPI.getLoggedIn();
+        const toastSpy = sandbox.spy(element, '_createToastAlert');
+        const responseText = Promise.resolve('Authentication required\n');
+
+        // fake an alert
+        element.fire('show-alert', {message: 'test reload', action: 'reload'});
+        const toast = toastSpy.lastCall.returnValue;
+        assert.isOk(toast);
+        assert.include(
+            Polymer.dom(toast.root).textContent, 'test reload');
+
+        // fake auth
+        window.fetch.returns(Promise.resolve({status: 403}));
+        element.fire('server-error',
+            {response: {status: 403, text() { return responseText; }}}
+        );
+        assert.equal(window.fetch.callCount, 1);
+        flush(() => {
+          // here needs two flush as there are two chanined
+          // promises on server-error handler and flush only flushes one
+          assert.equal(window.fetch.callCount, 2);
+          flush(() => {
+            // toast
+            const toast = toastSpy.lastCall.returnValue;
+            assert.include(
+                Polymer.dom(toast.root).textContent, 'Credentails expired.');
+            assert.include(
+                Polymer.dom(toast.root).textContent, 'Refresh credentials');
+            done();
+          });
+        });
+      });
+
+      test('regular toast should dismiss regular toast', () => {
+        // starts with authed state
+        element.$.restAPI.getLoggedIn();
+        const toastSpy = sandbox.spy(element, '_createToastAlert');
+
+        // fake an alert
+        element.fire('show-alert', {message: 'test reload', action: 'reload'});
+        let toast = toastSpy.lastCall.returnValue;
+        assert.isOk(toast);
+        assert.include(
+            Polymer.dom(toast.root).textContent, 'test reload');
+
+        // new alert
+        element.fire('show-alert', {message: 'second-test', action: 'reload'});
+
+        toast = toastSpy.lastCall.returnValue;
+        assert.include(Polymer.dom(toast.root).textContent, 'second-test');
+      });
+
+      test('regular toast should not dismiss auth toast', done => {
+        // starts with authed state
+        element.$.restAPI.getLoggedIn();
+        const toastSpy = sandbox.spy(element, '_createToastAlert');
+        const responseText = Promise.resolve('Authentication required\n');
+
+        // fake auth
+        window.fetch.returns(Promise.resolve({status: 403}));
+        element.fire('server-error',
+            {response: {status: 403, text() { return responseText; }}}
+        );
+        assert.equal(window.fetch.callCount, 1);
+        flush(() => {
+          // here needs two flush as there are two chanined
+          // promises on server-error handler and flush only flushes one
+          assert.equal(window.fetch.callCount, 2);
+          flush(() => {
+            let toast = toastSpy.lastCall.returnValue;
+            assert.include(
+                Polymer.dom(toast.root).textContent, 'Credentails expired.');
+            assert.include(
+                Polymer.dom(toast.root).textContent, 'Refresh credentials');
+
+            // fake an alert
+            element.fire('show-alert', {
+              message: 'test-alert', action: 'reload',
+            });
+            toast = toastSpy.lastCall.returnValue;
+            assert.isOk(toast);
+            assert.include(
+                Polymer.dom(toast.root).textContent, 'Credentails expired.');
+
+            done();
+          });
+        });
+      });
+
       test('show alert', () => {
         const alertObj = {message: 'foo'};
         sandbox.stub(element, '_showAlert');
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
index d5eeee8..53c5ef5 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
@@ -305,7 +305,8 @@
       if (!account) { return; }
 
       this.$.restAPI.getPreferences().then(prefs => {
-        this._userLinks = prefs.my.map(this._fixCustomMenuItem);
+        this._userLinks = prefs && prefs.my ?
+          prefs.my.map(this._fixCustomMenuItem) : [];
       });
     }
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
index 359a039..e46f959 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
@@ -192,15 +192,20 @@
       }
     }
 
-    /**
-     * For Polymer 2, use shadowRoot.getSelection instead.
-     */
     _getSelection() {
-      const diffHost = util.querySelector(document.body, 'gr-diff');
-      const selection = diffHost &&
-        diffHost.shadowRoot &&
-        diffHost.shadowRoot.getSelection();
-      return selection ? selection: window.getSelection();
+      const diffHosts = util.querySelectorAll(document.body, 'gr-diff');
+      if (!diffHosts.length) return window.getSelection();
+
+      const curDiffHost = diffHosts.find(diffHost => {
+        if (!diffHost || !diffHost.shadowRoot) return false;
+        const selection = diffHost.shadowRoot.getSelection();
+        // Pick the one with valid selection:
+        // https://developer.mozilla.org/en-US/docs/Web/API/Selection/type
+        return selection && selection.type !== 'None';
+      });
+
+      return curDiffHost ?
+        curDiffHost.shadowRoot.getSelection(): window.getSelection();
     }
 
     /**
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html
index 8ef561c..277bb78 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html
@@ -336,12 +336,16 @@
     });
   });
 
-  test('openOpenDialog', () => element.openOpenDialog('test/path.cpp')
-      .then(() => {
-        assert.isFalse(element.$.openDialog.hasAttribute('hidden'));
-        assert.equal(element.$.openDialog.querySelector('gr-autocomplete').text,
-            'test/path.cpp');
-      }));
+  test('openOpenDialog', done => {
+    element.openOpenDialog('test/path.cpp')
+        .then(() => {
+          assert.isFalse(element.$.openDialog.hasAttribute('hidden'));
+          assert.equal(
+              element.$.openDialog.querySelector('gr-autocomplete').text,
+              'test/path.cpp');
+          done();
+        });
+  });
 
   test('_getDialogFromEvent', () => {
     const spy = sandbox.spy(element, '_getDialogFromEvent');
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html
index 8eabe2a..e6f446b 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html
@@ -56,9 +56,12 @@
       assert.isOk(element);
     });
 
-    test('open uses open() from gr-overlay', () => element.open().then(() => {
-      assert.isTrue(element.$.overlay.open.called);
-    }));
+    test('open uses open() from gr-overlay', done => {
+      element.open().then(() => {
+        assert.isTrue(element.$.overlay.open.called);
+        done();
+      });
+    });
 
     test('close uses close() from gr-overlay', () => {
       element.close();
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html
index af3d770..55b7ac5 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html
@@ -69,22 +69,28 @@
         instance = new GrPopupInterface(plugin);
       });
 
-      test('open', () => instance.open().then(api => {
-        assert.strictEqual(api, instance);
-        const manual = document.createElement('div');
-        manual.id = 'foobar';
-        manual.innerHTML = 'manual content';
-        api._getElement().appendChild(manual);
-        flushAsynchronousOperations();
-        assert.equal(
-            container.querySelector('#foobar').textContent, 'manual content');
-      }));
+      test('open', done => {
+        instance.open().then(api => {
+          assert.strictEqual(api, instance);
+          const manual = document.createElement('div');
+          manual.id = 'foobar';
+          manual.innerHTML = 'manual content';
+          api._getElement().appendChild(manual);
+          flushAsynchronousOperations();
+          assert.equal(
+              container.querySelector('#foobar').textContent, 'manual content');
+          done();
+        });
+      });
 
-      test('close', () => instance.open().then(api => {
-        assert.isTrue(api._getElement().node.opened);
-        api.close();
-        assert.isFalse(api._getElement().node.opened);
-      }));
+      test('close', done => {
+        instance.open().then(api => {
+          assert.isTrue(api._getElement().node.opened);
+          api.close();
+          assert.isFalse(api._getElement().node.opened);
+          done();
+        });
+      });
     });
 
     suite('components', () => {
@@ -92,16 +98,22 @@
         instance = new GrPopupInterface(plugin, 'gr-user-test-popup');
       });
 
-      test('open', () => instance.open().then(api => {
-        assert.isNotNull(
-            Polymer.dom(container).querySelector('gr-user-test-popup'));
-      }));
+      test('open', done => {
+        instance.open().then(api => {
+          assert.isNotNull(
+              Polymer.dom(container).querySelector('gr-user-test-popup'));
+          done();
+        });
+      });
 
-      test('close', () => instance.open().then(api => {
-        assert.isTrue(api._getElement().node.opened);
-        api.close();
-        assert.isFalse(api._getElement().node.opened);
-      }));
+      test('close', done => {
+        instance.open().then(api => {
+          assert.isTrue(api._getElement().node.opened);
+          api.close();
+          assert.isFalse(api._getElement().node.opened);
+          done();
+        });
+      });
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
index 215f469..6a0769d 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
@@ -32,6 +32,8 @@
       return {
         text: String,
         actionText: String,
+        /** @type {?string} */
+        type: String,
         shown: {
           type: Boolean,
           value: true,
diff --git a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html
index adc4f68..3bad2a9 100644
--- a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html
@@ -126,16 +126,19 @@
             });
       });
 
-      test('does not query when repo is unset', () => element
-          ._getRepoBranchesSuggestions('')
-          .then(() => {
-            assert.isFalse(element.$.restAPI.getRepoBranches.called);
-            element.repo = 'gerrit';
-            return element._getRepoBranchesSuggestions('');
-          })
-          .then(() => {
-            assert.isTrue(element.$.restAPI.getRepoBranches.called);
-          }));
+      test('does not query when repo is unset', done => {
+        element
+            ._getRepoBranchesSuggestions('')
+            .then(() => {
+              assert.isFalse(element.$.restAPI.getRepoBranches.called);
+              element.repo = 'gerrit';
+              return element._getRepoBranchesSuggestions('');
+            })
+            .then(() => {
+              assert.isTrue(element.$.restAPI.getRepoBranches.called);
+              done();
+            });
+      });
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth.js
index 1562e3e..2978193 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth.js
@@ -39,6 +39,10 @@
       this._last_auth_check_time = Date.now();
     }
 
+    get baseUrl() {
+      return Gerrit.BaseUrlBehavior.getBaseUrl();
+    }
+
     /**
      * Returns if user is authed or not.
      *
@@ -49,7 +53,7 @@
         (Date.now() - this._last_auth_check_time > MAX_AUTH_CHECK_WAIT_TIME_MS)
       ) {
         // Refetch after last check expired
-        this._authCheckPromise = fetch('/auth-check');
+        this._authCheckPromise = fetch(`${this.baseUrl}/auth-check`);
         this._last_auth_check_time = Date.now();
       }
 
@@ -209,7 +213,7 @@
 
       if (accessToken) {
         params.push(`access_token=${accessToken}`);
-        const baseUrl = Gerrit.BaseUrlBehavior.getBaseUrl();
+        const baseUrl = this.baseUrl;
         const pathname = baseUrl ?
           url.substring(url.indexOf(baseUrl) + baseUrl.length) : url;
         if (!pathname.startsWith('/a/')) {
diff --git a/polygerrit-ui/app/scripts/util.js b/polygerrit-ui/app/scripts/util.js
index 672c43f..565b9b3 100644
--- a/polygerrit-ui/app/scripts/util.js
+++ b/polygerrit-ui/app/scripts/util.js
@@ -105,7 +105,7 @@
    */
   util.querySelector = (el, selector) => {
     let nodes = [el];
-    let element = null;
+    let result = null;
     while (nodes.length) {
       const node = nodes.pop();
 
@@ -113,19 +113,49 @@
       if (!node || !node.querySelector) continue;
 
       // Try find it with native querySelector directly
-      element = node.querySelector(selector);
+      result = node.querySelector(selector);
 
-      if (element) {
+      if (result) {
         break;
-      } else if (node.shadowRoot) {
-        // If shadowHost detected, add the host and its children
-        nodes = nodes.concat(Array.from(node.children));
-        nodes.push(node.shadowRoot);
-      } else {
-        nodes = nodes.concat(Array.from(node.children));
       }
+
+      // Add all nodes with shadowRoot and loop through
+      const allShadowNodes = [...node.querySelectorAll('*')]
+          .filter(child => !!child.shadowRoot)
+          .map(child => child.shadowRoot);
+      nodes = nodes.concat(allShadowNodes);
     }
-    return element;
+    return result;
+  };
+
+  /**
+   * Query selector all dom elements matching with certain selector.
+   *
+   * This is shadow DOM compatible, but only works when selector is within
+   * one shadow host, won't work if your selector is crossing
+   * multiple shadow hosts.
+   *
+   * Note: this can be very expensive, only use when have to.
+   */
+  util.querySelectorAll = (el, selector) => {
+    let nodes = [el];
+    const results = new Set();
+    while (nodes.length) {
+      const node = nodes.pop();
+
+      if (!node || !node.querySelectorAll) continue;
+
+      // Try find all from regular children
+      [...node.querySelectorAll(selector)]
+          .forEach(el => results.add(el));
+
+      // Add all nodes with shadowRoot and loop through
+      const allShadowNodes = [...node.querySelectorAll('*')]
+          .filter(child => !!child.shadowRoot)
+          .map(child => child.shadowRoot);
+      nodes = nodes.concat(allShadowNodes);
+    }
+    return [...results];
   };
 
   window.util = util;
diff --git a/polygerrit-ui/app/styles/themes/app-theme.html b/polygerrit-ui/app/styles/themes/app-theme.html
index 0dec63f..369af2b 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.html
+++ b/polygerrit-ui/app/styles/themes/app-theme.html
@@ -59,17 +59,15 @@
   --view-background-color: var(--background-color-primary);
   /* unique background colors */
   --assignee-highlight-color: #fcfad6;
+  --comment-background-color: #fcfad6;
+  --robot-comment-background-color: #e8f0fe;
   --edit-mode-background-color: #ebf5fb;
   --emphasis-color: #fff9c4;
   --hover-background-color: rgba(161, 194, 250, 0.2);
   --primary-button-background-color: #2a66d9;
   --selection-background-color: rgba(161, 194, 250, 0.1);
   --tooltip-background-color: #333;
-  /* comment background colors */
-  --comment-background-color: #fef7f0;
-  --robot-comment-background-color: #e8f0fe;
-  --unresolved-comment-background-color: #e8eaed;
-  /* vote background colors */
+  --unresolved-comment-background-color: #fcfaa6;
   --vote-color-approved: #9fcc6b;
   --vote-color-disliked: #f7c4cb;
   --vote-color-neutral: #ebf5fb;
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.html b/polygerrit-ui/app/styles/themes/dark-theme.html
index 57ceb32..80243dc 100644
--- a/polygerrit-ui/app/styles/themes/dark-theme.html
+++ b/polygerrit-ui/app/styles/themes/dark-theme.html
@@ -49,17 +49,15 @@
       /*   empty, because inheriting from app-theme is just fine
       /* unique background colors */
       --assignee-highlight-color: #3a361c;
+      --comment-background-color: #0b162b;
+      --robot-comment-background-color: rgba(232, 234, 237, 0.08);
       --edit-mode-background-color: #5c0a36;
       --emphasis-color: #383f4a;
       --hover-background-color: rgba(161, 194, 250, 0.2);
       --primary-button-background-color: var(--link-color);
       --selection-background-color: rgba(161, 194, 250, 0.1);
       --tooltip-background-color: #111;
-      /* comment background colors */
-      --comment-background-color: #303134;
-      --robot-comment-background-color: #40454e;
-      --unresolved-comment-background-color: #4b463a;
-      /* vote background colors */
+      --unresolved-comment-background-color: #385a9a;
       --vote-color-approved: #7fb66b;
       --vote-color-disliked: #bf6874;
       --vote-color-neutral: #597280;
diff --git a/resources/com/google/gerrit/pgm/init/libraries.config b/resources/com/google/gerrit/pgm/init/libraries.config
deleted file mode 100644
index 3d3545b..0000000
--- a/resources/com/google/gerrit/pgm/init/libraries.config
+++ /dev/null
@@ -1,50 +0,0 @@
-# Copyright (C) 2009 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-[library "mysqlDriver"]
-  name = MySQL Connector/J 5.1.43
-  url = https://repo1.maven.org/maven2/mysql/mysql-connector-java/5.1.43/mysql-connector-java-5.1.43.jar
-  sha1 = dee9103eec0d877f3a21c82d4d9e9f4fbd2d6e0a
-  remove = mysql-connector-java-.*[.]jar
-
-[library "mariadbDriver"]
-  name = MariaDB Connector/J 2.3.0
-  url = https://repo1.maven.org/maven2/org/mariadb/jdbc/mariadb-java-client/2.3.0/mariadb-java-client-2.3.0.jar
-  sha1 = c2b1a6002a169757d0649449288e9b3b776af76b
-  remove = mariadb-java-client-.*[.]jar
-
-[library "oracleDriver"]
-  name = Oracle JDBC driver 11g Release 2 (11.2.0)
-  url = file:///u01/app/oracle/product/11.2.0/xe/jdbc/lib/ojdbc6.jar
-  sha1 = 2f89cd9176772c3a6c261ce6a8e3d0d4425f5679
-  remove = ojdbc6.jar
-
-[library "db2Driver"]
-  name = DB2 Type 4 JDBC driver (10.5)
-  url = file:///opt/ibm/db2/V10.5/java/db2jcc4.jar
-  sha1 = 9344d4fd41d6511f2d1d1deb7759056495b3a39b
-  needs = db2DriverLicense
-  remove = db2jcc4.jar
-
-# Omit SHA-1 for license JAR as it's not stable and depends on the product
-# the customer has purchased.
-[library "db2DriverLicense"]
-  name = DB2 Type 4 JDBC driver license (10.5)
-  url = file:///opt/ibm/db2/V10.5/java/db2jcc_license_cu.jar
-  remove = db2jcc_license_cu.jar
-
-[library "hanaDriver"]
-  name = HANA JDBC driver
-  url = file:///usr/sap/hdbclient/ngdbc.jar
-  remove = ngdbc.jar
diff --git a/tools/bzl/maven.bzl b/tools/bzl/maven.bzl
deleted file mode 100644
index 36e3084e..0000000
--- a/tools/bzl/maven.bzl
+++ /dev/null
@@ -1,34 +0,0 @@
-# Copyright (C) 2016 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Merge maven files
-
-load("@rules_java//java:defs.bzl", "java_import")
-
-def cmd(jars):
-    return ("$(location //tools:merge_jars) $@ " +
-            " ".join(["$(location %s)" % j for j in jars]))
-
-def merge_maven_jars(name, srcs, **kwargs):
-    native.genrule(
-        name = "%s__merged_bin" % name,
-        cmd = cmd(srcs),
-        tools = srcs + ["//tools:merge_jars"],
-        outs = ["%s__merged.jar" % name],
-    )
-    java_import(
-        name = name,
-        jars = [":%s__merged_bin" % name],
-        **kwargs
-    )
diff --git a/tools/eclipse/project.py b/tools/eclipse/project.py
index 7f0e88b..9915a6e 100755
--- a/tools/eclipse/project.py
+++ b/tools/eclipse/project.py
@@ -239,7 +239,7 @@
             if p.endswith('libquery_parser.jar') or \
                p.endswith('libgerrit-prolog-common.jar') or \
                p.endswith('com_google_protobuf/libprotobuf_java.jar') or \
-               p.endswith('lucene-core-and-backward-codecs__merged.jar'):
+               p.endswith('lucene-core-and-backward-codecs-merged_deploy.jar'):
                 lib.add(p)
             if proto_library.match(p) :
                 proto.add(p)