Merge "Fix related changes for excessive numbers of patch sets"
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index 47e2505..229c463 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -13,6 +13,13 @@
 * `build/label`: Version of Gerrit server software.
 * `events`: Triggered events.
 
+=== Actions
+
+* `action/retry_attempt_counts`: Distribution of number of attempts made
+by RetryHelper to execute an action (1 == single attempt, no retry)
+* `action/retry_timeout_count`: Number of action executions of RetryHelper
+that ultimately timed out
+
 === Process
 
 * `proc/birth_timestamp`: Time at which the Gerrit process started.
@@ -87,10 +94,6 @@
 
 * `batch_update/execute_change_ops`: BatchUpdate change update latency,
 excluding reindexing
-* `batch_update/retry_attempt_counts`: Distribution of number of attempts made
-by RetryHelper (1 == single attempt, no retry)
-* `batch_update/retry_timeout_count`: Number of executions of RetryHelper that
-ultimately timed out
 
 === NoteDb
 
diff --git a/java/com/google/gerrit/server/account/AccountsUpdate.java b/java/com/google/gerrit/server/account/AccountsUpdate.java
index 85550aa..ee3cf87f 100644
--- a/java/com/google/gerrit/server/account/AccountsUpdate.java
+++ b/java/com/google/gerrit/server/account/AccountsUpdate.java
@@ -38,6 +38,7 @@
 import com.google.gerrit.server.mail.send.OutgoingEmailValidator;
 import com.google.gerrit.server.update.RefUpdateUtil;
 import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryHelper.ActionType;
 import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -473,6 +474,7 @@
   private Account deleteAccount(Account.Id accountId)
       throws IOException, OrmException, ConfigInvalidException {
     return retryHelper.execute(
+        ActionType.ACCOUNT_UPDATE,
         () -> {
           deleteUserBranch(accountId);
           return null;
@@ -525,6 +527,7 @@
   private Account updateAccount(AccountUpdate accountUpdate)
       throws IOException, ConfigInvalidException, OrmException {
     return retryHelper.execute(
+        ActionType.ACCOUNT_UPDATE,
         () -> {
           try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
             UpdatedAccount updatedAccount = accountUpdate.update(allUsersRepo);
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdsUpdate.java b/java/com/google/gerrit/server/account/externalids/ExternalIdsUpdate.java
index 8a05a6c..028fd8d 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdsUpdate.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdsUpdate.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryHelper.ActionType;
 import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -427,6 +428,7 @@
   private void updateNoteMap(ExternalIdUpdater updater)
       throws IOException, ConfigInvalidException, OrmException {
     retryHelper.execute(
+        ActionType.ACCOUNT_UPDATE,
         () -> {
           try (Repository repo = repoManager.openRepository(allUsersName)) {
             ExternalIdNotes extIdNotes =
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 585f2e0..a058aec 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -147,6 +147,8 @@
 import com.google.gerrit.server.update.Context;
 import com.google.gerrit.server.update.RepoContext;
 import com.google.gerrit.server.update.RepoOnlyOp;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryHelper.ActionType;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.LabelVote;
 import com.google.gerrit.server.util.MagicBranch;
@@ -324,6 +326,7 @@
   private final ReceiveConfig receiveConfig;
   private final RefOperationValidators.Factory refValidatorsFactory;
   private final ReplaceOp.Factory replaceOpFactory;
+  private final RetryHelper retryHelper;
   private final RequestScopePropagator requestScopePropagator;
   private final ReviewDb db;
   private final Sequences seq;
@@ -410,6 +413,7 @@
       ReceiveConfig receiveConfig,
       RefOperationValidators.Factory refValidatorsFactory,
       ReplaceOp.Factory replaceOpFactory,
+      RetryHelper retryHelper,
       RequestScopePropagator requestScopePropagator,
       ReviewDb db,
       Sequences seq,
@@ -453,6 +457,7 @@
     this.receiveConfig = receiveConfig;
     this.refValidatorsFactory = refValidatorsFactory;
     this.replaceOpFactory = replaceOpFactory;
+    this.retryHelper = retryHelper;
     this.requestScopePropagator = requestScopePropagator;
     this.seq = seq;
     this.sshInfo = sshInfo;
@@ -2831,94 +2836,113 @@
         refName);
     // TODO(dborowitz): Combine this BatchUpdate with the main one in
     // insertChangesAndPatchSets.
-    try (BatchUpdate bu =
-            batchUpdateFactory.create(db, projectState.getNameKey(), user, TimeUtil.nowTs());
-        ObjectInserter ins = repo.newObjectInserter();
-        ObjectReader reader = ins.newReader();
-        RevWalk rw = new RevWalk(reader)) {
-      bu.setRepository(repo, rw, ins).updateChangesInParallel();
-      bu.setRequestId(receiveId);
-      // TODO(dborowitz): Teach BatchUpdate to ignore missing changes.
+    try {
+      retryHelper.execute(
+          updateFactory -> {
+            try (BatchUpdate bu =
+                    updateFactory.create(db, projectState.getNameKey(), user, TimeUtil.nowTs());
+                ObjectInserter ins = repo.newObjectInserter();
+                ObjectReader reader = ins.newReader();
+                RevWalk rw = new RevWalk(reader)) {
+              bu.setRepository(repo, rw, ins).updateChangesInParallel();
+              bu.setRequestId(receiveId);
+              // TODO(dborowitz): Teach BatchUpdate to ignore missing changes.
 
-      RevCommit newTip = rw.parseCommit(cmd.getNewId());
-      Branch.NameKey branch = new Branch.NameKey(project.getNameKey(), refName);
+              RevCommit newTip = rw.parseCommit(cmd.getNewId());
+              Branch.NameKey branch = new Branch.NameKey(project.getNameKey(), refName);
 
-      rw.reset();
-      rw.markStart(newTip);
-      if (!ObjectId.zeroId().equals(cmd.getOldId())) {
-        rw.markUninteresting(rw.parseCommit(cmd.getOldId()));
-      }
+              rw.reset();
+              rw.markStart(newTip);
+              if (!ObjectId.zeroId().equals(cmd.getOldId())) {
+                rw.markUninteresting(rw.parseCommit(cmd.getOldId()));
+              }
 
-      ListMultimap<ObjectId, Ref> byCommit = changeRefsById();
-      Map<Change.Key, ChangeNotes> byKey = null;
-      List<ReplaceRequest> replaceAndClose = new ArrayList<>();
+              ListMultimap<ObjectId, Ref> byCommit = changeRefsById();
+              Map<Change.Key, ChangeNotes> byKey = null;
+              List<ReplaceRequest> replaceAndClose = new ArrayList<>();
 
-      int existingPatchSets = 0;
-      int newPatchSets = 0;
-      COMMIT:
-      for (RevCommit c; (c = rw.next()) != null; ) {
-        rw.parseBody(c);
+              int existingPatchSets = 0;
+              int newPatchSets = 0;
+              COMMIT:
+              for (RevCommit c; (c = rw.next()) != null; ) {
+                rw.parseBody(c);
 
-        for (Ref ref : byCommit.get(c.copy())) {
-          PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName());
-          Optional<ChangeData> cd = byLegacyId(psId.getParentKey());
-          if (cd.isPresent() && cd.get().change().getDest().equals(branch)) {
-            existingPatchSets++;
-            bu.addOp(
-                psId.getParentKey(),
-                mergedByPushOpFactory.create(requestScopePropagator, psId, refName));
-            continue COMMIT;
-          }
-        }
+                for (Ref ref : byCommit.get(c.copy())) {
+                  PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName());
+                  Optional<ChangeData> cd =
+                      retryHelper.execute(
+                          ActionType.CHANGE_QUERY,
+                          () -> byLegacyId(psId.getParentKey()),
+                          t -> t instanceof OrmException);
+                  if (cd.isPresent() && cd.get().change().getDest().equals(branch)) {
+                    existingPatchSets++;
+                    bu.addOp(
+                        psId.getParentKey(),
+                        mergedByPushOpFactory.create(requestScopePropagator, psId, refName));
+                    continue COMMIT;
+                  }
+                }
 
-        for (String changeId : c.getFooterLines(CHANGE_ID)) {
-          if (byKey == null) {
-            byKey = openChangesByKeyByBranch(branch);
-          }
+                for (String changeId : c.getFooterLines(CHANGE_ID)) {
+                  if (byKey == null) {
+                    byKey =
+                        retryHelper.execute(
+                            ActionType.CHANGE_QUERY,
+                            () -> openChangesByKeyByBranch(branch),
+                            t -> t instanceof OrmException);
+                  }
 
-          ChangeNotes onto = byKey.get(new Change.Key(changeId.trim()));
-          if (onto != null) {
-            newPatchSets++;
-            // Hold onto this until we're done with the walk, as the call to
-            // req.validate below calls isMergedInto which resets the walk.
-            ReplaceRequest req = new ReplaceRequest(onto.getChangeId(), c, cmd, false);
-            req.notes = onto;
-            replaceAndClose.add(req);
-            continue COMMIT;
-          }
-        }
-      }
+                  ChangeNotes onto = byKey.get(new Change.Key(changeId.trim()));
+                  if (onto != null) {
+                    newPatchSets++;
+                    // Hold onto this until we're done with the walk, as the call to
+                    // req.validate below calls isMergedInto which resets the walk.
+                    ReplaceRequest req = new ReplaceRequest(onto.getChangeId(), c, cmd, false);
+                    req.notes = onto;
+                    replaceAndClose.add(req);
+                    continue COMMIT;
+                  }
+                }
+              }
 
-      for (ReplaceRequest req : replaceAndClose) {
-        Change.Id id = req.notes.getChangeId();
-        if (!req.validate(true)) {
-          logDebug("Not closing {} because validation failed", id);
-          continue;
-        }
-        req.addOps(bu, null);
-        bu.addOp(
-            id,
-            mergedByPushOpFactory
-                .create(requestScopePropagator, req.psId, refName)
-                .setPatchSetProvider(
-                    new Provider<PatchSet>() {
-                      @Override
-                      public PatchSet get() {
-                        return req.replaceOp.getPatchSet();
-                      }
-                    }));
-        bu.addOp(id, new ChangeProgressOp(closeProgress));
-      }
+              for (ReplaceRequest req : replaceAndClose) {
+                Change.Id id = req.notes.getChangeId();
+                if (!req.validate(true)) {
+                  logDebug("Not closing {} because validation failed", id);
+                  continue;
+                }
+                req.addOps(bu, null);
+                bu.addOp(
+                    id,
+                    mergedByPushOpFactory
+                        .create(requestScopePropagator, req.psId, refName)
+                        .setPatchSetProvider(
+                            new Provider<PatchSet>() {
+                              @Override
+                              public PatchSet get() {
+                                return req.replaceOp.getPatchSet();
+                              }
+                            }));
+                bu.addOp(id, new ChangeProgressOp(closeProgress));
+              }
 
-      logDebug(
-          "Auto-closing {} changes with existing patch sets and {} with new patch sets",
-          existingPatchSets,
-          newPatchSets);
-      bu.execute();
+              logDebug(
+                  "Auto-closing {} changes with existing patch sets and {} with new patch sets",
+                  existingPatchSets,
+                  newPatchSets);
+              bu.execute();
+            } catch (IOException | OrmException | PermissionBackendException e) {
+              logError("Failed to auto-close changes", e);
+            }
+            return null;
+          },
+          // Use a multiple of the default timeout to account for inner retries that may otherwise
+          // eat up the whole timeout so that no time is left to retry this outer action.
+          RetryHelper.options().timeout(retryHelper.getDefaultTimeout().multipliedBy(5)).build());
     } catch (RestApiException e) {
       logError("Can't insert patchset", e);
-    } catch (IOException | OrmException | UpdateException | PermissionBackendException e) {
-      logError("Can't scan for changes to close", e);
+    } catch (UpdateException e) {
+      logError("Failed to auto-close changes", e);
     }
   }
 
diff --git a/java/com/google/gerrit/server/update/RetryHelper.java b/java/com/google/gerrit/server/update/RetryHelper.java
index 54726fe..0f5b00f 100644
--- a/java/com/google/gerrit/server/update/RetryHelper.java
+++ b/java/com/google/gerrit/server/update/RetryHelper.java
@@ -32,9 +32,10 @@
 import com.google.common.base.Throwables;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.metrics.Counter0;
+import com.google.gerrit.metrics.Counter1;
 import com.google.gerrit.metrics.Description;
-import com.google.gerrit.metrics.Histogram0;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.Histogram1;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.LockFailureException;
@@ -61,6 +62,12 @@
     T call() throws Exception;
   }
 
+  public enum ActionType {
+    ACCOUNT_UPDATE,
+    CHANGE_QUERY,
+    CHANGE_UPDATE
+  }
+
   /**
    * Options for retrying a single operation.
    *
@@ -96,25 +103,29 @@
   @VisibleForTesting
   @Singleton
   public static class Metrics {
-    final Histogram0 attemptCounts;
-    final Counter0 timeoutCount;
+    final Histogram1<ActionType> attemptCounts;
+    final Counter1<ActionType> timeoutCount;
 
     @Inject
     Metrics(MetricMaker metricMaker) {
+      Field<ActionType> view = Field.ofEnum(ActionType.class, "action_type");
       attemptCounts =
           metricMaker.newHistogram(
-              "batch_update/retry_attempt_counts",
+              "action/retry_attempt_counts",
               new Description(
-                      "Distribution of number of attempts made by RetryHelper"
+                      "Distribution of number of attempts made by RetryHelper to execute an action"
                           + " (1 == single attempt, no retry)")
                   .setCumulative()
-                  .setUnit("attempts"));
+                  .setUnit("attempts"),
+              view);
       timeoutCount =
           metricMaker.newCounter(
-              "batch_update/retry_timeout_count",
-              new Description("Number of executions of RetryHelper that ultimately timed out")
+              "action/retry_timeout_count",
+              new Description(
+                      "Number of action executions of RetryHelper that ultimately timed out")
                   .setCumulative()
-                  .setUnit("timeouts"));
+                  .setUnit("timeouts"),
+              view);
     }
   }
 
@@ -171,9 +182,16 @@
     return defaultTimeout;
   }
 
-  public <T> T execute(Action<T> action) throws IOException, ConfigInvalidException, OrmException {
+  public <T> T execute(ActionType actionType, Action<T> action)
+      throws IOException, ConfigInvalidException, OrmException {
+    return execute(actionType, action, t -> t instanceof LockFailureException);
+  }
+
+  public <T> T execute(
+      ActionType actionType, Action<T> action, Predicate<Throwable> exceptionPredicate)
+      throws IOException, ConfigInvalidException, OrmException {
     try {
-      return execute(action, defaults(), t -> t instanceof LockFailureException);
+      return execute(actionType, action, defaults(), exceptionPredicate);
     } catch (Throwable t) {
       Throwables.throwIfUnchecked(t);
       Throwables.throwIfInstanceOf(t, IOException.class);
@@ -195,10 +213,13 @@
         // transactions. Either way, retrying a partially-failed operation is not idempotent, so
         // don't do it automatically. Let the end user decide whether they want to retry.
         return execute(
-            () -> changeAction.call(updateFactory), RetryerBuilder.<T>newBuilder().build());
+            ActionType.CHANGE_UPDATE,
+            () -> changeAction.call(updateFactory),
+            RetryerBuilder.<T>newBuilder().build());
       }
 
       return execute(
+          ActionType.CHANGE_UPDATE,
           () -> changeAction.call(updateFactory),
           opts,
           t -> {
@@ -218,6 +239,7 @@
   /**
    * Executes an action with a given retryer.
    *
+   * @param actionType the type of the action
    * @param action the action which should be executed and retried on failure
    * @param opts options for retrying the action on failure
    * @param exceptionPredicate predicate to control on which exception the action should be retried
@@ -225,33 +247,39 @@
    * @throws Throwable any error or exception that made the action fail, callers are expected to
    *     catch and inspect this Throwable to decide carefully whether it should be re-thrown
    */
-  private <T> T execute(Action<T> action, Options opts, Predicate<Throwable> exceptionPredicate)
+  private <T> T execute(
+      ActionType actionType,
+      Action<T> action,
+      Options opts,
+      Predicate<Throwable> exceptionPredicate)
       throws Throwable {
     MetricListener listener = new MetricListener();
     try {
       RetryerBuilder<T> retryerBuilder = createRetryerBuilder(opts, exceptionPredicate);
       retryerBuilder.withRetryListener(listener);
-      return execute(action, retryerBuilder.build());
+      return execute(actionType, action, retryerBuilder.build());
     } finally {
-      metrics.attemptCounts.record(listener.getAttemptCount());
+      metrics.attemptCounts.record(actionType, listener.getAttemptCount());
     }
   }
 
   /**
    * Executes an action with a given retryer.
    *
+   * @param actionType the type of the action
    * @param action the action which should be executed and retried on failure
    * @param retryer the retryer
    * @return the result of executing the action
    * @throws Throwable any error or exception that made the action fail, callers are expected to
    *     catch and inspect this Throwable to decide carefully whether it should be re-thrown
    */
-  private <T> T execute(Action<T> action, Retryer<T> retryer) throws Throwable {
+  private <T> T execute(ActionType actionType, Action<T> action, Retryer<T> retryer)
+      throws Throwable {
     try {
       return retryer.call(() -> action.call());
     } catch (ExecutionException | RetryException e) {
       if (e instanceof RetryException) {
-        metrics.timeoutCount.increment();
+        metrics.timeoutCount.increment(actionType);
       }
       if (e.getCause() != null) {
         throw e.getCause();
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
index 37b2c21..a7b5e54 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
@@ -117,40 +117,32 @@
         if (linkCopy.name === 'Repositories' && this._repoName) {
           linkCopy.subsection = {
             name: this._repoName,
-            view: 'gr-repo',
+            view: Gerrit.Nav.View.REPO,
             noBaseUrl: true,
-            url: `/admin/repos/${this.encodeURL(this._repoName, true)}`,
+            url: Gerrit.Nav.getUrlForRepo(this._repoName),
             children: [{
               name: 'Access',
-              detailType: 'access',
-              view: 'gr-repo-access',
-              noBaseUrl: true,
-              url: `/admin/repos/` +
-                  `${this.encodeURL(this._repoName, true)},access`,
+              view: Gerrit.Nav.View.REPO,
+              detailType: Gerrit.Nav.RepoDetailView.ACCESS,
+              url: Gerrit.Nav.getUrlForRepoAccess(this._repoName),
             },
             {
               name: 'Commands',
-              detailType: 'commands',
-              view: 'gr-repo-commands',
-              noBaseUrl: true,
-              url: `/admin/repos/` +
-                  `${this.encodeURL(this._repoName, true)},commands`,
+              view: Gerrit.Nav.View.REPO,
+              detailType: Gerrit.Nav.RepoDetailView.COMMANDS,
+              url: Gerrit.Nav.getUrlForRepoCommands(this._repoName),
             },
             {
               name: 'Branches',
-              detailType: 'branches',
-              view: 'gr-repo-detail-list',
-              noBaseUrl: true,
-              url: `/admin/repos/` +
-                  `${this.encodeURL(this._repoName, true)},branches`,
+              view: Gerrit.Nav.View.REPO,
+              detailType: Gerrit.Nav.RepoDetailView.BRANCHES,
+              url: Gerrit.Nav.getUrlForRepoBranches(this._repoName),
             },
             {
               name: 'Tags',
-              detailType: 'tags',
-              view: 'gr-repo-detail-list',
-              noBaseUrl: true,
-              url: `/admin/repos/` +
-                  `${this.encodeURL(this._repoName, true)},tags`,
+              view: Gerrit.Nav.View.REPO,
+              detailType: Gerrit.Nav.RepoDetailView.TAGS,
+              url: Gerrit.Nav.getUrlForRepoTags(this._repoName),
             }],
           };
         }
@@ -268,6 +260,13 @@
         return '';
       }
 
+      if (params.view === Gerrit.Nav.View.REPO &&
+          itemView === Gerrit.Nav.View.REPO) {
+        if (!params.detail && !opt_detailType) { return 'selected'; }
+        if (params.detail === opt_detailType) { return 'selected'; }
+        return '';
+      }
+
       if (params.detailType && params.detailType !== opt_detailType) {
         return '';
       }
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
index 6a90488..bd79872 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
@@ -258,9 +258,8 @@
 
         test('repo', done => {
           element.params = {
-            view: Gerrit.Nav.View.ADMIN,
-            project: 'foo',
-            adminView: 'gr-repo',
+            view: Gerrit.Nav.View.REPO,
+            repoName: 'foo',
           };
           element._repoName = 'foo';
           element.reload().then(() => {
@@ -275,10 +274,9 @@
 
         test('repo access', done => {
           element.params = {
-            view: Gerrit.Nav.View.ADMIN,
-            adminView: 'gr-repo-access',
-            detailType: 'access',
-            repo: 'foo',
+            view: Gerrit.Nav.View.REPO,
+            detail: Gerrit.Nav.RepoDetailView.ACCESS,
+            repoName: 'foo',
           };
           element._repoName = 'foo';
           element.reload().then(() => {
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
index f8c7e74..7b17f22 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
@@ -55,6 +55,12 @@
     //    - `groupId`, required, String, the ID of the group.
     //    - `detail`, optional, String, the name of the group detail view.
     //      Takes any value from Gerrit.Nav.GroupDetailView.
+    //
+    //  - Gerrit.Nav.View.REPO:
+    //    - `repoName`, required, String, the name of the repo
+    //    - `detail`, optional, String, the name of the repo detail view.
+    //      Takes any value from Gerrit.Nav.RepoDetailView.
+
 
     window.Gerrit = window.Gerrit || {};
 
@@ -79,6 +85,7 @@
         EDIT: 'edit',
         GROUP: 'group',
         PLUGIN_SCREEN: 'plugin-screen',
+        REPO: 'repo',
         SEARCH: 'search',
         SETTINGS: 'settings',
       },
@@ -88,6 +95,13 @@
         LOG: 'log',
       },
 
+      RepoDetailView: {
+        ACCESS: 'access',
+        BRANCHES: 'branches',
+        COMMANDS: 'commands',
+        TAGS: 'tags',
+      },
+
       WeblinkType: {
         CHANGE: 'change',
         FILE: 'file',
@@ -362,6 +376,65 @@
       },
 
       /**
+       * @param {string} repoName
+       * @return {string}
+       */
+      getUrlForRepo(repoName) {
+        return this._getUrlFor({
+          view: Gerrit.Nav.View.REPO,
+          repoName,
+        });
+      },
+
+      /**
+       * @param {string} repoName
+       * @return {string}
+       */
+      getUrlForRepoTags(repoName) {
+        return this._getUrlFor({
+          view: Gerrit.Nav.View.REPO,
+          repoName,
+          detail: Gerrit.Nav.RepoDetailView.TAGS,
+        });
+      },
+
+      /**
+       * @param {string} repoName
+       * @return {string}
+       */
+      getUrlForRepoBranches(repoName) {
+        return this._getUrlFor({
+          view: Gerrit.Nav.View.REPO,
+          repoName,
+          detail: Gerrit.Nav.RepoDetailView.BRANCHES,
+        });
+      },
+
+      /**
+       * @param {string} repoName
+       * @return {string}
+       */
+      getUrlForRepoAccess(repoName) {
+        return this._getUrlFor({
+          view: Gerrit.Nav.View.REPO,
+          repoName,
+          detail: Gerrit.Nav.RepoDetailView.ACCESS,
+        });
+      },
+
+      /**
+       * @param {string} repoName
+       * @return {string}
+       */
+      getUrlForRepoCommands(repoName) {
+        return this._getUrlFor({
+          view: Gerrit.Nav.View.REPO,
+          repoName,
+          detail: Gerrit.Nav.RepoDetailView.COMMANDS,
+        });
+      },
+
+      /**
        * @param {string} groupId
        * @return {string}
        */
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
index f309747..37f9f23 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -233,6 +233,8 @@
         url = this._generateDiffOrEditUrl(params);
       } else if (params.view === Views.GROUP) {
         url = this._generateGroupUrl(params);
+      } else if (params.view === Views.REPO) {
+        url = this._generateRepoUrl(params);
       } else if (params.view === Views.SETTINGS) {
         url = this._generateSettingsUrl(params);
       } else {
@@ -448,6 +450,24 @@
      * @param {!Object} params
      * @return {string}
      */
+    _generateRepoUrl(params) {
+      let url = `/admin/repos/${this.encodeURL(params.repoName + '', true)}`;
+      if (params.detail === Gerrit.Nav.RepoDetailView.ACCESS) {
+        url += ',access';
+      } else if (params.detail === Gerrit.Nav.RepoDetailView.BRANCHES) {
+        url += ',branches';
+      } else if (params.detail === Gerrit.Nav.RepoDetailView.TAGS) {
+        url += ',tags';
+      } else if (params.detail === Gerrit.Nav.RepoDetailView.COMMANDS) {
+        url += ',commands';
+      }
+      return url;
+    },
+
+    /**
+     * @param {!Object} params
+     * @return {string}
+     */
     _generateSettingsUrl(params) {
       return '/settings';
     },
diff --git a/resources/com/google/gerrit/httpd/auth/ldap/LoginForm.html b/resources/com/google/gerrit/httpd/auth/ldap/LoginForm.html
index 64d16c5..39900e8 100644
--- a/resources/com/google/gerrit/httpd/auth/ldap/LoginForm.html
+++ b/resources/com/google/gerrit/httpd/auth/ldap/LoginForm.html
@@ -15,13 +15,13 @@
     </style>
     <style id="gerrit_sitecss" type="text/css"></style>
   </head>
-  <body>
+  <body class="login" id="login_ldap">
     <div id="gerrit_topmenu" style="height:45px;" class="gerritTopMenu"></div>
     <div id="gerrit_header"></div>
     <div id="gerrit_body" class="gerritBody">
       <h1>Sign In to Gerrit Code Review at <span id="hostName">example.com</span></h1>
       <div id="error_message">Invalid username or password.</div>
-      <form method="POST" action="#" id="login_form">
+      <form method="POST" action="#" id="login_form" onsubmit="return shouldSubmit()">
         <table style="border: 0;">
         <tr>
           <th>Username</th>
@@ -50,7 +50,7 @@
         <tr>
           <td></td>
           <td>
-            <input type="submit" value="Sign In" tabindex="4"/>
+            <input id="b_signin" type="submit" value="Sign In" tabindex="4"/>
             <a href="../" id="cancel_link">Cancel</a>
           </td>
         </tr>
@@ -62,6 +62,17 @@
     </div>
 
     <script type="text/javascript">
+    <![CDATA[
+      var submitted = false;
+      function shouldSubmit() {
+        if(!submitted) {
+          submitted = true;
+          document.getElementById('b_signin').disabled=true;
+          return true;
+        }
+        return false;
+      }
+
       var login_form = document.getElementById('login_form');
       var f_user = document.getElementById('f_user');
       var f_pass = document.getElementById('f_pass');
@@ -75,12 +86,13 @@
         }
       }
       f_pass.onkeyup = function(e) {
-        if (e.keyCode == 13) {
+        if (e.keyCode == 13 && shouldSubmit()) {
           login_form.submit();
           return false;
         }
       }
       f_user.focus();
+    ]]>
     </script>
   </body>
 </html>
diff --git a/resources/com/google/gerrit/httpd/auth/oauth/LoginForm.html b/resources/com/google/gerrit/httpd/auth/oauth/LoginForm.html
index f7814c0..67c40c3 100644
--- a/resources/com/google/gerrit/httpd/auth/oauth/LoginForm.html
+++ b/resources/com/google/gerrit/httpd/auth/oauth/LoginForm.html
@@ -24,7 +24,7 @@
     </style>
     <style id="gerrit_sitecss" type="text/css"></style>
   </head>
-  <body>
+  <body class="login" id="login_oauth">
     <div id="gerrit_topmenu" style="height:45px;" class="gerritTopMenu"></div>
     <div id="gerrit_header"></div>
     <div id="gerrit_body" class="gerritBody">
diff --git a/resources/com/google/gerrit/httpd/auth/openid/LoginForm.html b/resources/com/google/gerrit/httpd/auth/openid/LoginForm.html
index 07e09f5..4923143 100644
--- a/resources/com/google/gerrit/httpd/auth/openid/LoginForm.html
+++ b/resources/com/google/gerrit/httpd/auth/openid/LoginForm.html
@@ -39,7 +39,7 @@
     </style>
     <style id="gerrit_sitecss" type="text/css"></style>
   </head>
-  <body>
+  <body class="login" id="login_openid">
     <div id="gerrit_topmenu" style="height:45px;" class="gerritTopMenu"></div>
     <div id="gerrit_header"></div>
     <div id="gerrit_body" class="gerritBody">