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[]) {