Merge "PureRevert: Don't write any output when computing diff"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index ecc2ba0..91cb87f 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -3790,6 +3790,14 @@
Defaults to link:#retry.timeout[`retry.timeout`]; unit suffixes are supported,
and assumes milliseconds if not specified.
+[[retry.retryWithTraceOnFailure]]retry.retryWithTraceOnFailure::
++
+Whether Gerrit should automatically retry operations on failure with tracing
+enabled. The automatically generated traces can help with debugging. Please
+note that only some of the REST endpoints support automatic retry.
++
+By default this is set to false.
+
[[rules]]
=== Section rules
@@ -4552,6 +4560,52 @@
+
By default, true.
+[[tracing.traceid]]
+==== Subsection tracing.<trace-id>
+
+There can be multiple `tracing.<trace-id>` subsections to configure
+automatic tracing of requests. To be traced a request must match all
+conditions of one `tracing.<trace-id>` subsection. The subsection name
+is used as trace ID. Using this trace ID administrators can find
+matching log entries.
+
+[[tracing.traceid.requestType]]tracing.<trace-id>.requestType::
++
+Type of request for which request tracing should be always enabled (can
+be `GIT_RECEIVE`, `GIT_UPLOAD`, `REST` and `SSH`).
++
+May be specified multiple times.
++
+By default, unset (all request types are matched).
+
+[[tracing.traceid.requestUriPattern]]tracing.<trace-id>.requestUriPattern::
++
+Regular expression to match request URIs for which request tracing
+should be always enabled. Request URIs are only available for REST
+requests. Request URIs never include the '/a' prefix.
++
+May be specified multiple times.
++
+By default, unset (all request URIs are matched).
+
+[[tracing.traceid.account]]tracing.<trace-id>.account::
++
+Account ID of an account for which request tracing should be always
+enabled.
++
+May be specified multiple times.
++
+By default, unset (all accounts are matched).
+
+[[tracing.traceid.projectPattern]]tracing.<trace-id>.projectPattern::
++
+Regular expression to match project names for which request tracing
+should be always enabled.
++
+May be specified multiple times.
++
+By default, unset (all projects are matched).
+
[[trackingid]]
=== Section trackingid
diff --git a/Documentation/config-labels.txt b/Documentation/config-labels.txt
index ff43520..9c90ba7 100644
--- a/Documentation/config-labels.txt
+++ b/Documentation/config-labels.txt
@@ -297,12 +297,13 @@
[[label_copyAllScoresOnTrivialRebase]]
=== `label.Label-Name.copyAllScoresOnTrivialRebase`
-If true, all scores for the label are copied forward when a new patch
-set is uploaded that is a trivial rebase. A new patch set is considered
-as trivial rebase if the commit message is the same as in the previous
-patch set and if it has the same code delta as the previous patch set.
-This is the case if the change was rebased onto a different parent, or
-if the parent did not change at all.
+If true, all scores for the label are copied forward when a new patch set is
+uploaded that is a trivial rebase. A new patch set is considered to be trivial
+rebase if the commit message is the same as in the previous patch set and if it
+has the same diff (including context lines) as the previous patch set. This is
+the case if the change was rebased onto a different parent and that rebase did
+not require git to perform any conflict resolution, or if the parent did not
+change at all.
This can be used to enable sticky approvals, reducing turn-around for
trivial rebases prior to submitting a change.
@@ -313,13 +314,13 @@
[[label_copyAllScoresIfNoCodeChange]]
=== `label.Label-Name.copyAllScoresIfNoCodeChange`
-If true, all scores for the label are copied forward when a new patch
-set is uploaded that has the same parent tree as the previous patch
-set and the same code delta as the previous patch set. This means only
-the commit message is different. This can be used to enable sticky
-approvals on labels that only depend on the code, reducing turn-around
-if only the commit message is changed prior to submitting a change.
-For the Verified label that is optionally installed by the
+If true, all scores for the label are copied forward when a new patch set is
+uploaded that has the same parent tree as the previous patch set and the same
+code diff (including context lines) as the previous patch set. This means only
+the commit message is different; the change hasn't even been rebased. This can
+be used to enable sticky approvals on labels that only depend on the code,
+reducing turn-around if only the commit message is changed prior to submitting a
+change. For the Verified label that is optionally installed by the
link:pgm-init.html[init] site program this is enabled by default.
Defaults to false.
diff --git a/java/com/google/gerrit/extensions/common/AvatarInfo.java b/java/com/google/gerrit/extensions/common/AvatarInfo.java
index 00f1819..de609eb 100644
--- a/java/com/google/gerrit/extensions/common/AvatarInfo.java
+++ b/java/com/google/gerrit/extensions/common/AvatarInfo.java
@@ -21,7 +21,7 @@
* <p>The web UI prefers avatar images to be square, both the height and width of the image should
* be this size. The height is the more important dimension to match than the width.
*/
- public static final int DEFAULT_SIZE = 26;
+ public static final int DEFAULT_SIZE = 32;
public String url;
public Integer height;
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 736018a..2128777b 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -317,7 +317,7 @@
try (TraceContext traceContext = enableTracing(req, res)) {
List<IdString> path = splitPath(req);
- RequestInfo requestInfo = createRequestInfo(traceContext, path);
+ RequestInfo requestInfo = createRequestInfo(traceContext, requestUri(req), path);
globals.requestListeners.runEach(l -> l.onRequest(requestInfo));
try (PerThreadCache ignored = PerThreadCache.create()) {
@@ -1424,9 +1424,11 @@
return traceContext;
}
- private RequestInfo createRequestInfo(TraceContext traceContext, List<IdString> path) {
+ private RequestInfo createRequestInfo(
+ TraceContext traceContext, String requestUri, List<IdString> path) {
RequestInfo.Builder requestInfo =
- RequestInfo.builder(RequestInfo.RequestType.REST, globals.currentUser.get(), traceContext);
+ RequestInfo.builder(RequestInfo.RequestType.REST, globals.currentUser.get(), traceContext)
+ .requestUri(requestUri);
if (path.size() < 1) {
return requestInfo.build();
diff --git a/java/com/google/gerrit/server/RequestInfo.java b/java/com/google/gerrit/server/RequestInfo.java
index c86d34d..f5749fc 100644
--- a/java/com/google/gerrit/server/RequestInfo.java
+++ b/java/com/google/gerrit/server/RequestInfo.java
@@ -45,6 +45,15 @@
*/
public abstract String requestType();
+ /**
+ * Request URI.
+ *
+ * <p>Only set if request type is {@link RequestType#REST}.
+ *
+ * <p>Never includes the "/a" prefix.
+ */
+ public abstract Optional<String> requestUri();
+
/** The user that has sent the request. */
public abstract CurrentUser callingUser();
@@ -74,6 +83,8 @@
return requestType(requestType.name());
}
+ public abstract Builder requestUri(String requestUri);
+
public abstract Builder callingUser(CurrentUser callingUser);
public abstract Builder traceContext(TraceContext traceContext);
diff --git a/java/com/google/gerrit/server/TraceRequestListener.java b/java/com/google/gerrit/server/TraceRequestListener.java
index 1ae39e1..773f712 100644
--- a/java/com/google/gerrit/server/TraceRequestListener.java
+++ b/java/com/google/gerrit/server/TraceRequestListener.java
@@ -14,12 +14,215 @@
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.reviewdb.client.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;
+/**
+ * Request listener that sets additional logging tags and enables tracing automatically if the
+ * request matches any tracing configuration in gerrit.config (see description of
+ * 'tracing.<trace-id>' subsection in config-gerrit.txt).
+ */
@Singleton
public class TraceRequestListener implements RequestListener {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private final Config cfg;
+ private final ImmutableList<TraceConfig> traceConfigs;
+
+ @Inject
+ TraceRequestListener(@GerritServerConfig Config cfg) {
+ this.cfg = cfg;
+ this.traceConfigs = parseTraceConfigs();
+ }
+
@Override
public void onRequest(RequestInfo requestInfo) {
requestInfo.project().ifPresent(p -> requestInfo.traceContext().addTag("project", p));
+ traceConfigs.stream()
+ .filter(traceConfig -> traceConfig.matches(requestInfo))
+ .forEach(
+ traceConfig ->
+ 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();
+ }
}
}
diff --git a/java/com/google/gerrit/server/account/InternalAccountDirectory.java b/java/com/google/gerrit/server/account/InternalAccountDirectory.java
index 49afaf2..99ad570 100644
--- a/java/com/google/gerrit/server/account/InternalAccountDirectory.java
+++ b/java/com/google/gerrit/server/account/InternalAccountDirectory.java
@@ -150,20 +150,19 @@
if (options.contains(FillOptions.AVATARS)) {
AvatarProvider ap = avatar.get();
if (ap != null) {
- info.avatars = new ArrayList<>(3);
+ info.avatars = new ArrayList<>();
IdentifiedUser user = userFactory.create(account.getId());
- // GWT UI uses DEFAULT_SIZE (26px).
+ // PolyGerrit UI uses the following sizes for avatars:
+ // - 32px for avatars next to names e.g. on the dashboard. This is also Gerrit's default.
+ // - 56px for the user's own avatar in the menu
+ // - 100ox for other user's avatars on dashboards
+ // - 120px for the user's own profile settings page
addAvatar(ap, info, user, AvatarInfo.DEFAULT_SIZE);
-
- // PolyGerrit UI prefers 32px and 100px.
if (!info.avatars.isEmpty()) {
- if (32 != AvatarInfo.DEFAULT_SIZE) {
- addAvatar(ap, info, user, 32);
- }
- if (100 != AvatarInfo.DEFAULT_SIZE) {
- addAvatar(ap, info, user, 100);
- }
+ addAvatar(ap, info, user, 56);
+ addAvatar(ap, info, user, 100);
+ addAvatar(ap, info, user, 120);
}
}
}
diff --git a/java/com/google/gerrit/server/git/MergeUtil.java b/java/com/google/gerrit/server/git/MergeUtil.java
index 4088c81..e464e81 100644
--- a/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/java/com/google/gerrit/server/git/MergeUtil.java
@@ -17,15 +17,17 @@
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
import static com.google.gerrit.git.ObjectIds.abbreviateName;
import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Comparator.naturalOrder;
import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.joining;
-import com.google.common.base.Joiner;
import com.google.common.base.Strings;
-import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
@@ -68,7 +70,6 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
-import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -847,26 +848,22 @@
return String.format("Merge \"%s\"", c.getShortMessage());
}
- LinkedHashSet<String> topics = new LinkedHashSet<>(4);
- for (CodeReviewCommit c : merged) {
- if (!Strings.isNullOrEmpty(c.change().getTopic())) {
- topics.add(c.change().getTopic());
- }
- }
+ ImmutableSortedSet<String> topics =
+ merged.stream()
+ .map(c -> c.change().getTopic())
+ .filter(t -> !Strings.isNullOrEmpty(t))
+ .map(t -> "\"" + t + "\"")
+ .collect(toImmutableSortedSet(naturalOrder()));
- if (topics.size() == 1) {
- return String.format("Merge changes from topic \"%s\"", Iterables.getFirst(topics, null));
- } else if (topics.size() > 1) {
- return String.format("Merge changes from topics \"%s\"", Joiner.on("\", \"").join(topics));
- } else {
+ if (!topics.isEmpty()) {
return String.format(
- "Merge changes %s%s",
- FluentIterable.from(merged)
- .limit(5)
- .transform(c -> c.change().getKey().abbreviate())
- .join(Joiner.on(',')),
- merged.size() > 5 ? ", ..." : "");
+ "Merge changes from topic%s %s",
+ topics.size() > 1 ? "s" : "", topics.stream().collect(joining(", ")));
}
+ return merged.stream()
+ .limit(5)
+ .map(c -> c.change().getKey().abbreviate())
+ .collect(joining(",", "Merge changes ", merged.size() > 5 ? ", ..." : ""));
}
public ThreeWayMerger newThreeWayMerger(ObjectInserter inserter, Config repoConfig) {
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidators.java b/java/com/google/gerrit/server/git/validators/CommitValidators.java
index da051dd..02a24f7 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -146,12 +146,7 @@
new CommitterUploaderValidator(user, perm, urlFormatter.get()),
new SignedOffByValidator(user, perm, projectState),
new ChangeIdValidator(
- projectState,
- user,
- urlFormatter.get(),
- installCommitMsgHookCommand,
- sshInfo,
- change),
+ user, urlFormatter.get(), installCommitMsgHookCommand, sshInfo, change),
new ConfigValidator(projectConfigFactory, branch, user, rw, allUsers, allProjects),
new BannedCommitsValidator(rejectCommits),
new PluginCommitValidationListener(pluginValidators, skipValidation),
@@ -178,12 +173,7 @@
new AuthorUploaderValidator(user, perm, urlFormatter.get()),
new SignedOffByValidator(user, perm, projectCache.checkedGet(branch.project())),
new ChangeIdValidator(
- projectState,
- user,
- urlFormatter.get(),
- installCommitMsgHookCommand,
- sshInfo,
- change),
+ user, urlFormatter.get(), installCommitMsgHookCommand, sshInfo, change),
new ConfigValidator(projectConfigFactory, branch, user, rw, allUsers, allProjects),
new PluginCommitValidationListener(pluginValidators),
new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker),
@@ -257,7 +247,6 @@
private static final Pattern CHANGE_ID = Pattern.compile(CHANGE_ID_PATTERN);
- private final ProjectState projectState;
private final UrlFormatter urlFormatter;
private final String installCommitMsgHookCommand;
private final SshInfo sshInfo;
@@ -265,13 +254,11 @@
private final Change change;
public ChangeIdValidator(
- ProjectState projectState,
IdentifiedUser user,
UrlFormatter urlFormatter,
String installCommitMsgHookCommand,
SshInfo sshInfo,
Change change) {
- this.projectState = projectState;
this.urlFormatter = urlFormatter;
this.installCommitMsgHookCommand = installCommitMsgHookCommand;
this.sshInfo = sshInfo;
diff --git a/java/com/google/gerrit/server/mail/send/ProjectWatch.java b/java/com/google/gerrit/server/mail/send/ProjectWatch.java
index 06a2e0d..eb3b831 100644
--- a/java/com/google/gerrit/server/mail/send/ProjectWatch.java
+++ b/java/com/google/gerrit/server/mail/send/ProjectWatch.java
@@ -99,9 +99,9 @@
try {
add(matching, state.getNameKey(), nc);
} catch (QueryParseException e) {
- logger.atWarning().withCause(e).log(
- "Project %s has invalid notify %s filter \"%s\"",
- state.getName(), nc.getName(), nc.getFilter());
+ logger.atInfo().log(
+ "Project %s has invalid notify %s filter \"%s\": %s",
+ state.getName(), nc.getName(), nc.getFilter(), e.getMessage());
}
}
}
@@ -232,8 +232,8 @@
logger.atFine().log("The filter did not match for account %s; skip notification", accountId);
} catch (QueryParseException e) {
// Ignore broken filter expressions.
- logger.atWarning().withCause(e).log(
- "Account %s has invalid filter in project watch %s", accountId, key);
+ logger.atInfo().log(
+ "Account %s has invalid filter in project watch %s: %s", accountId, key, e.getMessage());
}
return false;
}
diff --git a/java/com/google/gerrit/server/restapi/account/PutUsername.java b/java/com/google/gerrit/server/restapi/account/PutUsername.java
index bc1ffc8..76a641d 100644
--- a/java/com/google/gerrit/server/restapi/account/PutUsername.java
+++ b/java/com/google/gerrit/server/restapi/account/PutUsername.java
@@ -20,9 +20,10 @@
import com.google.gerrit.exceptions.DuplicateKeyException;
import com.google.gerrit.extensions.api.accounts.UsernameInput;
import com.google.gerrit.extensions.client.AccountFieldName;
-import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.reviewdb.client.Account;
@@ -71,9 +72,7 @@
@Override
public String apply(AccountResource rsrc, UsernameInput input)
- throws AuthException, MethodNotAllowedException, UnprocessableEntityException,
- ResourceConflictException, IOException, ConfigInvalidException,
- PermissionBackendException {
+ throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
if (!self.get().hasSameAccountId(rsrc.getUser())) {
permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
}
@@ -82,17 +81,13 @@
throw new MethodNotAllowedException("realm does not allow editing username");
}
- if (input == null) {
- input = new UsernameInput();
- }
-
Account.Id accountId = rsrc.getUser().getAccountId();
if (!externalIds.byAccount(accountId, SCHEME_USERNAME).isEmpty()) {
throw new MethodNotAllowedException("Username cannot be changed.");
}
- if (Strings.isNullOrEmpty(input.username)) {
- return input.username;
+ if (input == null || Strings.isNullOrEmpty(input.username)) {
+ throw new BadRequestException("input required");
}
if (!ExternalId.isValidUsername(input.username)) {
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index 06c52c7..6c3d48b 100644
--- a/java/com/google/gerrit/server/submit/MergeOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -511,6 +511,8 @@
retryHelper
.getDefaultTimeout(ActionType.CHANGE_UPDATE)
.multipliedBy(cs.projects().size()))
+ .caller(getClass())
+ .retryWithTrace(t -> !(t instanceof RestApiException))
.build());
if (projects > 1) {
diff --git a/java/com/google/gerrit/server/update/RetryHelper.java b/java/com/google/gerrit/server/update/RetryHelper.java
index 14d04cb..695feba 100644
--- a/java/com/google/gerrit/server/update/RetryHelper.java
+++ b/java/com/google/gerrit/server/update/RetryHelper.java
@@ -40,11 +40,14 @@
import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.logging.RequestId;
+import com.google.gerrit.server.logging.TraceContext;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.time.Duration;
import java.util.Arrays;
import java.util.Map;
+import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
import java.util.function.Predicate;
@@ -94,12 +97,20 @@
@Nullable
abstract Duration timeout();
+ abstract Optional<Class<?>> caller();
+
+ abstract Optional<Predicate<Throwable>> retryWithTrace();
+
@AutoValue.Builder
public abstract static class Builder {
public abstract Builder listener(RetryListener listener);
public abstract Builder timeout(Duration timeout);
+ public abstract Builder caller(Class<?> caller);
+
+ public abstract Builder retryWithTrace(Predicate<Throwable> exceptionPredicate);
+
public abstract Options build();
}
}
@@ -147,6 +158,7 @@
private final Map<ActionType, Duration> defaultTimeouts;
private final WaitStrategy waitStrategy;
@Nullable private final Consumer<RetryerBuilder<?>> overwriteDefaultRetryerStrategySetup;
+ private final boolean retryWithTraceOnFailure;
@Inject
RetryHelper(@GerritServerConfig Config cfg, Metrics metrics, BatchUpdate.Factory updateFactory) {
@@ -186,6 +198,7 @@
MILLISECONDS),
WaitStrategies.randomWait(50, MILLISECONDS));
this.overwriteDefaultRetryerStrategySetup = overwriteDefaultRetryerStrategySetup;
+ this.retryWithTraceOnFailure = cfg.getBoolean("retry", "retryWithTraceOnFailure", false);
}
public Duration getDefaultTimeout(ActionType actionType) {
@@ -256,8 +269,35 @@
Predicate<Throwable> exceptionPredicate)
throws Throwable {
MetricListener listener = new MetricListener();
- try {
- RetryerBuilder<T> retryerBuilder = createRetryerBuilder(actionType, opts, exceptionPredicate);
+ try (TraceContext traceContext = TraceContext.open()) {
+ RetryerBuilder<T> retryerBuilder =
+ createRetryerBuilder(
+ actionType,
+ opts,
+ t -> {
+ // exceptionPredicate checks for temporary errors for which the operation should be
+ // retried (e.g. LockFailure). The retry has good chances to succeed.
+ if (exceptionPredicate.test(t)) {
+ return true;
+ }
+
+ // A non-recoverable failure occurred. Check if we should retry to capture a trace
+ // of the failure. If a trace was already done there is no need to retry.
+ if (retryWithTraceOnFailure
+ && opts.retryWithTrace().isPresent()
+ && opts.retryWithTrace().get().test(t)
+ && !traceContext.isTracing()) {
+ traceContext
+ .addTag(RequestId.Type.TRACE_ID, "retry-on-failure-" + new RequestId())
+ .forceLogging();
+ logger.atFine().withCause(t).log(
+ "%s failed, retry with tracing eanbled",
+ opts.caller().map(Class::getSimpleName).orElse("N/A"));
+ return true;
+ }
+
+ return false;
+ });
retryerBuilder.withRetryListener(listener);
return executeWithTimeoutCount(actionType, action, retryerBuilder.build());
} finally {
diff --git a/java/com/google/gerrit/server/update/RetryingRestCollectionModifyView.java b/java/com/google/gerrit/server/update/RetryingRestCollectionModifyView.java
index 7620386..9204565 100644
--- a/java/com/google/gerrit/server/update/RetryingRestCollectionModifyView.java
+++ b/java/com/google/gerrit/server/update/RetryingRestCollectionModifyView.java
@@ -17,6 +17,7 @@
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestCollectionModifyView;
import com.google.gerrit.extensions.restapi.RestResource;
@@ -32,7 +33,13 @@
@Override
public final O apply(P parentResource, I input)
throws AuthException, BadRequestException, ResourceConflictException, Exception {
- return retryHelper.execute((updateFactory) -> applyImpl(updateFactory, parentResource, input));
+ RetryHelper.Options retryOptions =
+ RetryHelper.options()
+ .caller(getClass())
+ .retryWithTrace(t -> !(t instanceof RestApiException))
+ .build();
+ return retryHelper.execute(
+ (updateFactory) -> applyImpl(updateFactory, parentResource, input), retryOptions);
}
protected abstract O applyImpl(BatchUpdate.Factory updateFactory, P parentResource, I input)
diff --git a/java/com/google/gerrit/server/update/RetryingRestModifyView.java b/java/com/google/gerrit/server/update/RetryingRestModifyView.java
index e2f4a02..b471d70 100644
--- a/java/com/google/gerrit/server/update/RetryingRestModifyView.java
+++ b/java/com/google/gerrit/server/update/RetryingRestModifyView.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.update;
+import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.RestResource;
@@ -27,7 +28,13 @@
@Override
public final O apply(R resource, I input) throws Exception {
- return retryHelper.execute((updateFactory) -> applyImpl(updateFactory, resource, input));
+ RetryHelper.Options retryOptions =
+ RetryHelper.options()
+ .caller(getClass())
+ .retryWithTrace(t -> !(t instanceof RestApiException))
+ .build();
+ return retryHelper.execute(
+ (updateFactory) -> applyImpl(updateFactory, resource, input), retryOptions);
}
protected abstract O applyImpl(BatchUpdate.Factory updateFactory, R resource, I input)
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index cc99a90..3d9b88d 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -2663,10 +2663,6 @@
return cds.get(0);
}
- private static void pushForReviewOk(TestRepository<?> testRepo) throws GitAPIException {
- pushForReview(testRepo, RemoteRefUpdate.Status.OK, null);
- }
-
private static void pushForReviewRejected(TestRepository<?> testRepo, String expectedMessage)
throws GitAPIException {
pushForReview(testRepo, RemoteRefUpdate.Status.REJECTED_OTHER_REASON, expectedMessage);
diff --git a/javatests/com/google/gerrit/acceptance/rest/TraceIT.java b/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
index 50d6dff..56a9b69 100644
--- a/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
@@ -16,6 +16,7 @@
import static com.google.common.truth.Truth.assertThat;
import static org.apache.http.HttpStatus.SC_CREATED;
+import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
import static org.apache.http.HttpStatus.SC_OK;
import com.google.auto.value.AutoValue;
@@ -28,6 +29,7 @@
import com.google.gerrit.acceptance.GerritConfig;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.events.ChangeIndexedListener;
import com.google.gerrit.extensions.registration.DynamicSet;
@@ -44,10 +46,14 @@
import com.google.gerrit.server.logging.PerformanceLogger;
import com.google.gerrit.server.logging.TraceContext;
import com.google.gerrit.server.project.CreateProjectArgs;
+import com.google.gerrit.server.project.SubmitRuleOptions;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.rules.SubmitRule;
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.Collection;
import java.util.List;
import java.util.SortedMap;
import java.util.SortedSet;
@@ -79,6 +85,7 @@
@Inject private DynamicSet<CommitValidationListener> commitValidationListeners;
@Inject private DynamicSet<ChangeIndexedListener> changeIndexedListeners;
@Inject private DynamicSet<PerformanceLogger> performanceLoggers;
+ @Inject private DynamicSet<SubmitRule> submitRules;
@Inject private WorkQueue workQueue;
private TraceValidatingProjectCreationValidationListener projectCreationListener;
@@ -367,6 +374,256 @@
assertThat(testPerformanceLogger.logEntries()).isEmpty();
}
+ @Test
+ @GerritConfig(name = "tracing.issue123.projectPattern", value = "new12")
+ public void traceProject() throws Exception {
+ RestResponse response = adminRestSession.put("/projects/new12");
+ assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+ assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+ assertThat(projectCreationListener.traceId).isEqualTo("issue123");
+ assertThat(projectCreationListener.isLoggingForced).isTrue();
+ assertThat(projectCreationListener.tags.get("project")).containsExactly("new12");
+ }
+
+ @Test
+ @GerritConfig(name = "tracing.issue123.projectPattern", value = "new.*")
+ public void traceProjectMatchRegEx() throws Exception {
+ RestResponse response = adminRestSession.put("/projects/new13");
+ assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+ assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+ assertThat(projectCreationListener.traceId).isEqualTo("issue123");
+ assertThat(projectCreationListener.isLoggingForced).isTrue();
+ assertThat(projectCreationListener.tags.get("project")).containsExactly("new13");
+ }
+
+ @Test
+ @GerritConfig(name = "tracing.issue123.projectPattern", value = "foo.*")
+ public void traceProjectNoMatch() throws Exception {
+ RestResponse response = adminRestSession.put("/projects/new13");
+ assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+ assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+ assertThat(projectCreationListener.traceId).isNull();
+ assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+ // The logging tag with the project name is also set if tracing is off.
+ assertThat(projectCreationListener.tags.get("project")).containsExactly("new13");
+ }
+
+ @Test
+ @GerritConfig(name = "tracing.issue123.projectPattern", value = "][")
+ public void traceProjectInvalidRegEx() throws Exception {
+ RestResponse response = adminRestSession.put("/projects/new14");
+ assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+ assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+ assertThat(projectCreationListener.traceId).isNull();
+ assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+ // The logging tag with the project name is also set if tracing is off.
+ assertThat(projectCreationListener.tags.get("project")).containsExactly("new14");
+ }
+
+ @Test
+ @GerritConfig(name = "tracing.issue123.account", value = "1000000")
+ public void traceAccount() throws Exception {
+ RestResponse response = adminRestSession.put("/projects/new15");
+ assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+ assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+ assertThat(projectCreationListener.traceId).isEqualTo("issue123");
+ assertThat(projectCreationListener.isLoggingForced).isTrue();
+ assertThat(projectCreationListener.tags.get("project")).containsExactly("new15");
+ }
+
+ @Test
+ @GerritConfig(name = "tracing.issue123.account", value = "1000001")
+ public void traceAccountNoMatch() throws Exception {
+ RestResponse response = adminRestSession.put("/projects/new16");
+ assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+ assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+ assertThat(projectCreationListener.traceId).isNull();
+ assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+ // The logging tag with the project name is also set if tracing is off.
+ assertThat(projectCreationListener.tags.get("project")).containsExactly("new16");
+ }
+
+ @Test
+ @GerritConfig(name = "tracing.issue123.account", value = "999")
+ public void traceAccountNotFound() throws Exception {
+ RestResponse response = adminRestSession.put("/projects/new17");
+ assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+ assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+ assertThat(projectCreationListener.traceId).isNull();
+ assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+ // The logging tag with the project name is also set if tracing is off.
+ assertThat(projectCreationListener.tags.get("project")).containsExactly("new17");
+ }
+
+ @Test
+ @GerritConfig(name = "tracing.issue123.account", value = "invalid")
+ public void traceAccountInvalidId() throws Exception {
+ RestResponse response = adminRestSession.put("/projects/new18");
+ assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+ assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+ assertThat(projectCreationListener.traceId).isNull();
+ assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+ // The logging tag with the project name is also set if tracing is off.
+ assertThat(projectCreationListener.tags.get("project")).containsExactly("new18");
+ }
+
+ @Test
+ @GerritConfig(name = "tracing.issue123.requestType", value = "REST")
+ public void traceRequestType() throws Exception {
+ RestResponse response = adminRestSession.put("/projects/new19");
+ assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+ assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+ assertThat(projectCreationListener.traceId).isEqualTo("issue123");
+ assertThat(projectCreationListener.isLoggingForced).isTrue();
+ assertThat(projectCreationListener.tags.get("project")).containsExactly("new19");
+ }
+
+ @Test
+ @GerritConfig(name = "tracing.issue123.requestType", value = "SSH")
+ public void traceRequestTypeNoMatch() throws Exception {
+ RestResponse response = adminRestSession.put("/projects/new20");
+ assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+ assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+ assertThat(projectCreationListener.traceId).isNull();
+ assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+ // The logging tag with the project name is also set if tracing is off.
+ assertThat(projectCreationListener.tags.get("project")).containsExactly("new20");
+ }
+
+ @Test
+ @GerritConfig(name = "tracing.issue123.requestType", value = "FOO")
+ public void traceProjectInvalidRequestType() throws Exception {
+ RestResponse response = adminRestSession.put("/projects/new21");
+ assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+ assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+ assertThat(projectCreationListener.traceId).isNull();
+ assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+ // The logging tag with the project name is also set if tracing is off.
+ assertThat(projectCreationListener.tags.get("project")).containsExactly("new21");
+ }
+
+ @Test
+ @GerritConfig(name = "tracing.issue123.account", value = "1000000")
+ @GerritConfig(name = "tracing.issue123.projectPattern", value = "new.*")
+ public void traceProjectForAccount() throws Exception {
+ RestResponse response = adminRestSession.put("/projects/new22");
+ assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+ assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+ assertThat(projectCreationListener.traceId).isEqualTo("issue123");
+ assertThat(projectCreationListener.isLoggingForced).isTrue();
+ assertThat(projectCreationListener.tags.get("project")).containsExactly("new22");
+ }
+
+ @Test
+ @GerritConfig(name = "tracing.issue123.account", value = "1000000")
+ @GerritConfig(name = "tracing.issue123.projectPattern", value = "foo.*")
+ public void traceProjectForAccountNoProjectMatch() throws Exception {
+ RestResponse response = adminRestSession.put("/projects/new23");
+ assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+ assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+ assertThat(projectCreationListener.traceId).isNull();
+ assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+ // The logging tag with the project name is also set if tracing is off.
+ assertThat(projectCreationListener.tags.get("project")).containsExactly("new23");
+ }
+
+ @Test
+ @GerritConfig(name = "tracing.issue123.account", value = "1000001")
+ @GerritConfig(name = "tracing.issue123.projectPattern", value = "new.*")
+ public void traceProjectForAccountNoAccountMatch() throws Exception {
+ RestResponse response = adminRestSession.put("/projects/new24");
+ assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+ assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+ assertThat(projectCreationListener.traceId).isNull();
+ assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+ // The logging tag with the project name is also set if tracing is off.
+ assertThat(projectCreationListener.tags.get("project")).containsExactly("new24");
+ }
+
+ @Test
+ @GerritConfig(name = "tracing.issue123.requestUriPattern", value = "/projects/.*")
+ public void traceRequestUri() throws Exception {
+ RestResponse response = adminRestSession.put("/projects/new23");
+ assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+ assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+ assertThat(projectCreationListener.traceId).isEqualTo("issue123");
+ assertThat(projectCreationListener.isLoggingForced).isTrue();
+ assertThat(projectCreationListener.tags.get("project")).containsExactly("new23");
+ }
+
+ @Test
+ @GerritConfig(name = "tracing.issue123.requestUriPattern", value = "/projects/.*/foo")
+ public void traceRequestUriNoMatch() throws Exception {
+ RestResponse response = adminRestSession.put("/projects/new23");
+ assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+ assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+ assertThat(projectCreationListener.traceId).isNull();
+ assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+ // The logging tag with the project name is also set if tracing is off.
+ assertThat(projectCreationListener.tags.get("project")).containsExactly("new23");
+ }
+
+ @Test
+ @GerritConfig(name = "tracing.issue123.requestUriPattern", value = "][")
+ public void traceRequestUriInvalidRegEx() throws Exception {
+ RestResponse response = adminRestSession.put("/projects/new24");
+ assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+ assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+ assertThat(projectCreationListener.traceId).isNull();
+ assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+ // The logging tag with the project name is also set if tracing is off.
+ assertThat(projectCreationListener.tags.get("project")).containsExactly("new24");
+ }
+
+ @GerritConfig(name = "retry.retryWithTraceOnFailure", value = "true")
+ public void autoRetryWithTrace() throws Exception {
+ String changeId = createChange().getChangeId();
+ approve(changeId);
+
+ TraceSubmitRule traceSubmitRule = new TraceSubmitRule();
+ traceSubmitRule.failOnce = true;
+ RegistrationHandle submitRuleRegistrationHandle = submitRules.add("gerrit", traceSubmitRule);
+ try {
+ RestResponse response = adminRestSession.post("/changes/" + changeId + "/submit");
+ assertThat(response.getStatusCode()).isEqualTo(SC_OK);
+ assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+ assertThat(traceSubmitRule.traceId).startsWith("retry-on-failure-");
+ assertThat(traceSubmitRule.isLoggingForced).isTrue();
+ } finally {
+ submitRuleRegistrationHandle.remove();
+ }
+ }
+
+ @Test
+ public void noAutoRetryWithTraceIfDisabled() throws Exception {
+ String changeId = createChange().getChangeId();
+ approve(changeId);
+
+ TraceSubmitRule traceSubmitRule = new TraceSubmitRule();
+ traceSubmitRule.failOnce = true;
+ RegistrationHandle submitRuleRegistrationHandle = submitRules.add("gerrit", traceSubmitRule);
+ try {
+ RestResponse response = adminRestSession.post("/changes/" + changeId + "/submit");
+ assertThat(response.getStatusCode()).isEqualTo(SC_INTERNAL_SERVER_ERROR);
+ assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+ assertThat(traceSubmitRule.traceId).isNull();
+ assertThat(traceSubmitRule.isLoggingForced).isNull();
+ } finally {
+ submitRuleRegistrationHandle.remove();
+ }
+ }
+
private void assertForceLogging(boolean expected) {
assertThat(LoggingContext.getInstance().shouldForceLogging(null, null, false))
.isEqualTo(expected);
@@ -417,6 +674,28 @@
public void onChangeDeleted(int id) {}
}
+ private static class TraceSubmitRule implements SubmitRule {
+ String traceId;
+ Boolean isLoggingForced;
+ boolean failOnce;
+
+ @Override
+ public Collection<SubmitRecord> evaluate(ChangeData changeData, SubmitRuleOptions options) {
+ if (failOnce) {
+ failOnce = false;
+ throw new IllegalStateException("forced failure from test");
+ }
+
+ this.traceId =
+ Iterables.getFirst(LoggingContext.getInstance().getTagsAsMap().get("TRACE_ID"), null);
+ this.isLoggingForced = LoggingContext.getInstance().shouldForceLogging(null, null, false);
+
+ SubmitRecord submitRecord = new SubmitRecord();
+ submitRecord.status = SubmitRecord.Status.OK;
+ return ImmutableList.of(submitRecord);
+ }
+ }
+
private static class TestPerformanceLogger implements PerformanceLogger {
private List<PerformanceLogEntry> logEntries = new ArrayList<>();
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index be3f2a0..c2df9ca 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -502,6 +502,81 @@
}
@Test
+ public void submitWholeTopicWithMultipleTopics() throws Throwable {
+ assume().that(isSubmitWholeTopicEnabled()).isTrue();
+ String topic1 = "test-topic-1";
+ String topic2 = "test-topic-2";
+ PushOneCommit.Result change1 = createChange("Change 1", "a.txt", "content", topic1);
+ PushOneCommit.Result change2 = createChange("Change 2", "b.txt", "content", topic1);
+ PushOneCommit.Result change3 = createChange("Change 3", "c.txt", "content", topic2);
+ PushOneCommit.Result change4 = createChange("Change 4", "d.txt", "content", topic2);
+ approve(change1.getChangeId());
+ approve(change2.getChangeId());
+ approve(change3.getChangeId());
+ approve(change4.getChangeId());
+ submit(change4.getChangeId());
+ String expectedTopic1 = name(topic1);
+ String expectedTopic2 = name(topic2);
+ if (getSubmitType() == SubmitType.CHERRY_PICK) {
+ change1.assertChange(Change.Status.NEW, expectedTopic1, admin);
+ change2.assertChange(Change.Status.NEW, expectedTopic1, admin);
+
+ } else {
+ change1.assertChange(Change.Status.MERGED, expectedTopic1, admin);
+ change2.assertChange(Change.Status.MERGED, expectedTopic1, admin);
+ }
+
+ // Check for the exact change to have the correct submitter.
+ assertSubmitter(change4);
+ // Also check submitters for changes submitted via the topic relationship.
+ assertSubmitter(change3);
+ if (getSubmitType() != SubmitType.CHERRY_PICK) {
+ assertSubmitter(change1);
+ assertSubmitter(change2);
+ }
+
+ // Check that the repo has the expected commits
+ List<RevCommit> log = getRemoteLog();
+ List<String> commitsInRepo = log.stream().map(RevCommit::getShortMessage).collect(toList());
+ int expectedCommitCount;
+ switch (getSubmitType()) {
+ case MERGE_ALWAYS:
+ // initial commit + 4 commits + merge commit
+ expectedCommitCount = 6;
+ break;
+ case CHERRY_PICK:
+ // initial commit + 2 commits
+ expectedCommitCount = 3;
+ break;
+ case FAST_FORWARD_ONLY:
+ case INHERIT:
+ case MERGE_IF_NECESSARY:
+ case REBASE_ALWAYS:
+ case REBASE_IF_NECESSARY:
+ default:
+ // initial commit + 4 commits
+ expectedCommitCount = 5;
+ break;
+ }
+ assertThat(log).hasSize(expectedCommitCount);
+
+ if (getSubmitType() == SubmitType.CHERRY_PICK) {
+ assertThat(commitsInRepo).containsAtLeast("Initial empty repository", "Change 3", "Change 4");
+ assertThat(commitsInRepo).doesNotContain("Change 1");
+ assertThat(commitsInRepo).doesNotContain("Change 2");
+ } else if (getSubmitType() == SubmitType.MERGE_ALWAYS) {
+ assertThat(commitsInRepo)
+ .contains(
+ String.format(
+ "Merge changes from topics \"%s\", \"%s\"", expectedTopic1, expectedTopic2));
+ } else {
+ assertThat(commitsInRepo)
+ .containsAtLeast(
+ "Initial empty repository", "Change 1", "Change 2", "Change 3", "Change 4");
+ }
+ }
+
+ @Test
public void submitReusingOldTopic() throws Throwable {
assume().that(isSubmitWholeTopicEnabled()).isTrue();
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
index babf886..37cb317 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
@@ -195,6 +195,9 @@
</template>
</ul>
<div class="rightItems">
+ <gr-endpoint-decorator
+ class="hideOnMobile"
+ name="header-small-banner"></gr-endpoint-decorator>
<gr-smart-search
id="search"
search-query="{{searchQuery}}"></gr-smart-search>