Merge "Add missing JavaDoc to validators package"
diff --git a/WORKSPACE b/WORKSPACE
index 0804bec..e7317af 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -919,48 +919,48 @@
     sha1 = "7e060dd5b19431e6d198e91ff670644372f60fbd",
 )
 
-JETTY_VERS = "9.4.18.v20190429"
+JETTY_VERS = "9.4.24.v20191120"
 
 maven_jar(
     name = "jetty-servlet",
     artifact = "org.eclipse.jetty:jetty-servlet:" + JETTY_VERS,
-    sha1 = "290f7a88f351950d51ebc9fb4a794752c62d7de5",
+    sha1 = "ca1803fde51b795c0a8346ca8bc6277d9d04d01d",
 )
 
 maven_jar(
     name = "jetty-security",
     artifact = "org.eclipse.jetty:jetty-security:" + JETTY_VERS,
-    sha1 = "01aceff3608ca1b223bfd275a497797cfe675ef4",
+    sha1 = "9fa640d36c088cf55843900043d28aef830ade4d",
 )
 
 maven_jar(
     name = "jetty-server",
     artifact = "org.eclipse.jetty:jetty-server:" + JETTY_VERS,
-    sha1 = "b76ef50e04635f11d4d43bc6ccb7c4482a8384f0",
+    sha1 = "7885cc3d5d7701a444acada7ab97f89846514875",
 )
 
 maven_jar(
     name = "jetty-jmx",
     artifact = "org.eclipse.jetty:jetty-jmx:" + JETTY_VERS,
-    sha1 = "f4c2654db1a55f0780acdfcee8bb98550f56ca70",
+    sha1 = "22be18a055850a6cf3b0efd56c789c3929c87e98",
 )
 
 maven_jar(
     name = "jetty-http",
     artifact = "org.eclipse.jetty:jetty-http:" + JETTY_VERS,
-    sha1 = "c2e73db2db5c369326b717da71b6587b3da11e0e",
+    sha1 = "d3f0b0fb016ef8d35ffb199d928ffbcbfa121c86",
 )
 
 maven_jar(
     name = "jetty-io",
     artifact = "org.eclipse.jetty:jetty-io:" + JETTY_VERS,
-    sha1 = "844af5efe58ab23fd0166a796efef123f4cb06b0",
+    sha1 = "dcb6d4d505ef74898e3a64a38c40195c01e97119",
 )
 
 maven_jar(
     name = "jetty-util",
     artifact = "org.eclipse.jetty:jetty-util:" + JETTY_VERS,
-    sha1 = "13e6148bfda7ae511f69ae7e5e3ea898bc9b0e33",
+    sha1 = "3095acb088f4ff9e3fd9aedf98db73e3c18ea849",
 )
 
 maven_jar(
diff --git a/java/com/google/gerrit/common/data/GarbageCollectionResult.java b/java/com/google/gerrit/common/data/GarbageCollectionResult.java
index 5ed0158..5e3601e 100644
--- a/java/com/google/gerrit/common/data/GarbageCollectionResult.java
+++ b/java/com/google/gerrit/common/data/GarbageCollectionResult.java
@@ -18,6 +18,7 @@
 import java.util.ArrayList;
 import java.util.List;
 
+/** A list of errors occurred during GC. */
 public class GarbageCollectionResult {
   protected List<Error> errors;
 
diff --git a/java/com/google/gerrit/common/data/GlobalCapability.java b/java/com/google/gerrit/common/data/GlobalCapability.java
index fbe1deb..10a66cc 100644
--- a/java/com/google/gerrit/common/data/GlobalCapability.java
+++ b/java/com/google/gerrit/common/data/GlobalCapability.java
@@ -20,7 +20,12 @@
 import java.util.Collections;
 import java.util.List;
 
-/** Server wide capabilities. Represented as {@link Permission} objects. */
+/**
+ * Server wide capabilities. Represented as {@link Permission} objects.
+ *
+ * <p>Contrary to {@link Permission}, global capabilities do not need a resource to check
+ * permissions on.
+ */
 public class GlobalCapability {
   /** Ability to view code review metadata refs in repositories. */
   public static final String ACCESS_DATABASE = "accessDatabase";
diff --git a/java/com/google/gerrit/extensions/api/projects/BanCommitInput.java b/java/com/google/gerrit/extensions/api/projects/BanCommitInput.java
index b0f674f..b24eca0 100644
--- a/java/com/google/gerrit/extensions/api/projects/BanCommitInput.java
+++ b/java/com/google/gerrit/extensions/api/projects/BanCommitInput.java
@@ -17,6 +17,7 @@
 import com.google.common.collect.Lists;
 import java.util.List;
 
+/** Commits that will forbidden to be uploaded. */
 public class BanCommitInput {
   public List<String> commits;
   public String reason;
diff --git a/java/com/google/gerrit/server/account/Realm.java b/java/com/google/gerrit/server/account/Realm.java
index 798b4e8..d56ed07 100644
--- a/java/com/google/gerrit/server/account/Realm.java
+++ b/java/com/google/gerrit/server/account/Realm.java
@@ -24,6 +24,12 @@
 import javax.naming.NamingException;
 import javax.security.auth.login.LoginException;
 
+/**
+ * Interface between Gerrit and an account system.
+ *
+ * <p>This interface provides the glue layer between the Gerrit and external account/authentication
+ * systems (eg. LDAP, OpenID).
+ */
 public interface Realm {
   /** Can the end-user modify this field of their own account? */
   boolean allowsEdit(AccountFieldName field);
diff --git a/java/com/google/gerrit/server/account/SetInactiveFlag.java b/java/com/google/gerrit/server/account/SetInactiveFlag.java
index a6c5d5c..32ed694 100644
--- a/java/com/google/gerrit/server/account/SetInactiveFlag.java
+++ b/java/com/google/gerrit/server/account/SetInactiveFlag.java
@@ -32,6 +32,7 @@
 import java.util.concurrent.atomic.AtomicReference;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
+/** Toggler for account active state. */
 @Singleton
 public class SetInactiveFlag {
   private final PluginSetContext<AccountActivationValidationListener>
diff --git a/java/com/google/gerrit/server/git/BanCommit.java b/java/com/google/gerrit/server/git/BanCommit.java
index 4473ab7..242c11b 100644
--- a/java/com/google/gerrit/server/git/BanCommit.java
+++ b/java/com/google/gerrit/server/git/BanCommit.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.git.LockFailureException;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
@@ -47,6 +46,12 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 
+/**
+ * Logic for banning commits from being uploaded.
+ *
+ * <p>Gerrit has a per-project list of commits that are forbidden to be pushed. This class reads and
+ * writes the banned commits list in {@code refs/meta/reject-commits}.
+ */
 @Singleton
 public class BanCommit {
   /**
@@ -91,9 +96,14 @@
     this.tz = gerritIdent.getTimeZone();
   }
 
+  /**
+   * Bans a list of commits from the given project.
+   *
+   * <p>The user must be specified, so it can be checked for the {@code BAN_COMMIT} permission.
+   */
   public BanCommitResult ban(
       Project.NameKey project, CurrentUser user, List<ObjectId> commitsToBan, String reason)
-      throws AuthException, LockFailureException, IOException, PermissionBackendException {
+      throws AuthException, IOException, PermissionBackendException {
     permissionBackend.user(user).project(project).check(ProjectPermission.BAN_COMMIT);
 
     final BanCommitResult result = new BanCommitResult();
diff --git a/java/com/google/gerrit/server/git/BanCommitResult.java b/java/com/google/gerrit/server/git/BanCommitResult.java
index 9fadae2..c78123e 100644
--- a/java/com/google/gerrit/server/git/BanCommitResult.java
+++ b/java/com/google/gerrit/server/git/BanCommitResult.java
@@ -18,6 +18,7 @@
 import java.util.List;
 import org.eclipse.jgit.lib.ObjectId;
 
+/** The outcome of the {@link com.google.gerrit.server.git.BanCommit} operation. */
 public class BanCommitResult {
   private final List<ObjectId> newlyBannedCommits = new ArrayList<>(4);
   private final List<ObjectId> alreadyBannedCommits = new ArrayList<>(4);
diff --git a/java/com/google/gerrit/server/git/BranchOrderSection.java b/java/com/google/gerrit/server/git/BranchOrderSection.java
index 4c77b61..0266655 100644
--- a/java/com/google/gerrit/server/git/BranchOrderSection.java
+++ b/java/com/google/gerrit/server/git/BranchOrderSection.java
@@ -18,6 +18,13 @@
 import com.google.gerrit.entities.RefNames;
 import java.util.List;
 
+/**
+ * An ordering of branches by stability.
+ *
+ * <p>The REST API supports automatically checking if changes on development branches can be merged
+ * into stable branches. This is configured by the {@code branchOrder.branch} project setting. This
+ * class represents the ordered list of branches, by increasing stability.
+ */
 public class BranchOrderSection {
 
   /**
diff --git a/java/com/google/gerrit/server/git/ChangeReportFormatter.java b/java/com/google/gerrit/server/git/ChangeReportFormatter.java
index f897a1d..e0efaef 100644
--- a/java/com/google/gerrit/server/git/ChangeReportFormatter.java
+++ b/java/com/google/gerrit/server/git/ChangeReportFormatter.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Change;
 
+/** Formatter for git command-line progress messages. */
 public interface ChangeReportFormatter {
   @AutoValue
   public abstract static class Input {
diff --git a/java/com/google/gerrit/server/git/DefaultChangeReportFormatter.java b/java/com/google/gerrit/server/git/DefaultChangeReportFormatter.java
index 5866c57..4f6094e 100644
--- a/java/com/google/gerrit/server/git/DefaultChangeReportFormatter.java
+++ b/java/com/google/gerrit/server/git/DefaultChangeReportFormatter.java
@@ -22,7 +22,7 @@
 import com.google.inject.Inject;
 import java.util.Optional;
 
-/** Print a change description for use in git command-line progress. */
+/** Default formatter for change descriptions for use in git command-line progress. */
 public class DefaultChangeReportFormatter implements ChangeReportFormatter {
   private static final int SUBJECT_MAX_LENGTH = 80;
   private static final String SUBJECT_CROP_APPENDIX = "...";
diff --git a/java/com/google/gerrit/server/git/DefaultQueueOp.java b/java/com/google/gerrit/server/git/DefaultQueueOp.java
index b30acfa..9fb1a9b 100644
--- a/java/com/google/gerrit/server/git/DefaultQueueOp.java
+++ b/java/com/google/gerrit/server/git/DefaultQueueOp.java
@@ -17,6 +17,10 @@
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
 
+/**
+ * Wrapper class so a Runnable can schedule itself onto the Gerrit Workqueue. Subclasses must
+ * implement the {@code run} method.
+ */
 public abstract class DefaultQueueOp implements Runnable {
   private final WorkQueue workQueue;
 
diff --git a/java/com/google/gerrit/server/git/GarbageCollection.java b/java/com/google/gerrit/server/git/GarbageCollection.java
index 090d439..9b52f48 100644
--- a/java/com/google/gerrit/server/git/GarbageCollection.java
+++ b/java/com/google/gerrit/server/git/GarbageCollection.java
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GarbageCollectionResult;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.events.GarbageCollectorListener;
@@ -37,6 +38,7 @@
 import org.eclipse.jgit.lib.TextProgressMonitor;
 import org.eclipse.jgit.storage.pack.PackConfig;
 
+/** Serial execution of GC on a list of repositories. */
 public class GarbageCollection {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
@@ -69,8 +71,9 @@
     return run(projectNames, gcConfig.isAggressive(), writer);
   }
 
+  /** Runs GC on the given projects, serially. Progress is written to writer if non-null. */
   public GarbageCollectionResult run(
-      List<Project.NameKey> projectNames, boolean aggressive, PrintWriter writer) {
+      List<Project.NameKey> projectNames, boolean aggressive, @Nullable PrintWriter writer) {
     GarbageCollectionResult result = new GarbageCollectionResult();
     Set<Project.NameKey> projectsToGc = gcQueue.addAll(projectNames);
     for (Project.NameKey projectName :
diff --git a/java/com/google/gerrit/server/git/GarbageCollectionQueue.java b/java/com/google/gerrit/server/git/GarbageCollectionQueue.java
index e3a923b..5df9ab5 100644
--- a/java/com/google/gerrit/server/git/GarbageCollectionQueue.java
+++ b/java/com/google/gerrit/server/git/GarbageCollectionQueue.java
@@ -21,6 +21,7 @@
 import java.util.HashSet;
 import java.util.Set;
 
+/** A thread-safe list of projects scheduled for GC. */
 @Singleton
 public class GarbageCollectionQueue {
   private final Set<Project.NameKey> projectsScheduledForGc = new HashSet<>();
diff --git a/java/com/google/gerrit/server/git/MergedByPushOp.java b/java/com/google/gerrit/server/git/MergedByPushOp.java
index 01d5380..b272cba 100644
--- a/java/com/google/gerrit/server/git/MergedByPushOp.java
+++ b/java/com/google/gerrit/server/git/MergedByPushOp.java
@@ -45,6 +45,12 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 
+/**
+ * Operation to close a change on push.
+ *
+ * <p>When we find a change corresponding to a commit that is pushed to a branch directly, we close
+ * the change. This class marks the change as merged, and sends out the email notification.
+ */
 public class MergedByPushOp implements BatchUpdateOp {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 5f6d4fd..e03e0fc 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -1428,7 +1428,6 @@
     private final ProjectState projectState;
     private final boolean defaultPublishComments;
 
-    boolean deprecatedTopicSeen;
     final ReceiveCommand cmd;
     final LabelTypes labelTypes;
     /**
@@ -1590,7 +1589,6 @@
         IdentifiedUser user, ProjectState projectState, ReceiveCommand cmd, LabelTypes labelTypes) {
       this.user = user;
       this.projectState = projectState;
-      this.deprecatedTopicSeen = false;
       this.cmd = cmd;
       this.labelTypes = labelTypes;
       GeneralPreferencesInfo prefs = user.state().generalPreferences();
@@ -1654,9 +1652,7 @@
     /**
      * returns the destination ref of the magic branch, and populates options in the cmdLineParser.
      */
-    String parse(
-        Repository repo, ReceivePackRefCache refCache, ListMultimap<String, String> pushOptions)
-        throws CmdLineException, IOException {
+    String parse(ListMultimap<String, String> pushOptions) throws CmdLineException {
       String ref = RefNames.fullName(MagicBranch.getDestBranchName(cmd.getRefName()));
 
       ListMultimap<String, String> options = LinkedListMultimap.create(pushOptions);
@@ -1678,28 +1674,7 @@
       if (!options.isEmpty()) {
         cmdLineParser.parseOptionMap(options);
       }
-
-      // We accept refs/for/BRANCHNAME/TOPIC. Since we don't know
-      // for sure where the branch ends and the topic starts, look
-      // backward for a split that works. This behavior is deprecated.
-      String head = readHEAD(repo);
-      int split = ref.length();
-      for (; ; ) {
-        String name = ref.substring(0, split);
-        if (refCache.exactRef(name) != null || name.equals(head)) {
-          break;
-        }
-
-        split = name.lastIndexOf('/', split - 1);
-        if (split <= Constants.R_REFS.length()) {
-          return ref;
-        }
-      }
-      if (split < ref.length()) {
-        topic = Strings.emptyToNull(ref.substring(split + 1));
-        deprecatedTopicSeen = true;
-      }
-      return ref.substring(0, split);
+      return ref;
     }
 
     public boolean shouldSetWorkInProgressOnNewChanges() {
@@ -1754,7 +1729,7 @@
       magicBranch.cmdLineParser = optionParserFactory.create(magicBranch);
 
       try {
-        ref = magicBranch.parse(repo, receivePackRefCache, pushOptions);
+        ref = magicBranch.parse(pushOptions);
       } catch (CmdLineException e) {
         if (!magicBranch.cmdLineParser.wasHelpRequestedByOption()) {
           logger.atFine().log("Invalid branch syntax");
@@ -1929,13 +1904,6 @@
         return;
       }
 
-      if (magicBranch.deprecatedTopicSeen) {
-        messages.add(
-            new ValidationMessage(
-                "WARNING: deprecated topic syntax. Use -o topic=TOPIC instead", false));
-        logger.atInfo().log("deprecated topic push seen for project %s", project.getName());
-      }
-
       if (validateConnected(magicBranch.cmd, magicBranch.dest, tip)) {
         this.magicBranch = magicBranch;
         this.resultChangeIds.setMagicPush(true);
diff --git a/java/com/google/gerrit/server/mail/send/DeleteKeySender.java b/java/com/google/gerrit/server/mail/send/DeleteKeySender.java
index c9bb1e4..defacd84 100644
--- a/java/com/google/gerrit/server/mail/send/DeleteKeySender.java
+++ b/java/com/google/gerrit/server/mail/send/DeleteKeySender.java
@@ -25,6 +25,9 @@
 import java.util.Collections;
 import java.util.List;
 
+/**
+ * Sender that informs a user by email about the removal of an SSH or GPG key from their account.
+ */
 public class DeleteKeySender extends OutgoingEmail {
   public interface Factory {
     DeleteKeySender create(IdentifiedUser user, AccountSshKey sshKey);
diff --git a/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java b/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java
index 2db2d6d..f24ad7a 100644
--- a/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java
+++ b/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java
@@ -21,6 +21,7 @@
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 
+/** Sender that informs a user by email that the HTTP password of their account was updated. */
 public class HttpPasswordUpdateSender extends OutgoingEmail {
   public interface Factory {
     HttpPasswordUpdateSender create(IdentifiedUser user, String operation);
diff --git a/java/com/google/gerrit/server/project/DefaultProjectNameLockManager.java b/java/com/google/gerrit/server/project/DefaultProjectNameLockManager.java
index 000fb09..ab347e5 100644
--- a/java/com/google/gerrit/server/project/DefaultProjectNameLockManager.java
+++ b/java/com/google/gerrit/server/project/DefaultProjectNameLockManager.java
@@ -21,6 +21,7 @@
 import com.google.inject.Singleton;
 import java.util.concurrent.locks.Lock;
 
+/** In-memory lock for project names. */
 @Singleton
 public class DefaultProjectNameLockManager implements ProjectNameLockManager {
 
diff --git a/java/com/google/gerrit/server/project/ProjectCreator.java b/java/com/google/gerrit/server/project/ProjectCreator.java
index 04d0859..ba44142 100644
--- a/java/com/google/gerrit/server/project/ProjectCreator.java
+++ b/java/com/google/gerrit/server/project/ProjectCreator.java
@@ -55,6 +55,12 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.transport.ReceiveCommand;
 
+/**
+ * Business logic for creating projects.
+ *
+ * <p>This creates the repository, the underlying configuration in {@code refs/meta/config} and
+ * initializes a first commit if necessary.
+ */
 public class ProjectCreator {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
diff --git a/java/com/google/gerrit/server/project/ProjectJson.java b/java/com/google/gerrit/server/project/ProjectJson.java
index cd67fbd..f2254d6 100644
--- a/java/com/google/gerrit/server/project/ProjectJson.java
+++ b/java/com/google/gerrit/server/project/ProjectJson.java
@@ -31,6 +31,7 @@
 import com.google.inject.Singleton;
 import java.util.HashMap;
 
+/** Collection of routines to populate {@link ProjectInfo}. */
 @Singleton
 public class ProjectJson {
 
diff --git a/java/com/google/gerrit/server/project/ProjectNameLockManager.java b/java/com/google/gerrit/server/project/ProjectNameLockManager.java
index 72036a7..f67dd04 100644
--- a/java/com/google/gerrit/server/project/ProjectNameLockManager.java
+++ b/java/com/google/gerrit/server/project/ProjectNameLockManager.java
@@ -17,6 +17,14 @@
 import com.google.gerrit.entities.Project;
 import java.util.concurrent.locks.Lock;
 
+/**
+ * A per-repo lock mechanism.
+ *
+ * <p>This ensures that project creation (repo creation, config creation, first commit) is atomic,
+ * and can be used to separate creation and deletion in the delete-project plugin.
+ *
+ * <p>This is an interface because distributed setup may need something beyond an in-memory lock.
+ */
 public interface ProjectNameLockManager {
   public Lock getLock(Project.NameKey name);
 }
diff --git a/java/com/google/gerrit/server/restapi/project/BanCommit.java b/java/com/google/gerrit/server/restapi/project/BanCommit.java
index a20d462..eb5473d 100644
--- a/java/com/google/gerrit/server/restapi/project/BanCommit.java
+++ b/java/com/google/gerrit/server/restapi/project/BanCommit.java
@@ -31,6 +31,7 @@
 import java.util.List;
 import org.eclipse.jgit.lib.ObjectId;
 
+/** The REST endpoint that marks commits as banned in a project. */
 @Singleton
 public class BanCommit implements RestModifyView<ProjectResource, BanCommitInput> {
   private final com.google.gerrit.server.git.BanCommit banCommit;
diff --git a/java/com/google/gerrit/server/restapi/project/GarbageCollect.java b/java/com/google/gerrit/server/restapi/project/GarbageCollect.java
index 25a2c90..c5423e6 100644
--- a/java/com/google/gerrit/server/restapi/project/GarbageCollect.java
+++ b/java/com/google/gerrit/server/restapi/project/GarbageCollect.java
@@ -43,6 +43,7 @@
 import java.util.Collections;
 import java.util.Optional;
 
+/** REST endpoint that executes GC on a project. */
 @RequiresCapability(GlobalCapability.RUN_GC)
 @Singleton
 public class GarbageCollect
diff --git a/java/com/google/gerrit/server/update/RetryableAction.java b/java/com/google/gerrit/server/update/RetryableAction.java
index 9a2807a..75ebeb37 100644
--- a/java/com/google/gerrit/server/update/RetryableAction.java
+++ b/java/com/google/gerrit/server/update/RetryableAction.java
@@ -37,6 +37,17 @@
  * retry via {@link #retryOn(Predicate)}.
  */
 public class RetryableAction<T> {
+  /**
+   * Type of an retryable action.
+   *
+   * <p>The action type is used for two purposes:
+   *
+   * <ul>
+   *   <li>to determine the default timeout for executing the action (see {@link
+   *       RetryHelper#getDefaultTimeout(String)})
+   *   <li>as bucket for all retry metrics (see {@link RetryHelper.Metrics})
+   * </ul>
+   */
   public enum ActionType {
     ACCOUNT_UPDATE,
     CHANGE_UPDATE,
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 60cf4f1..4258009 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -485,15 +485,9 @@
 
   @Test
   public void pushForMasterWithTopic() throws Exception {
-    // specify topic in ref
     String topic = "my/topic";
-    PushOneCommit.Result r = pushTo("refs/for/master/" + topic);
-    r.assertOkStatus();
-    r.assertChange(Change.Status.NEW, topic);
-    r.assertMessage("deprecated topic syntax");
-
     // specify topic as option
-    r = pushTo("refs/for/master%topic=" + topic);
+    PushOneCommit.Result r = pushTo("refs/for/master%topic=" + topic);
     r.assertOkStatus();
     r.assertChange(Change.Status.NEW, topic);
   }
@@ -514,14 +508,7 @@
   }
 
   @Test
-  public void pushForMasterWithTopicInRefExceedLimitFails() throws Exception {
-    String topic = Stream.generate(() -> "t").limit(2049).collect(joining());
-    PushOneCommit.Result r = pushTo("refs/for/master/" + topic);
-    r.assertErrorStatus("topic length exceeds the limit (2048)");
-  }
-
-  @Test
-  public void pushForMasterWithTopicAsOptionExceedLimitFails() throws Exception {
+  public void pushForMasterWithTopicExceedLimitFails() throws Exception {
     String topic = Stream.generate(() -> "t").limit(2049).collect(joining());
     PushOneCommit.Result r = pushTo("refs/for/master%topic=" + topic);
     r.assertErrorStatus("topic length exceeds the limit (2048)");
@@ -605,16 +592,16 @@
   public void pushForMasterWithCc() throws Exception {
     // cc one user
     String topic = "my/topic";
-    PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%cc=" + user.email());
+    PushOneCommit.Result r = pushTo("refs/for/master%topic=" + topic + ",cc=" + user.email());
     r.assertOkStatus();
     r.assertChange(Change.Status.NEW, topic, ImmutableList.of(), ImmutableList.of(user));
 
     // cc several users
     r =
         pushTo(
-            "refs/for/master/"
+            "refs/for/master%topic="
                 + topic
-                + "%cc="
+                + ",cc="
                 + admin.email()
                 + ",cc="
                 + user.email()
@@ -632,9 +619,9 @@
     String nonExistingEmail = "non.existing@example.com";
     r =
         pushTo(
-            "refs/for/master/"
+            "refs/for/master%topic="
                 + topic
-                + "%cc="
+                + ",cc="
                 + admin.email()
                 + ",cc="
                 + nonExistingEmail
@@ -684,7 +671,7 @@
   public void pushForMasterWithReviewer() throws Exception {
     // add one reviewer
     String topic = "my/topic";
-    PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%r=" + user.email());
+    PushOneCommit.Result r = pushTo("refs/for/master%topic=" + topic + ",r=" + user.email());
     r.assertOkStatus();
     r.assertChange(Change.Status.NEW, topic, user);
 
@@ -693,9 +680,9 @@
         accountCreator.create("another-user", "another.user@example.com", "Another User");
     r =
         pushTo(
-            "refs/for/master/"
+            "refs/for/master%topic="
                 + topic
-                + "%r="
+                + ",r="
                 + admin.email()
                 + ",r="
                 + user.email()
@@ -709,9 +696,9 @@
     String nonExistingEmail = "non.existing@example.com";
     r =
         pushTo(
-            "refs/for/master/"
+            "refs/for/master%topic="
                 + topic
-                + "%r="
+                + ",r="
                 + admin.email()
                 + ",r="
                 + nonExistingEmail
@@ -942,7 +929,7 @@
 
   @Test
   public void pushForMasterWithMessage() throws Exception {
-    PushOneCommit.Result r = pushTo("refs/for/master/%m=my_test_message");
+    PushOneCommit.Result r = pushTo("refs/for/master%m=my_test_message");
     r.assertOkStatus();
     r.assertChange(Change.Status.NEW, null);
     ChangeInfo ci = get(r.getChangeId(), MESSAGES, ALL_REVISIONS);
@@ -966,7 +953,7 @@
         pushFactory.create(admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
     // %2C is comma; the value below tests that percent decoding happens after splitting.
     // All three ways of representing space ("%20", "+", and "_" are also exercised.
-    PushOneCommit.Result r = push.to("refs/for/master/%m=my_test%20+_message%2Cm=");
+    PushOneCommit.Result r = push.to("refs/for/master%m=my_test%20+_message%2Cm=");
     r.assertOkStatus();
 
     push =
@@ -977,7 +964,7 @@
             "b.txt",
             "anotherContent",
             r.getChangeId());
-    r = push.to("refs/for/master/%m=new_test_message");
+    r = push.to("refs/for/master%m=new_test_message");
     r.assertOkStatus();
 
     ChangeInfo ci = get(r.getChangeId(), ALL_REVISIONS);
@@ -997,7 +984,7 @@
     // Exercise percent-encoding of UTF-8, underscores, and patterns reserved by git-rev-parse.
     PushOneCommit.Result r =
         pushTo(
-            "refs/for/master/%m="
+            "refs/for/master%m="
                 + "Punctu%2E%2e%2Eation%7E%2D%40%7Bu%7D%20%7C%20%28%E2%95%AF%C2%B0%E2%96%A1%C2%B0"
                 + "%EF%BC%89%E2%95%AF%EF%B8%B5%20%E2%94%BB%E2%94%81%E2%94%BB%20%5E%5F%5E");
     r.assertOkStatus();
@@ -1018,7 +1005,7 @@
 
   @Test
   public void pushForMasterWithInvalidPercentEncodedMessage() throws Exception {
-    PushOneCommit.Result r = pushTo("refs/for/master/%m=not_percent_decodable_%%oops%20");
+    PushOneCommit.Result r = pushTo("refs/for/master%m=not_percent_decodable_%%oops%20");
     r.assertOkStatus();
     r.assertChange(Change.Status.NEW, null);
     ChangeInfo ci = get(r.getChangeId(), MESSAGES, ALL_REVISIONS);
@@ -1036,7 +1023,7 @@
 
   @Test
   public void pushForMasterWithApprovals() throws Exception {
-    PushOneCommit.Result r = pushTo("refs/for/master/%l=Code-Review");
+    PushOneCommit.Result r = pushTo("refs/for/master%l=Code-Review");
     r.assertOkStatus();
     ChangeInfo ci = get(r.getChangeId(), DETAILED_LABELS, MESSAGES, DETAILED_ACCOUNTS);
     LabelInfo cr = ci.labels.get("Code-Review");
@@ -1054,7 +1041,7 @@
             "b.txt",
             "anotherContent",
             r.getChangeId());
-    r = push.to("refs/for/master/%l=Code-Review+2");
+    r = push.to("refs/for/master%l=Code-Review+2");
 
     ci = get(r.getChangeId(), DETAILED_LABELS, MESSAGES, DETAILED_ACCOUNTS);
     cr = ci.labels.get("Code-Review");
@@ -1075,7 +1062,7 @@
             "c.txt",
             "moreContent",
             r.getChangeId());
-    r = push.to("refs/for/master/%l=Code-Review+2");
+    r = push.to("refs/for/master%l=Code-Review+2");
     ci = get(r.getChangeId(), MESSAGES);
     assertThat(Iterables.getLast(ci.messages).message).isEqualTo("Uploaded patch set 3.");
   }
@@ -1093,7 +1080,7 @@
             "b.txt",
             "anotherContent",
             r.getChangeId());
-    r = push.to("refs/for/master/%l=Code-Review+2");
+    r = push.to("refs/for/master%l=Code-Review+2");
 
     ChangeInfo ci = get(r.getChangeId(), DETAILED_LABELS, MESSAGES, DETAILED_ACCOUNTS);
     LabelInfo cr = ci.labels.get("Code-Review");
@@ -1180,7 +1167,7 @@
             .create();
 
     // Push this commit as "Administrator" (requires Forge Committer Identity)
-    pushHead(testRepo, "refs/for/master/%l=Code-Review+1", false);
+    pushHead(testRepo, "refs/for/master%l=Code-Review+1", false);
 
     // Expected Code-Review votes:
     // 1. 0 from User (committer):
@@ -1228,7 +1215,7 @@
             .message(PushOneCommit.SUBJECT)
             .create();
 
-    pushHead(testRepo, "refs/for/master/%l=Code-Review+1,l=Custom-Label-1", false);
+    pushHead(testRepo, "refs/for/master%l=Code-Review+1,l=Custom-Label-1", false);
 
     ChangeInfo ci = get(GitUtil.getChangeId(testRepo, c).get(), DETAILED_LABELS, DETAILED_ACCOUNTS);
     LabelInfo cr = ci.labels.get("Code-Review");
@@ -1258,13 +1245,13 @@
 
   @Test
   public void pushForMasterWithApprovals_MissingLabel() throws Exception {
-    PushOneCommit.Result r = pushTo("refs/for/master/%l=Verify");
+    PushOneCommit.Result r = pushTo("refs/for/master%l=Verify");
     r.assertErrorStatus("label \"Verify\" is not a configured label");
   }
 
   @Test
   public void pushForMasterWithApprovals_ValueOutOfRange() throws Exception {
-    PushOneCommit.Result r = pushTo("refs/for/master/%l=Code-Review-3");
+    PushOneCommit.Result r = pushTo("refs/for/master%l=Code-Review-3");
     r.assertErrorStatus("label \"Code-Review\": -3 is not a valid value");
   }
 
@@ -1297,7 +1284,7 @@
             "b.txt",
             "anotherContent",
             r.getChangeId());
-    r = push.to("refs/for/master/%hashtag=" + hashtag2);
+    r = push.to("refs/for/master%hashtag=" + hashtag2);
     r.assertOkStatus();
     expected = ImmutableSet.of(hashtag1, hashtag2);
     hashtags = gApi.changes().id(r.getChangeId()).getHashtags();
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java b/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
index 01323a0..a0725c3 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
@@ -139,7 +139,7 @@
 
     String pushedRef = ref;
     if (!topic.isEmpty()) {
-      pushedRef += "/" + name(topic);
+      pushedRef += "%topic=" + name(topic);
     }
     String refspec = "HEAD:" + pushedRef;
 
diff --git a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
index 329723b7..0efc4f9 100644
--- a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
@@ -565,7 +565,7 @@
     // Create change as user.
     PushOneCommit push =
         pushFactory.create(user.newIdent(), repo2, "Change 2", "b.txt", "other content");
-    PushOneCommit.Result pushResult2 = push.to("refs/for/master/" + name(topic));
+    PushOneCommit.Result pushResult2 = push.to("refs/for/master%topic=" + name(topic));
     approve(pushResult2.getChangeId());
 
     // Submit the topic, 2 changes with the different author.
diff --git a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
index 283c95f..0715b7e 100644
--- a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
@@ -109,7 +109,7 @@
         .git()
         .push()
         .setRemote("origin")
-        .setRefSpecs(new RefSpec("HEAD:refs/for/master/" + name("topic-foo")))
+        .setRefSpecs(new RefSpec("HEAD:refs/for/master%topic=" + name("topic-foo")))
         .call();
 
     subRepo.reset(c.getId());
@@ -134,7 +134,7 @@
         .git()
         .push()
         .setRemote("origin")
-        .setRefSpecs(new RefSpec("HEAD:refs/for/master/" + name("topic-foo")))
+        .setRefSpecs(new RefSpec("HEAD:refs/for/master%topic=" + name("topic-foo")))
         .call();
 
     String id1 = getChangeId(subRepo, c1).get();
@@ -212,7 +212,7 @@
         .git()
         .push()
         .setRemote("origin")
-        .setRefSpecs(new RefSpec("HEAD:refs/for/master/" + name("topic-foo")))
+        .setRefSpecs(new RefSpec("HEAD:refs/for/master%topic=" + name("topic-foo")))
         .call();
 
     subRepo.reset(c.getId());
@@ -237,7 +237,7 @@
         .git()
         .push()
         .setRemote("origin")
-        .setRefSpecs(new RefSpec("HEAD:refs/for/master/" + name("topic-foo")))
+        .setRefSpecs(new RefSpec("HEAD:refs/for/master%topic=" + name("topic-foo")))
         .call();
 
     RevCommit c4 =
@@ -252,7 +252,7 @@
         .git()
         .push()
         .setRemote("origin")
-        .setRefSpecs(new RefSpec("HEAD:refs/for/master/" + name("topic-foo")))
+        .setRefSpecs(new RefSpec("HEAD:refs/for/master%topic=" + name("topic-foo")))
         .call();
 
     String id1 = getChangeId(subRepo, c1).get();
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index eff98b3..0c0a8ed 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -614,11 +614,11 @@
   @Test
   public void submitWithHiddenBranchInSameTopic() throws Throwable {
     assume().that(isSubmitWholeTopicEnabled()).isTrue();
-    PushOneCommit.Result visible = createChange("refs/for/master/" + name("topic"));
+    PushOneCommit.Result visible = createChange("refs/for/master%topic=" + name("topic"));
     Change.Id num = visible.getChange().getId();
 
     createBranch(BranchNameKey.create(project, "hidden"));
-    PushOneCommit.Result hidden = createChange("refs/for/hidden/" + name("topic"));
+    PushOneCommit.Result hidden = createChange("refs/for/hidden%topic=" + name("topic"));
     approve(hidden.getChangeId());
     projectOperations
         .project(project)
@@ -789,8 +789,8 @@
 
     // create and submit 2 changes with the same topic
     String topic = name("topic");
-    PushOneCommit.Result change1 = createChange("refs/for/master/" + topic);
-    PushOneCommit.Result change2 = createChange("refs/for/master/" + topic);
+    PushOneCommit.Result change1 = createChange("refs/for/master%topic=" + topic);
+    PushOneCommit.Result change2 = createChange("refs/for/master%topic=" + topic);
     approve(change1.getChangeId());
     submit(change2.getChangeId());
     assertMerged(change1.getChangeId());
@@ -938,7 +938,7 @@
     testRepo
         .git()
         .push()
-        .setRefSpecs(new RefSpec("refs/heads/stable:refs/for/stable/" + name("topic")))
+        .setRefSpecs(new RefSpec("refs/heads/stable:refs/for/stable%topic=" + name("topic")))
         .call();
 
     // Merge the fix into master.
@@ -955,7 +955,7 @@
     testRepo
         .git()
         .push()
-        .setRefSpecs(new RefSpec("refs/heads/master:refs/for/master/" + name("topic")))
+        .setRefSpecs(new RefSpec("refs/heads/master:refs/for/master%topic=" + name("topic")))
         .call();
 
     // Submit together.
@@ -1475,6 +1475,6 @@
   protected PushOneCommit.Result createChange(
       String subject, String fileName, String content, String topic) throws Throwable {
     PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo, subject, fileName, content);
-    return push.to("refs/for/master/" + name(topic));
+    return push.to("refs/for/master%topic=" + name(topic));
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
index a4fa84b..f77552d 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
@@ -102,11 +102,11 @@
     PushOneCommit.Result change1 =
         pushFactory
             .create(admin.newIdent(), testRepo, "Change 1", "a", "a")
-            .to("refs/for/master/" + name("topic"));
+            .to("refs/for/master%topic=" + name("topic"));
 
     PushOneCommit push2 = pushFactory.create(admin.newIdent(), testRepo, "Change 2", "b", "b");
     push2.noParents();
-    PushOneCommit.Result change2 = push2.to("refs/for/master/" + name("topic"));
+    PushOneCommit.Result change2 = push2.to("refs/for/master%topic=" + name("topic"));
     change2.assertOkStatus();
 
     approve(change1.getChangeId());
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
index 73f10e5..a63d60a 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
@@ -217,7 +217,7 @@
             "new.txt",
             "Conflicting line #2",
             ImmutableList.of(f.getCommit()),
-            "refs/for/master/" + name("topic1"));
+            "refs/for/master%topic=" + name("topic1"));
 
     PushOneCommit.Result h = createChange(project2, "H");
     PushOneCommit.Result i =
@@ -231,7 +231,7 @@
             "new.txt",
             "Sadly conflicting topic-wise",
             ImmutableList.of(i.getCommit(), j.getCommit()),
-            "refs/for/master/" + name("topic1"));
+            "refs/for/master%topic=" + name("topic1"));
 
     approve(h.getChangeId());
     approve(i.getChangeId());
@@ -253,7 +253,7 @@
             "new.txt",
             "Resolving conflicts again",
             ImmutableList.of(c.getCommit(), g.getCommit()),
-            "refs/for/master/" + name("topic1"));
+            "refs/for/master%topic=" + name("topic1"));
 
     approve(l.getChangeId());
     assertChangeSetMergeable(l.getChange(), true);
diff --git a/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java b/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
index 445f787..a97fb49 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
@@ -117,12 +117,12 @@
     // Create two independent commits and push.
     RevCommit c1_1 = commitBuilder().add("a.txt", "1").message("subject: 1").create();
     String id1 = getChangeId(c1_1);
-    pushHead(testRepo, "refs/for/master/" + name("connectingTopic"), false);
+    pushHead(testRepo, "refs/for/master%topic=" + name("connectingTopic"), false);
 
     testRepo.reset(initialHead);
     RevCommit c2_1 = commitBuilder().add("b.txt", "2").message("subject: 2").create();
     String id2 = getChangeId(c2_1);
-    pushHead(testRepo, "refs/for/master/" + name("connectingTopic"), false);
+    pushHead(testRepo, "refs/for/master%topic=" + name("connectingTopic"), false);
 
     if (isSubmitWholeTopicEnabled()) {
       assertSubmittedTogether(id1, id2, id1);
@@ -137,12 +137,12 @@
   public void anonymousWholeTopic() throws Exception {
     RevCommit initialHead = projectOperations.project(project).getHead("master");
     RevCommit a = commitBuilder().add("a", "1").message("change 1").create();
-    pushHead(testRepo, "refs/for/master/" + name("topic"), false);
+    pushHead(testRepo, "refs/for/master%topic=" + name("topic"), false);
     String id1 = getChangeId(a);
 
     testRepo.reset(initialHead);
     RevCommit b = commitBuilder().add("b", "1").message("change 2").create();
-    pushHead(testRepo, "refs/for/master/" + name("topic"), false);
+    pushHead(testRepo, "refs/for/master%topic=" + name("topic"), false);
     String id2 = getChangeId(b);
 
     requestScopeOperations.setApiUserAnonymous();
@@ -161,16 +161,16 @@
 
     RevCommit c1_1 = commitBuilder().add("a.txt", "1").message("subject: 1").create();
     String id1 = getChangeId(c1_1);
-    pushHead(testRepo, "refs/for/master/" + name("connectingTopic"), false);
+    pushHead(testRepo, "refs/for/master%topic=" + name("connectingTopic"), false);
 
     testRepo.reset(initialHead);
     RevCommit c2_1 = commitBuilder().add("b.txt", "2").message("subject: 2").create();
     String id2 = getChangeId(c2_1);
-    pushHead(testRepo, "refs/for/master/" + name("connectingTopic"), false);
+    pushHead(testRepo, "refs/for/master%topic=" + name("connectingTopic"), false);
 
     RevCommit c3_1 = commitBuilder().add("b.txt", "3").message("subject: 3").create();
     String id3 = getChangeId(c3_1);
-    pushHead(testRepo, "refs/for/master/" + name("unrelated-topic"), false);
+    pushHead(testRepo, "refs/for/master%topic=" + name("unrelated-topic"), false);
 
     if (isSubmitWholeTopicEnabled()) {
       assertSubmittedTogether(id1, id2, id1);
@@ -189,16 +189,16 @@
 
     RevCommit c1_1 = commitBuilder().add("a.txt", "1").message("subject: 1").create();
     String id1 = getChangeId(c1_1);
-    pushHead(testRepo, "refs/for/master/" + name("connectingTopic"), false);
+    pushHead(testRepo, "refs/for/master%topic=" + name("connectingTopic"), false);
 
     testRepo.reset(initialHead);
     RevCommit c2_1 = commitBuilder().add("b.txt", "2").message("subject: 2").create();
     String id2 = getChangeId(c2_1);
-    pushHead(testRepo, "refs/for/master/" + name("otherConnectingTopic"), false);
+    pushHead(testRepo, "refs/for/master%topic=" + name("otherConnectingTopic"), false);
 
     RevCommit c3_1 = commitBuilder().add("b.txt", "3").message("subject: 3").create();
     String id3 = getChangeId(c3_1);
-    pushHead(testRepo, "refs/for/master/" + name("connectingTopic"), false);
+    pushHead(testRepo, "refs/for/master%topic=" + name("connectingTopic"), false);
 
     RevCommit c4_1 = commitBuilder().add("b.txt", "4").message("subject: 4").create();
     String id4 = getChangeId(c4_1);
@@ -211,7 +211,7 @@
 
     RevCommit c6_1 = commitBuilder().add("c.txt", "6").message("subject: 6").create();
     String id6 = getChangeId(c6_1);
-    pushHead(testRepo, "refs/for/master/" + name("otherConnectingTopic"), false);
+    pushHead(testRepo, "refs/for/master%topic=" + name("otherConnectingTopic"), false);
 
     if (isSubmitWholeTopicEnabled()) {
       assertSubmittedTogether(id1, id6, id5, id3, id2, id1);
diff --git a/package.json b/package.json
index 95edf0b..2656f74 100644
--- a/package.json
+++ b/package.json
@@ -15,7 +15,7 @@
   "scripts": {
     "start": "polygerrit-ui/run-server.sh",
     "test": "WCT_HEADLESS_MODE=1 WCT_ARGS='--verbose -l chrome' ./polygerrit-ui/app/run_test.sh",
-    "eslint": "./node_modules/eslint/bin/eslint.js --ignore-pattern 'bower_components/' --ignore-pattern 'gr-linked-text' --ignore-pattern 'scripts/vendor' --ext .html,.js polygerrit-ui/app",
+    "eslint": "./node_modules/eslint/bin/eslint.js --ext .html,.js polygerrit-ui/app",
     "eslintfix": "npm run eslint -- --fix",
     "test-template": "./polygerrit-ui/app/run_template_test.sh",
     "polylint": "bazel test polygerrit-ui/app:polylint_test"
diff --git a/polygerrit-ui/app/lint_test.sh b/polygerrit-ui/app/lint_test.sh
index 35939ba..bcd94ba 100755
--- a/polygerrit-ui/app/lint_test.sh
+++ b/polygerrit-ui/app/lint_test.sh
@@ -19,4 +19,4 @@
     exit 1
 fi
 
-${eslint_bin} --ignore-pattern 'bower_components/' --ignore-pattern 'gr-linked-text' --ignore-pattern 'scripts/vendor' --ext .html,.js .
+${eslint_bin} --ext .html,.js .
diff --git a/tools/dev-hooks/pre-commit b/tools/dev-hooks/pre-commit
new file mode 100755
index 0000000..b77f382
--- /dev/null
+++ b/tools/dev-hooks/pre-commit
@@ -0,0 +1,47 @@
+#!/bin/sh
+#
+# Part of Gerrit Code Review (https://www.gerritcodereview.com/)
+#
+# Copyright (C) 2019 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.
+
+
+# To enable this hook:
+# - copy this file or content to ".git/hooks/pre-commit"
+# - (optional if you copied this file) make it executable: `chmod +x .git/hooks/pre-commit`
+
+set -ue
+
+# gitroot, default to .
+gitroot=$(git rev-parse --show-cdup)
+gitroot=${gitroot:-.};
+
+# eslint
+eslint=${gitroot}/node_modules/eslint/bin/eslint.js
+
+# Run eslint over changed frontend code
+CHANGED_UI_FILES=$(git diff --cached --name-only -- '*.js' '*.html' | grep 'polygerrit-ui') && true
+if [ "${CHANGED_UI_FILES}" ]; then
+  if $eslint --fix ${CHANGED_UI_FILES}; then
+    # Add again in case lint fix modified some files
+    git add ${CHANGED_UI_FILES}
+    exit 0
+  else
+    echo "Failed to fix all linter issues.";
+    exit 1
+  fi
+else
+  echo "No UI files changed"
+  exit 0
+fi
\ No newline at end of file