Merge "Update yarn.lock"
diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt
index 99ff0db..ab341e8 100644
--- a/Documentation/cmd-index.txt
+++ b/Documentation/cmd-index.txt
@@ -247,6 +247,18 @@
Given the trace ID an administrator can find the corresponding logs and
investigate issues more easily.
+[[deadline]]
+=== Setting a deadline
+
+When invoking an SSH command it's possible that the client sets a deadline
+after which the request should be aborted. To do this the
+`--deadline <deadline>` option must be set on the request. Values must be
+specified using standard time unit abbreviations ('ms', 'sec', 'min', etc.).
+
+----
+ $ ssh -p 29418 review.example.com gerrit create-project --deadline 5m foo/bar
+----
+
GERRIT
------
Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 2204c65..d9c2e4c 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -4294,6 +4294,9 @@
be specified using standard time unit abbreviations ('ms', 'sec',
'min', etc.).
+
+The receive timeout cannot be overriden by setting a higher
+link:user-upload#deadline[deadline] on the git push request.
++
Default is 4 minutes. If no unit is specified, milliseconds
is assumed.
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 0c5ea40..517e4a8 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -7722,8 +7722,8 @@
RevertSubmission endpoint is `revert-{submission_id}-{timestamp.now}`.
Topic can't contain quotation marks.
|`work_in_progress` |optional|
-When present, change is marked as Work In Progress. This will also override
-the notify value to `OWNER`. +
+When present, change is marked as Work In Progress. The `notify` input is
+used if it's present, otherwise it will be overridden to `OWNER`. +
If not set, the default is false.
|=============================
diff --git a/Documentation/rest-api.txt b/Documentation/rest-api.txt
index ee5882a..7d2781c 100644
--- a/Documentation/rest-api.txt
+++ b/Documentation/rest-api.txt
@@ -244,6 +244,20 @@
Given the trace ID an administrator can find the corresponding logs and
investigate issues more easily.
+[[deadline]]
+=== Setting a deadline
+
+When invoking a REST endpoint it's possible that the client sets a deadline
+after which the request should be aborted. To do this the `X-Gerrit-Deadline`
+header must be set on the request. Values must be specified using standard time
+unit abbreviations ('ms', 'sec', 'min', etc.).
+
+.Example Request
+----
+ GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/suggest_reviewers?q=J
+ X-Gerrit-Deadline: 5m
+----
+
[[updated-refs]]
=== X-Gerrit-UpdatedRef
This is only enabled when "X-Gerrit-UpdatedRef-Enabled" is set to "true" in the
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt
index a04ff35..3816dc4 100644
--- a/Documentation/user-upload.txt
+++ b/Documentation/user-upload.txt
@@ -460,6 +460,21 @@
Given the trace ID an administrator can find the corresponding logs and
investigate issues more easily.
+[[deadline]]
+==== Setting a deadline
+
+When pushing to Gerrit it's possible that the client sets a deadline after which
+the push should be aborted. To do this the `deadline=<deadline>` push option
+must be set on the git push. Values must be specified using standard time unit
+abbreviations ('ms', 'sec', 'min', etc.).
+
+----
+ git push -o deadline=10m ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/master
+----
+
+Setting a deadline doesn't override the server-side
+link:config.html#receive.timeout[receive timeout] after which a push is aborted.
+
[[push_replace]]
=== Replace Changes
diff --git a/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java b/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
index 98e660c..3bdcb1a 100644
--- a/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
+++ b/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
@@ -66,7 +66,7 @@
"is:open owner:${user} -is:wip -is:ignored limit:25";
public static final String DASHBOARD_INCOMING_QUERY =
"is:open -owner:${user} -is:wip -is:ignored (reviewer:${user} OR assignee:${user}) limit:25";
- public static final String CC_QUERY = "is:open -is:ignored cc:${user} limit:10";
+ public static final String CC_QUERY = "is:open -is:ignored -is:wip cc:${user} limit:10";
public static final String DASHBOARD_RECENTLY_CLOSED_QUERY =
"is:closed -is:ignored (-is:wip OR owner:self) "
+ "(owner:${user} OR reviewer:${user} OR assignee:${user} "
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 375aa54..7c2b354 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -106,14 +106,17 @@
import com.google.gerrit.server.AccessPath;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.DeadlineChecker;
import com.google.gerrit.server.DynamicOptions;
import com.google.gerrit.server.ExceptionHook;
+import com.google.gerrit.server.InvalidDeadlineException;
import com.google.gerrit.server.OptionUtil;
import com.google.gerrit.server.RequestInfo;
import com.google.gerrit.server.RequestListener;
import com.google.gerrit.server.audit.ExtendedHttpAuditEvent;
import com.google.gerrit.server.cache.PerThreadCache;
import com.google.gerrit.server.cancellation.RequestCancelledException;
+import com.google.gerrit.server.cancellation.RequestStateContext;
import com.google.gerrit.server.cancellation.RequestStateProvider;
import com.google.gerrit.server.change.ChangeFinder;
import com.google.gerrit.server.change.RevisionResource;
@@ -208,6 +211,7 @@
private static final String FORM_TYPE = "application/x-www-form-urlencoded";
+ @VisibleForTesting public static final String X_GERRIT_DEADLINE = "X-Gerrit-Deadline";
@VisibleForTesting public static final String X_GERRIT_TRACE = "X-Gerrit-Trace";
@VisibleForTesting public static final String X_GERRIT_UPDATED_REF = "X-Gerrit-UpdatedRef";
@@ -350,7 +354,10 @@
try (TraceContext traceContext = enableTracing(req, res)) {
List<IdString> path = splitPath(req);
- try (PerThreadCache ignored = PerThreadCache.create()) {
+ try (RequestStateContext requestStateContext =
+ RequestStateContext.open()
+ .addRequestStateProvider(new DeadlineChecker(req.getHeader(X_GERRIT_DEADLINE)));
+ PerThreadCache ignored = PerThreadCache.create()) {
RequestInfo requestInfo = createRequestInfo(traceContext, requestUri(req), path);
globals.requestListeners.runEach(l -> l.onRequest(requestInfo));
@@ -713,6 +720,10 @@
messageOr(e, "Quota limit reached"),
e.caching(),
e);
+ } catch (InvalidDeadlineException e) {
+ cause = Optional.of(e);
+ responseBytes =
+ replyError(req, res, statusCode = SC_BAD_REQUEST, messageOr(e, "Bad Request"), e);
} catch (Exception e) {
cause = Optional.of(e);
diff --git a/java/com/google/gerrit/server/DeadlineChecker.java b/java/com/google/gerrit/server/DeadlineChecker.java
new file mode 100644
index 0000000..03504d4
--- /dev/null
+++ b/java/com/google/gerrit/server/DeadlineChecker.java
@@ -0,0 +1,137 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+
+import com.google.common.base.Function;
+import com.google.common.base.Strings;
+import com.google.common.primitives.Longs;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.server.cancellation.RequestStateProvider;
+import com.google.gerrit.server.config.ConfigUtil;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+/** {@link RequestStateProvider} that checks whether a client provided deadline is exceeded. */
+public class DeadlineChecker implements RequestStateProvider {
+ /**
+ * Formatter to format a timeout as {@code timeout=<TIMEOUT><TIME_UNIT>}.
+ *
+ * <p>If the timeout is 1 minute or greater, minutes is used as a time unit. Otherwise
+ * milliseconds is just as a time unit.
+ */
+ public static Function<Long, String> TIMEOUT_FORMATTER =
+ timeout -> {
+ String formattedTimeout = MILLISECONDS.convert(timeout, NANOSECONDS) + "ms";
+ long timeoutInMinutes = MINUTES.convert(timeout, NANOSECONDS);
+ if (timeoutInMinutes > 0) {
+ formattedTimeout = timeoutInMinutes + "m";
+ }
+ return String.format("timeout=%s", formattedTimeout);
+ };
+
+ /**
+ * Timeout in nanoseconds after which the request should be aborted.
+ *
+ * <p>{@code 0} means that no timeout should be applied.
+ */
+ private final long timeout;
+
+ /**
+ * The deadline in nanoseconds after which a request should be aborted.
+ *
+ * <p>deadline = start + timeout
+ *
+ * <p>{@link Optional#empty()} if no timeout was set.
+ */
+ private final Optional<Long> deadline;
+
+ /**
+ * Creates a {@code ClientProvidedDeadlineChecker}.
+ *
+ * <p>No deadline is enforced if the client provided deadline value is {@code null} or {@code 0}.
+ *
+ * @param clientProvidedTimeoutValue the timeout value that the client provided, must represent a
+ * numerical time unit (e.g. "5m"), if no time unit is specified milliseconds are assumed, may
+ * be {@code null}
+ * @throws InvalidDeadlineException thrown if the client provided deadline value cannot be parsed,
+ * e.g. because it uses a bad time unit
+ */
+ public DeadlineChecker(@Nullable String clientProvidedTimeoutValue)
+ throws InvalidDeadlineException {
+ this(System.nanoTime(), clientProvidedTimeoutValue);
+ }
+
+ /**
+ * Creates a {@code ClientProvidedDeadlineChecker}.
+ *
+ * <p>No deadline is enforced if the client provided deadline value is {@code null} or {@code 0}.
+ *
+ * @param start the start time of the request in nanoseconds
+ * @param clientProvidedTimeoutValue the timeout value that the client provided, must represent a
+ * numerical time unit (e.g. "5m"), if no time unit is specified milliseconds are assumed, may
+ * be {@code null}
+ * @throws InvalidDeadlineException thrown if the client provided deadline value cannot be parsed,
+ * e.g. because it uses a bad time unit
+ */
+ public DeadlineChecker(long start, @Nullable String clientProvidedTimeoutValue)
+ throws InvalidDeadlineException {
+ this.timeout = parseTimeout(clientProvidedTimeoutValue);
+ this.deadline = timeout > 0 ? Optional.of(start + timeout) : Optional.empty();
+ }
+
+ @Override
+ public void checkIfCancelled(OnCancelled onCancelled) {
+ long now = System.nanoTime();
+ if (deadline.isPresent() && now > deadline.get()) {
+ onCancelled.onCancel(
+ Reason.CLIENT_PROVIDED_DEADLINE_EXCEEDED, TIMEOUT_FORMATTER.apply(timeout));
+ }
+ }
+
+ /**
+ * Parses the given timeout value.
+ *
+ * @param timeoutValue the timeout that should be parsed, must represent a numerical time unit
+ * (e.g. "5m"), if no time unit is specified minutes are assumed, may be {@code null}
+ * @return the parsed timeout in nanoseconds, {@code 0} if no timeout should be applied
+ * @throws InvalidDeadlineException thrown if the provided deadline value cannot be parsed, e.g.
+ * because it uses a bad time unit
+ */
+ private static long parseTimeout(@Nullable String timeoutValue) throws InvalidDeadlineException {
+ if (Strings.isNullOrEmpty(timeoutValue) || "0".equals(timeoutValue)) {
+ return 0;
+ }
+
+ // If no time unit was specified, assume milliseconds.
+ if (Longs.tryParse(timeoutValue) != null) {
+ throw new InvalidDeadlineException(String.format("Missing time unit: %s", timeoutValue));
+ }
+
+ try {
+ long parsedTimeout =
+ ConfigUtil.getTimeUnit(timeoutValue, /* defaultValue= */ -1, TimeUnit.NANOSECONDS);
+ if (parsedTimeout == -1) {
+ throw new InvalidDeadlineException(String.format("Invalid value: %s", timeoutValue));
+ }
+ return parsedTimeout;
+ } catch (IllegalArgumentException e) {
+ throw new InvalidDeadlineException(e.getMessage(), e);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/InvalidDeadlineException.java b/java/com/google/gerrit/server/InvalidDeadlineException.java
new file mode 100644
index 0000000..d23b289
--- /dev/null
+++ b/java/com/google/gerrit/server/InvalidDeadlineException.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server;
+
+/** Exception that is thrown is a deadline cannot be parsed. */
+public class InvalidDeadlineException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ private static final String MESSAGE_PREFIX = "Invalid deadline. ";
+
+ public InvalidDeadlineException(String message) {
+ super(MESSAGE_PREFIX + message);
+ }
+
+ public InvalidDeadlineException(String message, Throwable cause) {
+ super(MESSAGE_PREFIX + message, cause);
+ }
+}
diff --git a/java/com/google/gerrit/server/RequestConfig.java b/java/com/google/gerrit/server/RequestConfig.java
new file mode 100644
index 0000000..bbefd7a
--- /dev/null
+++ b/java/com/google/gerrit/server/RequestConfig.java
@@ -0,0 +1,200 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Account;
+import java.util.Optional;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+
+/**
+ * Represents a configuration on request level that matches requests by request type, URI pattern,
+ * caller and/or project pattern.
+ */
+@AutoValue
+public abstract class RequestConfig {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ public static ImmutableList<RequestConfig> parseConfigs(Config cfg, String section) {
+ ImmutableList.Builder<RequestConfig> requestConfigs = ImmutableList.builder();
+
+ for (String id : cfg.getSubsections(section)) {
+ try {
+ RequestConfig.Builder requestConfig = RequestConfig.builder();
+ requestConfig.id(id);
+ requestConfig.requestTypes(parseRequestTypes(cfg, section, id));
+ requestConfig.requestUriPatterns(parseRequestUriPatterns(cfg, section, id));
+ requestConfig.accountIds(parseAccounts(cfg, section, id));
+ requestConfig.projectPatterns(parseProjectPatterns(cfg, section, id));
+ requestConfigs.add(requestConfig.build());
+ } catch (ConfigInvalidException e) {
+ logger.atWarning().log("Ignoring invalid %s configuration:\n %s", section, e.getMessage());
+ }
+ }
+
+ return requestConfigs.build();
+ }
+
+ private static ImmutableSet<String> parseRequestTypes(Config cfg, String section, String id) {
+ return ImmutableSet.copyOf(cfg.getStringList(section, id, "requestType"));
+ }
+
+ private static ImmutableSet<Pattern> parseRequestUriPatterns(
+ Config cfg, String section, String id) throws ConfigInvalidException {
+ return parsePatterns(cfg, section, id, "requestUriPattern");
+ }
+
+ private static ImmutableSet<Account.Id> parseAccounts(Config cfg, String section, String id)
+ throws ConfigInvalidException {
+ ImmutableSet.Builder<Account.Id> accountIds = ImmutableSet.builder();
+ String[] accounts = cfg.getStringList(section, id, "account");
+ for (String account : accounts) {
+ Optional<Account.Id> accountId = Account.Id.tryParse(account);
+ if (!accountId.isPresent()) {
+ throw new ConfigInvalidException(
+ String.format(
+ "Invalid request config ('%s.%s.account = %s'): invalid account ID",
+ section, id, account));
+ }
+ accountIds.add(accountId.get());
+ }
+ return accountIds.build();
+ }
+
+ private static ImmutableSet<Pattern> parseProjectPatterns(Config cfg, String section, String id)
+ throws ConfigInvalidException {
+ return parsePatterns(cfg, section, id, "projectPattern");
+ }
+
+ private static ImmutableSet<Pattern> parsePatterns(
+ Config cfg, String section, String id, String name) throws ConfigInvalidException {
+ ImmutableSet.Builder<Pattern> patterns = ImmutableSet.builder();
+ String[] patternRegExs = cfg.getStringList(section, id, name);
+ for (String patternRegEx : patternRegExs) {
+ try {
+ patterns.add(Pattern.compile(patternRegEx));
+ } catch (PatternSyntaxException e) {
+ throw new ConfigInvalidException(
+ String.format(
+ "Invalid request config ('%s.%s.%s = %s'): %s",
+ section, id, name, patternRegEx, e.getMessage()));
+ }
+ }
+ return patterns.build();
+ }
+
+ /** ID of the config */
+ abstract String id();
+
+ /** request types that should be matched */
+ abstract ImmutableSet<String> requestTypes();
+
+ /** pattern matching request URIs */
+ abstract ImmutableSet<Pattern> requestUriPatterns();
+
+ /** accounts IDs matching calling user */
+ abstract ImmutableSet<Account.Id> accountIds();
+
+ /** pattern matching projects names */
+ abstract ImmutableSet<Pattern> projectPatterns();
+
+ private static Builder builder() {
+ return new AutoValue_RequestConfig.Builder();
+ }
+
+ /**
+ * Whether this request config matches a given request.
+ *
+ * @param requestInfo request info
+ * @return whether this request config matches
+ */
+ boolean matches(RequestInfo requestInfo) {
+ // If in the request config request types are set and none of them matches, then the request is
+ // not matched.
+ if (!requestTypes().isEmpty()
+ && requestTypes().stream()
+ .noneMatch(type -> type.equalsIgnoreCase(requestInfo.requestType()))) {
+ return false;
+ }
+
+ // If in the request config request URI patterns are set and none of them matches, then the
+ // request is not matched.
+ if (!requestUriPatterns().isEmpty()) {
+ if (!requestInfo.requestUri().isPresent()) {
+ // The request has no request URI, hence it cannot match a request URI pattern.
+ return false;
+ }
+
+ if (requestUriPatterns().stream()
+ .noneMatch(p -> p.matcher(requestInfo.requestUri().get()).matches())) {
+ return false;
+ }
+ }
+
+ // If in the request config accounts are set and none of them matches, then the request is not
+ // matched.
+ if (!accountIds().isEmpty()) {
+ try {
+ if (accountIds().stream()
+ .noneMatch(id -> id.equals(requestInfo.callingUser().getAccountId()))) {
+ return false;
+ }
+ } catch (UnsupportedOperationException e) {
+ // The calling user is not logged in, hence it cannot match an account.
+ return false;
+ }
+ }
+
+ // If in the request config project patterns are set and none of them matches, then the request
+ // is not matched.
+ if (!projectPatterns().isEmpty()) {
+ if (!requestInfo.project().isPresent()) {
+ // The request is not for a project, hence it cannot match a project pattern.
+ return false;
+ }
+
+ if (projectPatterns().stream()
+ .noneMatch(p -> p.matcher(requestInfo.project().get().get()).matches())) {
+ return false;
+ }
+ }
+
+ // For any match criteria (request type, request URI pattern, account, project pattern) that
+ // was specified in the request config, at least one of the configured value matched the
+ // request.
+ return true;
+ }
+
+ @AutoValue.Builder
+ abstract static class Builder {
+ abstract Builder id(String id);
+
+ abstract Builder requestTypes(ImmutableSet<String> requestTypes);
+
+ abstract Builder requestUriPatterns(ImmutableSet<Pattern> requestUriPatterns);
+
+ abstract Builder accountIds(ImmutableSet<Account.Id> accountIds);
+
+ abstract Builder projectPatterns(ImmutableSet<Pattern> projectPatterns);
+
+ abstract RequestConfig build();
+ }
+}
diff --git a/java/com/google/gerrit/server/TraceRequestListener.java b/java/com/google/gerrit/server/TraceRequestListener.java
index 20c9f57..7136e47 100644
--- a/java/com/google/gerrit/server/TraceRequestListener.java
+++ b/java/com/google/gerrit/server/TraceRequestListener.java
@@ -14,19 +14,11 @@
package com.google.gerrit.server;
-import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.entities.Account;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.logging.RequestId;
import com.google.inject.Inject;
import com.google.inject.Singleton;
-import java.util.Optional;
-import java.util.regex.Pattern;
-import java.util.regex.PatternSyntaxException;
-import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
/**
@@ -36,15 +28,13 @@
*/
@Singleton
public class TraceRequestListener implements RequestListener {
- private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private static String SECTION_TRACING = "tracing";
- private final Config cfg;
- private final ImmutableList<TraceConfig> traceConfigs;
+ private final ImmutableList<RequestConfig> traceConfigs;
@Inject
TraceRequestListener(@GerritServerConfig Config cfg) {
- this.cfg = cfg;
- this.traceConfigs = parseTraceConfigs();
+ this.traceConfigs = RequestConfig.parseConfigs(cfg, SECTION_TRACING);
}
@Override
@@ -57,172 +47,6 @@
requestInfo
.traceContext()
.forceLogging()
- .addTag(RequestId.Type.TRACE_ID, traceConfig.traceId()));
- }
-
- private ImmutableList<TraceConfig> parseTraceConfigs() {
- ImmutableList.Builder<TraceConfig> traceConfigs = ImmutableList.builder();
-
- for (String traceId : cfg.getSubsections("tracing")) {
- try {
- TraceConfig.Builder traceConfig = TraceConfig.builder();
- traceConfig.traceId(traceId);
- traceConfig.requestTypes(parseRequestTypes(traceId));
- traceConfig.requestUriPatterns(parseRequestUriPatterns(traceId));
- traceConfig.accountIds(parseAccounts(traceId));
- traceConfig.projectPatterns(parseProjectPatterns(traceId));
- traceConfigs.add(traceConfig.build());
- } catch (ConfigInvalidException e) {
- logger.atWarning().log("Ignoring invalid tracing configuration:\n %s", e.getMessage());
- }
- }
-
- return traceConfigs.build();
- }
-
- private ImmutableSet<String> parseRequestTypes(String traceId) {
- return ImmutableSet.copyOf(cfg.getStringList("tracing", traceId, "requestType"));
- }
-
- private ImmutableSet<Pattern> parseRequestUriPatterns(String traceId)
- throws ConfigInvalidException {
- return parsePatterns(traceId, "requestUriPattern");
- }
-
- private ImmutableSet<Account.Id> parseAccounts(String traceId) throws ConfigInvalidException {
- ImmutableSet.Builder<Account.Id> accountIds = ImmutableSet.builder();
- String[] accounts = cfg.getStringList("tracing", traceId, "account");
- for (String account : accounts) {
- Optional<Account.Id> accountId = Account.Id.tryParse(account);
- if (!accountId.isPresent()) {
- throw new ConfigInvalidException(
- String.format(
- "Invalid tracing config ('tracing.%s.account = %s'): invalid account ID",
- traceId, account));
- }
- accountIds.add(accountId.get());
- }
- return accountIds.build();
- }
-
- private ImmutableSet<Pattern> parseProjectPatterns(String traceId) throws ConfigInvalidException {
- return parsePatterns(traceId, "projectPattern");
- }
-
- private ImmutableSet<Pattern> parsePatterns(String traceId, String name)
- throws ConfigInvalidException {
- ImmutableSet.Builder<Pattern> patterns = ImmutableSet.builder();
- String[] patternRegExs = cfg.getStringList("tracing", traceId, name);
- for (String patternRegEx : patternRegExs) {
- try {
- patterns.add(Pattern.compile(patternRegEx));
- } catch (PatternSyntaxException e) {
- throw new ConfigInvalidException(
- String.format(
- "Invalid tracing config ('tracing.%s.%s = %s'): %s",
- traceId, name, patternRegEx, e.getMessage()));
- }
- }
- return patterns.build();
- }
-
- @AutoValue
- abstract static class TraceConfig {
- /** ID for the trace */
- abstract String traceId();
-
- /** request types that should be traced */
- abstract ImmutableSet<String> requestTypes();
-
- /** pattern matching request URIs */
- abstract ImmutableSet<Pattern> requestUriPatterns();
-
- /** accounts IDs matching calling user */
- abstract ImmutableSet<Account.Id> accountIds();
-
- /** pattern matching projects names */
- abstract ImmutableSet<Pattern> projectPatterns();
-
- static Builder builder() {
- return new AutoValue_TraceRequestListener_TraceConfig.Builder();
- }
-
- /**
- * Whether this trace config matches a given request.
- *
- * @param requestInfo request info
- * @return whether this trace config matches
- */
- boolean matches(RequestInfo requestInfo) {
- // If in the trace config request types are set and none of them matches, then the request is
- // not matched.
- if (!requestTypes().isEmpty()
- && requestTypes().stream()
- .noneMatch(type -> type.equalsIgnoreCase(requestInfo.requestType()))) {
- return false;
- }
-
- // If in the trace config request URI patterns are set and none of them matches, then the
- // request is not matched.
- if (!requestUriPatterns().isEmpty()) {
- if (!requestInfo.requestUri().isPresent()) {
- // The request has no request URI, hence it cannot match a request URI pattern.
- return false;
- }
-
- if (requestUriPatterns().stream()
- .noneMatch(p -> p.matcher(requestInfo.requestUri().get()).matches())) {
- return false;
- }
- }
-
- // If in the trace config accounts are set and none of them matches, then the request is not
- // matched.
- if (!accountIds().isEmpty()) {
- try {
- if (accountIds().stream()
- .noneMatch(id -> id.equals(requestInfo.callingUser().getAccountId()))) {
- return false;
- }
- } catch (UnsupportedOperationException e) {
- // The calling user is not logged in, hence it cannot match an account.
- return false;
- }
- }
-
- // If in the trace config project patterns are set and none of them matches, then the request
- // is not matched.
- if (!projectPatterns().isEmpty()) {
- if (!requestInfo.project().isPresent()) {
- // The request is not for a project, hence it cannot match a project pattern.
- return false;
- }
-
- if (projectPatterns().stream()
- .noneMatch(p -> p.matcher(requestInfo.project().get().get()).matches())) {
- return false;
- }
- }
-
- // For any match criteria (request type, request URI pattern, account, project pattern) that
- // was specified in the trace config, at least one of the configured value matched the
- // request.
- return true;
- }
-
- @AutoValue.Builder
- abstract static class Builder {
- abstract Builder traceId(String traceId);
-
- abstract Builder requestTypes(ImmutableSet<String> requestTypes);
-
- abstract Builder requestUriPatterns(ImmutableSet<Pattern> requestUriPatterns);
-
- abstract Builder accountIds(ImmutableSet<Account.Id> accountIds);
-
- abstract Builder projectPatterns(ImmutableSet<Pattern> projectPatterns);
-
- abstract TraceConfig build();
- }
+ .addTag(RequestId.Type.TRACE_ID, traceConfig.id()));
}
}
diff --git a/java/com/google/gerrit/server/git/CommitUtil.java b/java/com/google/gerrit/server/git/CommitUtil.java
index 0e0185a..2259741 100644
--- a/java/com/google/gerrit/server/git/CommitUtil.java
+++ b/java/com/google/gerrit/server/git/CommitUtil.java
@@ -265,7 +265,7 @@
Change changeToRevert = notes.getChange();
Change.Id changeId = Change.id(seq.nextChangeId());
if (input.workInProgress) {
- input.notify = NotifyHandling.OWNER;
+ input.notify = firstNonNull(input.notify, NotifyHandling.OWNER);
}
NotifyResolver.Result notify =
notifyResolver.resolve(firstNonNull(input.notify, NotifyHandling.ALL), input.notifyDetails);
diff --git a/java/com/google/gerrit/server/git/MultiProgressMonitor.java b/java/com/google/gerrit/server/git/MultiProgressMonitor.java
index 6c68dfc..e55f70b 100644
--- a/java/com/google/gerrit/server/git/MultiProgressMonitor.java
+++ b/java/com/google/gerrit/server/git/MultiProgressMonitor.java
@@ -14,8 +14,8 @@
package com.google.gerrit.server.git;
+import static com.google.gerrit.server.DeadlineChecker.TIMEOUT_FORMATTER;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
-import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import com.google.common.base.Strings;
@@ -415,22 +415,7 @@
} else if (deadlineExceeded) {
onCancelled.onCancel(
RequestStateProvider.Reason.SERVER_DEADLINE_EXCEEDED,
- formatTimeout().map(t -> "timeout=" + t).orElse(null));
+ timeout.map(TIMEOUT_FORMATTER).orElse(null));
}
}
-
- private Optional<String> formatTimeout() {
- if (!timeout.isPresent()) {
- return Optional.empty();
- }
-
- String formattedTimeout = MILLISECONDS.convert(timeout.get(), NANOSECONDS) + "ms";
-
- long timeoutInMinutes = MINUTES.convert(timeout.get(), NANOSECONDS);
- if (timeoutInMinutes > 0) {
- formattedTimeout = timeoutInMinutes + "m";
- }
-
- return Optional.of(formattedTimeout);
- }
}
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index fea7ba5..fd9e6b9 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -107,7 +107,9 @@
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.CreateGroupPermissionSyncer;
+import com.google.gerrit.server.DeadlineChecker;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.InvalidDeadlineException;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.PublishCommentUtil;
import com.google.gerrit.server.PublishCommentsOp;
@@ -618,7 +620,10 @@
ReceiveCommitsResult processCommands(
Collection<ReceiveCommand> commands, MultiProgressMonitor progress) throws StorageException {
checkState(!used, "Tried to re-use a ReceiveCommits objects that is single-use only");
+ long start = System.nanoTime();
parsePushOptions();
+ String clientProvidedDeadlineValue =
+ Iterables.getLast(pushOptions.get("deadline"), /* defaultValue= */ null);
int commandCount = commands.size();
try (TraceContext traceContext =
TraceContext.newTrace(
@@ -645,9 +650,13 @@
commands.stream().map(c -> wrapReceiveCommand(c, commandProgress)).collect(toList());
try (RequestStateContext requestStateContext =
- RequestStateContext.open().addRequestStateProvider(progress)) {
+ RequestStateContext.open()
+ .addRequestStateProvider(progress)
+ .addRequestStateProvider(new DeadlineChecker(start, clientProvidedDeadlineValue))) {
processCommandsUnsafe(commands, progress);
rejectRemaining(commands, INTERNAL_SERVER_ERROR);
+ } catch (InvalidDeadlineException e) {
+ rejectRemaining(commands, e.getMessage());
} catch (RuntimeException e) {
Optional<RequestCancelledException> requestCancelledException =
RequestCancelledException.getFromCausalChain(e);
@@ -1561,6 +1570,12 @@
@Option(name = "--trace", metaVar = "NAME", usage = "enable tracing")
String trace;
+ @Option(
+ name = "--deadline",
+ metaVar = "NAME",
+ usage = "deadline after which the push should be aborted")
+ String deadline;
+
@Option(name = "--base", metaVar = "BASE", usage = "merge base of changes")
List<ObjectId> base;
diff --git a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
index 20249df..8bde6e7 100644
--- a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
+++ b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
@@ -258,7 +258,7 @@
Project.NameKey project = projectAndBranch.project();
cherryPickInput.destination = projectAndBranch.branch();
if (revertInput.workInProgress) {
- cherryPickInput.notify = NotifyHandling.OWNER;
+ cherryPickInput.notify = firstNonNull(cherryPickInput.notify, NotifyHandling.OWNER);
}
Collection<ChangeData> changesInProjectAndBranch =
changesPerProjectAndBranch.get(projectAndBranch);
diff --git a/java/com/google/gerrit/sshd/SshCommand.java b/java/com/google/gerrit/sshd/SshCommand.java
index e58040f..f95e72e 100644
--- a/java/com/google/gerrit/sshd/SshCommand.java
+++ b/java/com/google/gerrit/sshd/SshCommand.java
@@ -17,10 +17,13 @@
import com.google.common.base.Throwables;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.server.AccessPath;
+import com.google.gerrit.server.DeadlineChecker;
import com.google.gerrit.server.DynamicOptions;
+import com.google.gerrit.server.InvalidDeadlineException;
import com.google.gerrit.server.RequestInfo;
import com.google.gerrit.server.RequestListener;
import com.google.gerrit.server.cancellation.RequestCancelledException;
+import com.google.gerrit.server.cancellation.RequestStateContext;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.logging.PerformanceLogContext;
import com.google.gerrit.server.logging.PerformanceLogger;
@@ -46,6 +49,9 @@
@Option(name = "--trace-id", usage = "trace ID (can only be set if --trace was set too)")
private String traceId;
+ @Option(name = "--deadline", usage = "deadline after which the request should be aborted)")
+ private String deadline;
+
protected PrintWriter stdout;
protected PrintWriter stderr;
@@ -59,11 +65,16 @@
stderr = toPrintWriter(err);
try (TraceContext traceContext = enableTracing();
PerformanceLogContext performanceLogContext =
- new PerformanceLogContext(config, performanceLoggers)) {
+ new PerformanceLogContext(config, performanceLoggers);
+ RequestStateContext requestStateContext =
+ RequestStateContext.open()
+ .addRequestStateProvider(new DeadlineChecker(deadline))) {
RequestInfo requestInfo =
RequestInfo.builder(RequestInfo.RequestType.SSH, user, traceContext).build();
requestListeners.runEach(l -> l.onRequest(requestInfo));
SshCommand.this.run();
+ } catch (InvalidDeadlineException e) {
+ stderr.println(e.getMessage());
} catch (RuntimeException e) {
Optional<RequestCancelledException> requestCancelledException =
RequestCancelledException.getFromCausalChain(e);
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java b/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
index 6cf3f3e..ff88f31 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
@@ -359,7 +359,10 @@
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
sender.clear();
- gApi.changes().id(r.getChangeId()).revert(createWipRevertInput()).get();
+ // If notify input not specified, the endpoint overrides it to OWNER
+ RevertInput revertInput = createWipRevertInput();
+ revertInput.notify = null;
+ gApi.changes().id(r.getChangeId()).revert(revertInput).get();
assertThat(sender.getMessages()).isEmpty();
}
@@ -702,7 +705,7 @@
}
@Test
- public void revertSubmissionWipNotificationsAreSupressed() throws Exception {
+ public void revertSubmissionWipNotificationsWithNotifyHandlingAll() throws Exception {
String changeId1 = createChange("first change", "a.txt", "message").getChangeId();
approve(changeId1);
gApi.changes().id(changeId1).addReviewer(user.email());
@@ -714,13 +717,12 @@
sender.clear();
+ // If notify handling is specified, it will be used by the API
RevertInput revertInput = createWipRevertInput();
- // Setting the Notifications to ALL will be overridden because the WIP flag overrides the
- // notifications to OWNER
revertInput.notify = NotifyHandling.ALL;
gApi.changes().id(changeId2).revertSubmission(revertInput);
- assertThat(sender.getMessages()).isEmpty();
+ assertThat(sender.getMessages()).hasSize(4);
}
@Test
diff --git a/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java b/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java
index 6025e67..d94bae6 100644
--- a/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java
@@ -24,6 +24,7 @@
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.httpd.restapi.RestApiServlet;
import com.google.gerrit.server.cancellation.RequestCancelledException;
import com.google.gerrit.server.cancellation.RequestStateProvider;
import com.google.gerrit.server.events.CommitReceivedEvent;
@@ -35,7 +36,9 @@
import com.google.gerrit.server.validators.ProjectCreationValidationListener;
import com.google.gerrit.server.validators.ValidationException;
import com.google.inject.Inject;
+import java.util.ArrayList;
import java.util.List;
+import org.apache.http.message.BasicHeader;
import org.junit.Test;
public class CancellationIT extends AbstractDaemonTest {
@@ -144,6 +147,53 @@
}
@Test
+ public void abortIfClientProvidedDeadlineExceeded() throws Exception {
+ RestResponse response =
+ adminRestSession.putWithHeaders(
+ "/projects/" + name("new"), new BasicHeader(RestApiServlet.X_GERRIT_DEADLINE, "1ms"));
+ assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
+ assertThat(response.getEntityContent())
+ .isEqualTo("Client Provided Deadline Exceeded\n\ntimeout=1ms");
+ }
+
+ @Test
+ public void requestRejectedIfInvalidDeadlineIsProvided_missingTimeUnit() throws Exception {
+ RestResponse response =
+ adminRestSession.putWithHeaders(
+ "/projects/" + name("new"), new BasicHeader(RestApiServlet.X_GERRIT_DEADLINE, "1"));
+ response.assertBadRequest();
+ assertThat(response.getEntityContent()).isEqualTo("Invalid deadline. Missing time unit: 1");
+ }
+
+ @Test
+ public void requestRejectedIfInvalidDeadlineIsProvided_invalidTimeUnit() throws Exception {
+ RestResponse response =
+ adminRestSession.putWithHeaders(
+ "/projects/" + name("new"), new BasicHeader(RestApiServlet.X_GERRIT_DEADLINE, "1x"));
+ response.assertBadRequest();
+ assertThat(response.getEntityContent())
+ .isEqualTo("Invalid deadline. Invalid time unit value: 1x");
+ }
+
+ @Test
+ public void requestRejectedIfInvalidDeadlineIsProvided_invalidValue() throws Exception {
+ RestResponse response =
+ adminRestSession.putWithHeaders(
+ "/projects/" + name("new"),
+ new BasicHeader(RestApiServlet.X_GERRIT_DEADLINE, "invalid"));
+ response.assertBadRequest();
+ assertThat(response.getEntityContent()).isEqualTo("Invalid deadline. Invalid value: invalid");
+ }
+
+ @Test
+ public void requestSucceedsWithinDeadline() throws Exception {
+ RestResponse response =
+ adminRestSession.putWithHeaders(
+ "/projects/" + name("new"), new BasicHeader(RestApiServlet.X_GERRIT_DEADLINE, "10m"));
+ response.assertCreated();
+ }
+
+ @Test
public void handleClientDisconnectedForPush() throws Exception {
CommitValidationListener commitValidationListener =
new CommitValidationListener() {
@@ -266,4 +316,68 @@
PushOneCommit.Result r = push.to("refs/for/master");
r.assertOkStatus();
}
+
+ @Test
+ public void abortPushIfClientProvidedDeadlineExceeded() throws Exception {
+ List<String> pushOptions = new ArrayList<>();
+ pushOptions.add("deadline=1ms");
+ PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+ push.setPushOptions(pushOptions);
+ PushOneCommit.Result r = push.to("refs/for/master");
+ r.assertErrorStatus("Client Provided Deadline Exceeded (timeout=1ms)");
+ }
+
+ @Test
+ public void pushRejectedIfInvalidDeadlineIsProvided_missingTimeUnit() throws Exception {
+ List<String> pushOptions = new ArrayList<>();
+ pushOptions.add("deadline=1");
+ PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+ push.setPushOptions(pushOptions);
+ PushOneCommit.Result r = push.to("refs/for/master");
+ r.assertErrorStatus("Invalid deadline. Missing time unit: 1");
+ }
+
+ @Test
+ public void pushRejectedIfInvalidDeadlineIsProvided_invalidTimeUnit() throws Exception {
+ List<String> pushOptions = new ArrayList<>();
+ pushOptions.add("deadline=1x");
+ PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+ push.setPushOptions(pushOptions);
+ PushOneCommit.Result r = push.to("refs/for/master");
+ r.assertErrorStatus("Invalid deadline. Invalid time unit value: 1x");
+ }
+
+ @Test
+ public void pushRejectedIfInvalidDeadlineIsProvided_invalidValue() throws Exception {
+ List<String> pushOptions = new ArrayList<>();
+ pushOptions.add("deadline=invalid");
+ PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+ push.setPushOptions(pushOptions);
+ PushOneCommit.Result r = push.to("refs/for/master");
+ r.assertErrorStatus("Invalid deadline. Invalid value: invalid");
+ }
+
+ @Test
+ public void pushSucceedsWithinDeadline() throws Exception {
+ List<String> pushOptions = new ArrayList<>();
+ pushOptions.add("deadline=10m");
+ PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+ push.setPushOptions(pushOptions);
+ PushOneCommit.Result r = push.to("refs/for/master");
+ r.assertOkStatus();
+ }
+
+ @Test
+ @GerritConfig(name = "receive.timeout", value = "1ms")
+ @GerritConfig(
+ name = "experiments.enabled",
+ value = ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_PUSH_CANCELLATION)
+ public void clientProvidedDeadlineOnPushDoesntOverrideServerTimeout() throws Exception {
+ List<String> pushOptions = new ArrayList<>();
+ pushOptions.add("deadline=10m");
+ PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+ push.setPushOptions(pushOptions);
+ PushOneCommit.Result r = push.to("refs/for/master");
+ r.assertErrorStatus("Server Deadline Exceeded (timeout=1ms)");
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SshCancellationIT.java b/javatests/com/google/gerrit/acceptance/ssh/SshCancellationIT.java
index 3e31c16..113382e 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/SshCancellationIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/SshCancellationIT.java
@@ -117,4 +117,34 @@
adminSshSession.assertFailure("Server Deadline Exceeded (deadline = 10m)");
}
}
+
+ @Test
+ public void abortIfClientProvidedDeadlineExceeded() throws Exception {
+ adminSshSession.exec("gerrit create-project --deadline 1ms " + name("new"));
+ adminSshSession.assertFailure("Client Provided Deadline Exceeded (timeout=1ms)");
+ }
+
+ @Test
+ public void requestRejectedIfInvalidDeadlineIsProvided_missingTimeUnit() throws Exception {
+ adminSshSession.exec("gerrit create-project --deadline 1 " + name("new"));
+ adminSshSession.assertFailure("Invalid deadline. Missing time unit: 1");
+ }
+
+ @Test
+ public void requestRejectedIfInvalidDeadlineIsProvided_invalidTimeUnit() throws Exception {
+ adminSshSession.exec("gerrit create-project --deadline 1x " + name("new"));
+ adminSshSession.assertFailure("Invalid deadline. Invalid time unit value: 1x");
+ }
+
+ @Test
+ public void requestRejectedIfInvalidDeadlineIsProvided_invalidValue() throws Exception {
+ adminSshSession.exec("gerrit create-project --deadline invalid " + name("new"));
+ adminSshSession.assertFailure("Invalid deadline. Invalid value: invalid");
+ }
+
+ @Test
+ public void requestSucceedsWithinDeadline() throws Exception {
+ adminSshSession.exec("gerrit create-project --deadline 10m " + name("new"));
+ adminSshSession.assertSuccess();
+ }
}
diff --git a/polygerrit-ui/app/api/rest-api.ts b/polygerrit-ui/app/api/rest-api.ts
index fe9d00d..6c6fff0 100644
--- a/polygerrit-ui/app/api/rest-api.ts
+++ b/polygerrit-ui/app/api/rest-api.ts
@@ -418,6 +418,7 @@
cherry_pick_of_patch_set?: PatchSetNum;
contains_git_conflicts?: boolean;
internalHost?: string; // TODO(TS): provide an explanation what is its
+ submit_requirements?: SubmitRequirementResultInfo[];
}
// The ID of the change in the format "'<project>~<branch>~<Change-Id>'"
@@ -1019,3 +1020,29 @@
/** URL to the icon of the link. */
image_url: string;
}
+
+/**
+ * The SubmitRequirementResultInfo describes the result of evaluating
+ * a submit requirement on a change.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#submit-requirement-result-info
+ */
+export declare interface SubmitRequirementResultInfo {
+ name: string;
+ description?: string;
+ status: string;
+ applicability_expression_result?: SubmitRequirementExpressionInfo;
+ submittability_expression_result: SubmitRequirementExpressionInfo;
+ override_expression_result?: SubmitRequirementExpressionInfo;
+}
+
+/**
+ * The SubmitRequirementExpressionInfo describes the result of evaluating
+ * a single submit requirement expression, for example label:code-review=+2.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#submit-requirement-expression-info
+ */
+export declare interface SubmitRequirementExpressionInfo {
+ expression: string;
+ fulfilled: boolean;
+ passing_atoms: string;
+ failing_atoms: string;
+}
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
index 5c456ed..c79511f 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
@@ -489,7 +489,7 @@
</section>
<div class="separatedSection">
<template is="dom-if" if="[[_isSubmitRequirementsUiEnabled]]">
- <gr-submit-requirements></gr-submit-requirements>
+ <gr-submit-requirements change="[[change]]"></gr-submit-requirements>
</template>
<template is="dom-if" if="[[!_isSubmitRequirementsUiEnabled]]">
<gr-change-requirements
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
index 16e176c..c7c9612 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
@@ -15,25 +15,63 @@
* limitations under the License.
*/
import {GrLitElement} from '../../lit/gr-lit-element';
-import {css, customElement, html} from 'lit-element';
+import {css, customElement, html, property} from 'lit-element';
+import {ParsedChangeInfo} from '../../../types/types';
@customElement('gr-submit-requirements')
export class GrSubmitRequirements extends GrLitElement {
+ @property({type: Object})
+ change?: ParsedChangeInfo;
+
static get styles() {
return [
css`
+ :host {
+ display: table;
+ width: 100%;
+ }
.metadata-title {
font-size: 100%;
font-weight: var(--font-weight-bold);
color: var(--deemphasized-text-color);
padding-left: var(--metadata-horizontal-padding);
}
+ section {
+ display: table-row;
+ }
+ .title {
+ min-width: 10em;
+ padding: var(--spacing-s) var(--spacing-m) 0
+ var(--requirements-horizontal-padding);
+ }
+ .value {
+ padding: var(--spacing-s) 0 0 0;
+ }
+ .title,
+ .value {
+ display: table-cell;
+ vertical-align: top;
+ }
`,
];
}
render() {
- return html`<h3 class="metadata-title">Submit Requirements</h3>`;
+ const submit_requirements = this.change?.submit_requirements ?? [];
+ return html`<h3 class="metadata-title">Submit Requirements</h3>
+
+ ${submit_requirements.map(
+ requirement => html`<section>
+ <div class="title">
+ <gr-limited-text
+ class="name"
+ limit="25"
+ text="${requirement.name}"
+ ></gr-limited-text>
+ </div>
+ <div class="value">${requirement.status}</div>
+ </section>`
+ )}`;
}
}
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
index 6eaca5e..f27e0f1 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
@@ -206,7 +206,7 @@
// Open changes the viewed user is CCed on. Changes ignored by the viewing
// user are filtered out.
name: 'CCed on',
- query: 'is:open -is:ignored cc:${user}',
+ query: 'is:open -is:ignored -is:wip cc:${user}',
suffixForDashboard: 'limit:10',
};
export const CLOSED: DashboardSection = {
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
index 841089e..eea95e1 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
@@ -315,6 +315,8 @@
// Fetch projects.
return this.projectSuggestions(predicate, expression);
+ case 'assignee':
+ case 'attention':
case 'author':
case 'cc':
case 'commentby':
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts
index 28bc229..ee9163b 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts
@@ -156,6 +156,7 @@
import {firePageError, fireServerError} from '../../../utils/event-util';
import {ParsedChangeInfo} from '../../../types/types';
import {ErrorCallback} from '../../../api/rest';
+import {FlagsService, KnownExperimentId} from '../../../services/flags/flags';
const MAX_PROJECT_RESULTS = 25;
// This value is somewhat arbitrary and not based on research or calculations.
@@ -291,14 +292,17 @@
// The value is set in created, before any other actions
private authService: AuthService;
+ private flagService: FlagsService;
+
// The value is set in created, before any other actions
private readonly _restApiHelper: GrRestApiHelper;
- constructor(authService?: AuthService) {
+ constructor(authService?: AuthService, flagService?: FlagsService) {
super();
// TODO: Make the authService constructor parameter required when we have
// changed all usages of this class to not instantiate via createElement().
this.authService = authService ?? appContext.authService;
+ this.flagService = flagService ?? appContext.flagsService;
this._restApiHelper = new GrRestApiHelper(
this._cache,
this.authService,
@@ -1148,7 +1152,8 @@
if (
window.DEFAULT_DETAIL_HEXES &&
window.DEFAULT_DETAIL_HEXES.changePage &&
- (!config || !(config.receive && config.receive.enable_signed_push))
+ (!config || !(config.receive && config.receive.enable_signed_push)) &&
+ !this.flagService?.isEnabled(KnownExperimentId.SUBMIT_REQUIREMENTS_UI)
) {
return window.DEFAULT_DETAIL_HEXES.changePage;
}
@@ -1169,6 +1174,9 @@
if (config?.receive?.enable_signed_push) {
options.push(ListChangesOption.PUSH_CERTIFICATES);
}
+ if (this.flagService?.isEnabled(KnownExperimentId.SUBMIT_REQUIREMENTS_UI)) {
+ options.push(ListChangesOption.SUBMIT_REQUIREMENTS);
+ }
return listChangesOptionsToHex(...options);
}
diff --git a/polygerrit-ui/app/services/app-context-init.ts b/polygerrit-ui/app/services/app-context-init.ts
index f74962a..ade9529 100644
--- a/polygerrit-ui/app/services/app-context-init.ts
+++ b/polygerrit-ui/app/services/app-context-init.ts
@@ -73,7 +73,8 @@
reportingService: () => new GrReporting(appContext.flagsService),
eventEmitter: () => new EventEmitter(),
authService: () => new Auth(appContext.eventEmitter),
- restApiService: () => new GrRestApiInterface(appContext.authService),
+ restApiService: () =>
+ new GrRestApiInterface(appContext.authService, appContext.flagsService),
changeService: () => new ChangeService(),
commentsService: () => new CommentsService(appContext.restApiService),
checksService: () => new ChecksService(appContext.reportingService),
diff --git a/polygerrit-ui/app/utils/change-util.ts b/polygerrit-ui/app/utils/change-util.ts
index c54c099..c94493b 100644
--- a/polygerrit-ui/app/utils/change-util.ts
+++ b/polygerrit-ui/app/utils/change-util.ts
@@ -105,6 +105,9 @@
* deletions field (number of lines deleted)
*/
SKIP_DIFFSTAT: 23,
+
+ /** Include the evaluated submit requirements for the caller. */
+ SUBMIT_REQUIREMENTS: 24,
};
export function listChangesOptionsToHex(...args: number[]) {