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">