Merge "Set lineNum column background even when wrapping"
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index 410bf42..0caebfc 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -1342,6 +1342,7 @@
"date_format": "STD",
"time_format": "HHMM_12",
"size_bar_in_change_table": true,
+ "disable_keyboard_shortcuts": true,
"diff_view": "SIDE_BY_SIDE",
"mute_common_path_prefixes": true,
"my": [
@@ -1392,6 +1393,7 @@
"size_bar_in_change_table": true,
"diff_view": "SIDE_BY_SIDE",
"publish_comments_on_push": true,
+ "disable_keyboard_shortcuts": true,
"work_in_progress_by_default": true,
"mute_common_path_prefixes": true,
"my": [
@@ -2883,6 +2885,8 @@
The base which should be pre-selected in the 'Diff Against' drop-down
list when the change screen is opened for a merge commit.
Allowed values are `AUTO_MERGE` and `FIRST_PARENT`.
+|`disable_keyboard_shortcuts` |not set if `false`|
+Whether to disable all keyboard shortcuts.
|============================================
[[query-limit-info]]
diff --git a/java/com/google/gerrit/httpd/BUILD b/java/com/google/gerrit/httpd/BUILD
index cd3ebb9..ea7c609 100644
--- a/java/com/google/gerrit/httpd/BUILD
+++ b/java/com/google/gerrit/httpd/BUILD
@@ -20,6 +20,7 @@
"//java/com/google/gerrit/metrics",
"//java/com/google/gerrit/server",
"//java/com/google/gerrit/server/audit",
+ "//java/com/google/gerrit/server/cancellation",
"//java/com/google/gerrit/server/git/receive",
"//java/com/google/gerrit/server/ioutil",
"//java/com/google/gerrit/server/logging",
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 7e6ab58..375aa54 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -114,6 +114,7 @@
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.RequestStateProvider;
import com.google.gerrit.server.change.ChangeFinder;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.config.GerritServerConfig;
@@ -712,50 +713,45 @@
messageOr(e, "Quota limit reached"),
e.caching(),
e);
- } catch (RequestCancelledException e) {
- cause = Optional.of(e);
- switch (e.getCancellationReason()) {
- case CLIENT_CLOSED_REQUEST:
- statusCode = SC_CLIENT_CLOSED_REQUEST;
- break;
- case CLIENT_PROVIDED_DEADLINE_EXCEEDED:
- case SERVER_DEADLINE_EXCEEDED:
- statusCode = SC_REQUEST_TIMEOUT;
- break;
- }
-
- StringBuilder msg = new StringBuilder(e.formatCancellationReason());
- if (e.getCancellationMessage().isPresent()) {
- msg.append("\n\n");
- msg.append(e.getCancellationMessage().get());
- }
-
- responseBytes = replyError(req, res, statusCode, msg.toString(), e);
} catch (Exception e) {
cause = Optional.of(e);
- statusCode = SC_INTERNAL_SERVER_ERROR;
- Optional<ExceptionHook.Status> status = getStatus(e);
- statusCode = status.map(ExceptionHook.Status::statusCode).orElse(SC_INTERNAL_SERVER_ERROR);
-
- if (res.isCommitted()) {
- responseBytes = 0;
- if (statusCode == SC_INTERNAL_SERVER_ERROR) {
- logger.atSevere().withCause(e).log(
- "Error in %s %s, response already committed", req.getMethod(), uriForLogging(req));
- } else {
- logger.atWarning().log(
- "Response for %s %s already committed, wanted to set status %d",
- req.getMethod(), uriForLogging(req), statusCode);
- }
+ Optional<RequestCancelledException> requestCancelledException =
+ RequestCancelledException.getFromCausalChain(e);
+ if (requestCancelledException.isPresent()) {
+ statusCode =
+ getCancellationStatusCode(requestCancelledException.get().getCancellationReason());
+ responseBytes =
+ replyError(
+ req, res, statusCode, getCancellationMessage(requestCancelledException.get()), e);
} else {
- res.reset();
- traceContext.getTraceId().ifPresent(traceId -> res.addHeader(X_GERRIT_TRACE, traceId));
+ statusCode = SC_INTERNAL_SERVER_ERROR;
- if (status.isPresent()) {
- responseBytes = reply(req, res, e, status.get(), getUserMessages(traceContext, e));
+ Optional<ExceptionHook.Status> status = getStatus(e);
+ statusCode =
+ status.map(ExceptionHook.Status::statusCode).orElse(SC_INTERNAL_SERVER_ERROR);
+
+ if (res.isCommitted()) {
+ responseBytes = 0;
+ if (statusCode == SC_INTERNAL_SERVER_ERROR) {
+ logger.atSevere().withCause(e).log(
+ "Error in %s %s, response already committed",
+ req.getMethod(), uriForLogging(req));
+ } else {
+ logger.atWarning().log(
+ "Response for %s %s already committed, wanted to set status %d",
+ req.getMethod(), uriForLogging(req), statusCode);
+ }
} else {
- responseBytes = replyInternalServerError(req, res, e, getUserMessages(traceContext, e));
+ res.reset();
+ traceContext.getTraceId().ifPresent(traceId -> res.addHeader(X_GERRIT_TRACE, traceId));
+
+ if (status.isPresent()) {
+ responseBytes = reply(req, res, e, status.get(), getUserMessages(traceContext, e));
+ } else {
+ responseBytes =
+ replyInternalServerError(req, res, e, getUserMessages(traceContext, e));
+ }
}
}
} finally {
@@ -1961,6 +1957,28 @@
return replyBinaryResult(req, res, BinaryResult.create(text).setContentType(PLAIN_TEXT));
}
+ private static int getCancellationStatusCode(RequestStateProvider.Reason cancellationReason) {
+ switch (cancellationReason) {
+ case CLIENT_CLOSED_REQUEST:
+ return SC_CLIENT_CLOSED_REQUEST;
+ case CLIENT_PROVIDED_DEADLINE_EXCEEDED:
+ case SERVER_DEADLINE_EXCEEDED:
+ return SC_REQUEST_TIMEOUT;
+ }
+ logger.atSevere().log("Unexpected cancellation reason: %s", cancellationReason);
+ return SC_INTERNAL_SERVER_ERROR;
+ }
+
+ private static String getCancellationMessage(
+ RequestCancelledException requestCancelledException) {
+ StringBuilder msg = new StringBuilder(requestCancelledException.formatCancellationReason());
+ if (requestCancelledException.getCancellationMessage().isPresent()) {
+ msg.append("\n\n");
+ msg.append(requestCancelledException.getCancellationMessage().get());
+ }
+ return msg.toString();
+ }
+
private static boolean acceptsGzip(HttpServletRequest req) {
if (req != null) {
String accepts = req.getHeader(HttpHeaders.ACCEPT_ENCODING);
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java b/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
index 50a2f69..435a524 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
@@ -29,6 +29,7 @@
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.git.ObjectIds;
import com.google.gerrit.metrics.Counter0;
import com.google.gerrit.metrics.Description;
@@ -97,12 +98,17 @@
protected final ExternalIdCache externalIdCache;
protected final MetricMaker metricMaker;
protected final AllUsersName allUsersName;
+ protected final DynamicMap<ExternalIdUpsertPreprocessor> upsertPreprocessors;
protected ExternalIdNotesLoader(
- ExternalIdCache externalIdCache, MetricMaker metricMaker, AllUsersName allUsersName) {
+ ExternalIdCache externalIdCache,
+ MetricMaker metricMaker,
+ AllUsersName allUsersName,
+ DynamicMap<ExternalIdUpsertPreprocessor> upsertPreprocessors) {
this.externalIdCache = externalIdCache;
this.metricMaker = metricMaker;
this.allUsersName = allUsersName;
+ this.upsertPreprocessors = upsertPreprocessors;
}
/**
@@ -192,21 +198,24 @@
ExternalIdCache externalIdCache,
Provider<AccountIndexer> accountIndexer,
MetricMaker metricMaker,
- AllUsersName allUsersName) {
- super(externalIdCache, metricMaker, allUsersName);
+ AllUsersName allUsersName,
+ DynamicMap<ExternalIdUpsertPreprocessor> upsertPreprocessors) {
+ super(externalIdCache, metricMaker, allUsersName, upsertPreprocessors);
this.accountIndexer = accountIndexer;
}
@Override
public ExternalIdNotes load(Repository allUsersRepo)
throws IOException, ConfigInvalidException {
- return new ExternalIdNotes(metricMaker, allUsersName, allUsersRepo).load();
+ return new ExternalIdNotes(metricMaker, allUsersName, allUsersRepo, upsertPreprocessors)
+ .load();
}
@Override
public ExternalIdNotes load(Repository allUsersRepo, @Nullable ObjectId rev)
throws IOException, ConfigInvalidException {
- return new ExternalIdNotes(metricMaker, allUsersName, allUsersRepo).load(rev);
+ return new ExternalIdNotes(metricMaker, allUsersName, allUsersRepo, upsertPreprocessors)
+ .load(rev);
}
@Override
@@ -220,20 +229,27 @@
@Inject
FactoryNoReindex(
- ExternalIdCache externalIdCache, MetricMaker metricMaker, AllUsersName allUsersName) {
- super(externalIdCache, metricMaker, allUsersName);
+ ExternalIdCache externalIdCache,
+ MetricMaker metricMaker,
+ AllUsersName allUsersName,
+ DynamicMap<ExternalIdUpsertPreprocessor> upsertPreprocessors) {
+ super(externalIdCache, metricMaker, allUsersName, upsertPreprocessors);
}
@Override
public ExternalIdNotes load(Repository allUsersRepo)
throws IOException, ConfigInvalidException {
- return new ExternalIdNotes(metricMaker, allUsersName, allUsersRepo).setNoReindex().load();
+ return new ExternalIdNotes(metricMaker, allUsersName, allUsersRepo, upsertPreprocessors)
+ .setNoReindex()
+ .load();
}
@Override
public ExternalIdNotes load(Repository allUsersRepo, @Nullable ObjectId rev)
throws IOException, ConfigInvalidException {
- return new ExternalIdNotes(metricMaker, allUsersName, allUsersRepo).setNoReindex().load(rev);
+ return new ExternalIdNotes(metricMaker, allUsersName, allUsersRepo, upsertPreprocessors)
+ .setNoReindex()
+ .load(rev);
}
@Override
@@ -255,7 +271,8 @@
public static ExternalIdNotes loadReadOnly(
AllUsersName allUsersName, Repository allUsersRepo, @Nullable ObjectId rev)
throws IOException, ConfigInvalidException {
- return new ExternalIdNotes(new DisabledMetricMaker(), allUsersName, allUsersRepo)
+ return new ExternalIdNotes(
+ new DisabledMetricMaker(), allUsersName, allUsersRepo, DynamicMap.emptyMap())
.setReadOnly()
.setNoCacheUpdate()
.setNoReindex()
@@ -275,7 +292,8 @@
public static ExternalIdNotes loadNoCacheUpdate(
AllUsersName allUsersName, Repository allUsersRepo)
throws IOException, ConfigInvalidException {
- return new ExternalIdNotes(new DisabledMetricMaker(), allUsersName, allUsersRepo)
+ return new ExternalIdNotes(
+ new DisabledMetricMaker(), allUsersName, allUsersRepo, DynamicMap.emptyMap())
.setNoCacheUpdate()
.setNoReindex()
.load();
@@ -284,6 +302,7 @@
private final AllUsersName allUsersName;
private final Counter0 updateCount;
private final Repository repo;
+ private final DynamicMap<ExternalIdUpsertPreprocessor> upsertPreprocessors;
private final CallerFinder callerFinder;
private NoteMap noteMap;
@@ -312,13 +331,17 @@
private boolean noReindex = false;
private ExternalIdNotes(
- MetricMaker metricMaker, AllUsersName allUsersName, Repository allUsersRepo) {
+ MetricMaker metricMaker,
+ AllUsersName allUsersName,
+ Repository allUsersRepo,
+ DynamicMap<ExternalIdUpsertPreprocessor> upsertPreprocessors) {
this.updateCount =
metricMaker.newCounter(
"notedb/external_id_update_count",
new Description("Total number of external ID updates.").setRate().setUnit("updates"));
this.allUsersName = requireNonNull(allUsersName, "allUsersRepo");
this.repo = requireNonNull(allUsersRepo, "allUsersRepo");
+ this.upsertPreprocessors = upsertPreprocessors;
this.callerFinder =
CallerFinder.builder()
// 1. callers that come through ExternalIds
@@ -490,6 +513,7 @@
(rw, n) -> {
for (ExternalId extId : extIds) {
ExternalId insertedExtId = upsert(rw, inserter, noteMap, extId);
+ preprocessUpsert(insertedExtId);
newExtIds.add(insertedExtId);
}
});
@@ -519,6 +543,7 @@
(rw, n) -> {
for (ExternalId extId : extIds) {
ExternalId updatedExtId = upsert(rw, inserter, noteMap, extId);
+ preprocessUpsert(updatedExtId);
updatedExtIds.add(updatedExtId);
}
});
@@ -634,6 +659,7 @@
for (ExternalId extId : toAdd) {
ExternalId insertedExtId = upsert(rw, inserter, noteMap, extId);
+ preprocessUpsert(insertedExtId);
updatedExtIds.add(insertedExtId);
}
});
@@ -667,6 +693,7 @@
for (ExternalId extId : toAdd) {
ExternalId insertedExtId = upsert(rw, inserter, noteMap, extId);
+ preprocessUpsert(insertedExtId);
updatedExtIds.add(insertedExtId);
}
});
@@ -809,9 +836,9 @@
}
/**
- * Insert or updates an new external ID and sets it in the note map.
+ * Inserts or updates a new external ID and sets it in the note map.
*
- * <p>If the external ID already exists it is overwritten.
+ * <p>If the external ID already exists, it is overwritten.
*/
private static ExternalId upsert(
RevWalk rw, ObjectInserter ins, NoteMap noteMap, ExternalId extId)
@@ -867,6 +894,7 @@
* @return the external ID that was removed, {@code null} if no external ID with the specified key
* exists
*/
+ @Nullable
private static ExternalId remove(
RevWalk rw, NoteMap noteMap, ExternalId.Key extIdKey, Account.Id expectedAccountId)
throws IOException, ConfigInvalidException {
@@ -917,6 +945,10 @@
checkState(noteMap != null, "External IDs not loaded yet");
}
+ private void preprocessUpsert(ExternalId extId) {
+ upsertPreprocessors.forEach(p -> p.get().upsert(extId));
+ }
+
@FunctionalInterface
private interface NoteMapUpdate {
void execute(RevWalk rw, NoteMap noteMap)
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdUpsertPreprocessor.java b/java/com/google/gerrit/server/account/externalids/ExternalIdUpsertPreprocessor.java
new file mode 100644
index 0000000..c0697db
--- /dev/null
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdUpsertPreprocessor.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.account.externalids;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+/**
+ * This optional preprocessor is called in {@link ExternalIdNotes} before an update is committed.
+ */
+@ExtensionPoint
+public interface ExternalIdUpsertPreprocessor {
+ /**
+ * Called when inserting or updating an external ID. {@link ExternalId#blobId()} is set. The
+ * upsert can be blocked by throwing {@link com.google.gerrit.exceptions.StorageException}, e.g.
+ * when a precondition or preparatory work fails.
+ */
+ void upsert(ExternalId extId);
+}
diff --git a/java/com/google/gerrit/server/cancellation/BUILD b/java/com/google/gerrit/server/cancellation/BUILD
new file mode 100644
index 0000000..05530a5
--- /dev/null
+++ b/java/com/google/gerrit/server/cancellation/BUILD
@@ -0,0 +1,14 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
+java_library(
+ name = "cancellation",
+ srcs = glob(
+ ["*.java"],
+ ),
+ visibility = ["//visibility:public"],
+ deps = [
+ "//java/com/google/gerrit/common:annotations",
+ "//lib:guava",
+ "//lib/commons:lang",
+ ],
+)
diff --git a/java/com/google/gerrit/server/cancellation/RequestCancelledException.java b/java/com/google/gerrit/server/cancellation/RequestCancelledException.java
index 3c668fb..d89701f 100644
--- a/java/com/google/gerrit/server/cancellation/RequestCancelledException.java
+++ b/java/com/google/gerrit/server/cancellation/RequestCancelledException.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.cancellation;
+import com.google.common.base.Throwables;
import com.google.gerrit.common.Nullable;
import java.util.Optional;
import org.apache.commons.lang.WordUtils;
@@ -22,6 +23,17 @@
public class RequestCancelledException extends RuntimeException {
private static final long serialVersionUID = 1L;
+ /**
+ * Checks whether the given exception was caused by {@link RequestCancelledException}. If yes, the
+ * {@link RequestCancelledException} is returned. If not, {@link Optional#empty()} is returned.
+ */
+ public static Optional<RequestCancelledException> getFromCausalChain(Throwable e) {
+ return Throwables.getCausalChain(e).stream()
+ .filter(RequestCancelledException.class::isInstance)
+ .map(RequestCancelledException.class::cast)
+ .findFirst();
+ }
+
private final RequestStateProvider.Reason cancellationReason;
private final Optional<String> cancellationMessage;
diff --git a/java/com/google/gerrit/server/cancellation/RequestStateContext.java b/java/com/google/gerrit/server/cancellation/RequestStateContext.java
new file mode 100644
index 0000000..183d779
--- /dev/null
+++ b/java/com/google/gerrit/server/cancellation/RequestStateContext.java
@@ -0,0 +1,136 @@
+// 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.cancellation;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableSet;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Context that allows to register {@link RequestStateProvider}s.
+ *
+ * <p>The registered {@link RequestStateProvider}s are stored in {@link ThreadLocal} so that they
+ * can be accessed during the request execution (via {@link #getRequestStateProviders()}.
+ *
+ * <p>On {@link #close()} the {@link RequestStateProvider}s that have been registered by this {@code
+ * RequestStateContext} instance are removed from {@link ThreadLocal}.
+ *
+ * <p>Nesting {@code RequestStateContext}s is possible.
+ *
+ * <p>Currently there is no logic to automatically copy the {@link RequestStateContext} to
+ * background threads, but implementing this may be considered in the future. This means that by
+ * default we only support cancellation of the main thread, but not of background threads. That's
+ * fine as all significant work is being done in the main thread.
+ *
+ * <p>{@link com.google.gerrit.server.util.RequestContext} is also a context that is available for
+ * the time of the request, but it is not suitable to manage registrations of {@link
+ * RequestStateProvider}s. Hence {@link RequestStateProvider} registrations are managed by a
+ * separate context, which is this class, {@link RequestStateContext}:
+ *
+ * <ul>
+ * <li>{@link com.google.gerrit.server.util.RequestContext} is an interface that has many
+ * implementations and hence cannot manage a {@link ThreadLocal} state.
+ * <li>{@link com.google.gerrit.server.util.RequestContext} is not an {@link AutoCloseable} and
+ * hence cannot cleanup any {@link ThreadLocal} state on close (turning it into an {@link
+ * AutoCloseable} would require a large refactoring).
+ * <li>Despite the name {@link com.google.gerrit.server.util.RequestContext} is not only used for
+ * requests scopes but also for other scopes that are not a request (e.g. plugin invocations,
+ * email sending, manual scopes).
+ * <li>{@link com.google.gerrit.server.util.RequestContext} is not copied to background and should
+ * not be, but for {@link RequestStateContext} we may consider doing this in the future.
+ * </ul>
+ */
+public class RequestStateContext implements AutoCloseable {
+ /** The {@link RequestStateProvider}s that have been registered for the thread. */
+ private static final ThreadLocal<Set<RequestStateProvider>> threadLocalRequestStateProviders =
+ new ThreadLocal<>();
+
+ /**
+ * Aborts the current request by throwing a {@link RequestCancelledException} if any of the
+ * registered {@link RequestStateProvider}s reports the request as cancelled.
+ *
+ * @throws RequestCancelledException thrown if the current request is cancelled and should be
+ * aborted
+ */
+ public static void abortIfCancelled() throws RequestCancelledException {
+ getRequestStateProviders()
+ .forEach(
+ requestStateProvider ->
+ requestStateProvider.checkIfCancelled(
+ (reason, message) -> {
+ throw new RequestCancelledException(reason, message);
+ }));
+ }
+
+ /** Returns the {@link RequestStateProvider}s that have been registered for the thread. */
+ @VisibleForTesting
+ static ImmutableSet<RequestStateProvider> getRequestStateProviders() {
+ if (threadLocalRequestStateProviders.get() == null) {
+ return ImmutableSet.of();
+ }
+ return ImmutableSet.copyOf(threadLocalRequestStateProviders.get());
+ }
+
+ /** Opens a {@code RequestStateContext}. */
+ public static RequestStateContext open() {
+ return new RequestStateContext();
+ }
+
+ /**
+ * The {@link RequestStateProvider}s that have been registered by this {@code
+ * RequestStateContext}.
+ */
+ private Set<RequestStateProvider> requestStateProviders = new HashSet<>();
+
+ private RequestStateContext() {}
+
+ /**
+ * Registers a {@link RequestStateProvider}.
+ *
+ * @param requestStateProvider the {@link RequestStateProvider} that should be registered
+ * @return the {@code RequestStateContext} instance for chaining calls
+ */
+ public RequestStateContext addRequestStateProvider(RequestStateProvider requestStateProvider) {
+ if (threadLocalRequestStateProviders.get() == null) {
+ threadLocalRequestStateProviders.set(new HashSet<>());
+ }
+ if (threadLocalRequestStateProviders.get().add(requestStateProvider)) {
+ requestStateProviders.add(requestStateProvider);
+ }
+ return this;
+ }
+
+ /**
+ * Closes this {@code RequestStateContext}.
+ *
+ * <p>Ensures that all {@link RequestStateProvider}s that have been registered by this {@code
+ * RequestStateContext} instance are removed from {@link #threadLocalRequestStateProviders}.
+ *
+ * <p>If no {@link RequestStateProvider}s remain in {@link #threadLocalRequestStateProviders},
+ * {@link #threadLocalRequestStateProviders} is unset.
+ */
+ @Override
+ public void close() {
+ if (threadLocalRequestStateProviders.get() != null) {
+ requestStateProviders.forEach(
+ requestStateProvider ->
+ threadLocalRequestStateProviders.get().remove(requestStateProvider));
+ if (threadLocalRequestStateProviders.get().isEmpty()) {
+ threadLocalRequestStateProviders.remove();
+ }
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/cancellation/RequestStateProvider.java b/java/com/google/gerrit/server/cancellation/RequestStateProvider.java
index e1716eb..683ca1d 100644
--- a/java/com/google/gerrit/server/cancellation/RequestStateProvider.java
+++ b/java/com/google/gerrit/server/cancellation/RequestStateProvider.java
@@ -31,6 +31,7 @@
void checkIfCancelled(OnCancelled onCancelled);
/** Callback interface to be invoked if a request is cancelled. */
+ @FunctionalInterface
interface OnCancelled {
/**
* Callback that is invoked if the request is cancelled.
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 076ba46..201f122 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -100,6 +100,7 @@
import com.google.gerrit.server.account.ServiceUserClassifierImpl;
import com.google.gerrit.server.account.VersionedAuthorizedKeys;
import com.google.gerrit.server.account.externalids.ExternalIdModule;
+import com.google.gerrit.server.account.externalids.ExternalIdUpsertPreprocessor;
import com.google.gerrit.server.approval.ApprovalCacheImpl;
import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.auth.AuthBackend;
@@ -166,6 +167,7 @@
import com.google.gerrit.server.mime.FileTypeRegistry;
import com.google.gerrit.server.mime.MimeUtilFileTypeRegistry;
import com.google.gerrit.server.notedb.NoteDbModule;
+import com.google.gerrit.server.notedb.StoreSubmitRequirementsOp;
import com.google.gerrit.server.patch.DiffOperationsImpl;
import com.google.gerrit.server.patch.PatchListCacheImpl;
import com.google.gerrit.server.patch.PatchScriptFactory;
@@ -466,6 +468,7 @@
factory(MergedByPushOp.Factory.class);
factory(GitModules.Factory.class);
factory(VersionedAuthorizedKeys.Factory.class);
+ factory(StoreSubmitRequirementsOp.Factory.class);
bind(AccountManager.class);
bind(SubscriptionGraph.Factory.class).to(ConfiguredSubscriptionGraphFactory.class);
@@ -476,5 +479,6 @@
bind(ReloadPluginListener.class)
.annotatedWith(UniqueAnnotations.create())
.to(PluginConfigFactory.class);
+ DynamicMap.mapOf(binder(), ExternalIdUpsertPreprocessor.class);
}
}
diff --git a/java/com/google/gerrit/server/git/MultiProgressMonitor.java b/java/com/google/gerrit/server/git/MultiProgressMonitor.java
index 2d854a5..1122551 100644
--- a/java/com/google/gerrit/server/git/MultiProgressMonitor.java
+++ b/java/com/google/gerrit/server/git/MultiProgressMonitor.java
@@ -131,7 +131,7 @@
private int spinnerIndex;
private char spinnerState = NO_SPINNER;
private boolean done;
- private boolean write = true;
+ private boolean clientDisconnected;
private final long maxIntervalNanos;
@@ -343,14 +343,14 @@
}
private void send(StringBuilder s) {
- if (write) {
+ if (!clientDisconnected) {
try {
out.write(Constants.encode(s.toString()));
out.flush();
} catch (IOException e) {
logger.atWarning().withCause(e).log(
"Sending progress to client failed. Stop sending updates for task %s", taskName);
- write = false;
+ clientDisconnected = true;
}
}
}
diff --git a/java/com/google/gerrit/server/git/receive/BUILD b/java/com/google/gerrit/server/git/receive/BUILD
index b59d431..5c1cf52 100644
--- a/java/com/google/gerrit/server/git/receive/BUILD
+++ b/java/com/google/gerrit/server/git/receive/BUILD
@@ -18,6 +18,7 @@
"//java/com/google/gerrit/index",
"//java/com/google/gerrit/metrics",
"//java/com/google/gerrit/server",
+ "//java/com/google/gerrit/server/cancellation",
"//java/com/google/gerrit/server/logging",
"//java/com/google/gerrit/server/restapi",
"//java/com/google/gerrit/server/util/time",
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index d074f1e..29c2698 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -46,6 +46,7 @@
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
+import com.google.common.base.Throwables;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.ImmutableList;
@@ -645,10 +646,18 @@
try {
processCommandsUnsafe(commands, progress);
rejectRemaining(commands, INTERNAL_SERVER_ERROR);
- } catch (RequestCancelledException e) {
- StringBuilder msg = new StringBuilder(e.formatCancellationReason());
- if (e.getCancellationMessage().isPresent()) {
- msg.append(String.format(" (%s)", e.getCancellationMessage().get()));
+ } catch (RuntimeException e) {
+ Optional<RequestCancelledException> requestCancelledException =
+ RequestCancelledException.getFromCausalChain(e);
+ if (!requestCancelledException.isPresent()) {
+ Throwables.throwIfUnchecked(e);
+ }
+ StringBuilder msg =
+ new StringBuilder(requestCancelledException.get().formatCancellationReason());
+ if (requestCancelledException.get().getCancellationMessage().isPresent()) {
+ msg.append(
+ String.format(
+ " (%s)", requestCancelledException.get().getCancellationMessage().get()));
}
rejectRemaining(commands, msg.toString());
}
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index 1ee12fe..5caceef 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -70,6 +70,7 @@
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.index.change.StalenessChecker.RefStatePattern;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
+import com.google.gerrit.server.notedb.SubmitRequirementProtoConverter;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
@@ -1078,6 +1079,27 @@
return result;
}
+ /** Serialized submit requirements, used for pre-populating results. */
+ public static final FieldDef<ChangeData, Iterable<byte[]>> STORED_SUBMIT_REQUIREMENTS =
+ storedOnly("full_submit_requirements")
+ .buildRepeatable(
+ cd ->
+ toProtos(
+ SubmitRequirementProtoConverter.INSTANCE, cd.submitRequirements().values()),
+ (cd, field) -> parseSubmitRequirements(field, cd));
+
+ private static void parseSubmitRequirements(Iterable<byte[]> values, ChangeData out) {
+ out.setSubmitRequirements(
+ StreamSupport.stream(values.spliterator(), false)
+ .map(
+ f ->
+ SubmitRequirementProtoConverter.INSTANCE.fromProto(
+ Protos.parseUnchecked(
+ SubmitRequirementProtoConverter.INSTANCE.getParser(), f)))
+ .collect(
+ ImmutableMap.toImmutableMap(sr -> sr.submitRequirement(), Function.identity())));
+ }
+
/**
* All values of all refs that were used in the course of indexing this document.
*
diff --git a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index 879da4f..bee0c35 100644
--- a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -148,10 +148,14 @@
* The computation of the {@link ChangeField#DIRECTORY} field is changed, hence reindexing is
* required.
*/
- static final Schema<ChangeData> V63 = schema(V62, false);
+ @Deprecated static final Schema<ChangeData> V63 = schema(V62, false);
/** Added support for MIN/MAX/ANY for {@link ChangeField#LABEL} */
- static final Schema<ChangeData> V64 = schema(V63, false);
+ @Deprecated static final Schema<ChangeData> V64 = schema(V63, false);
+
+ /** Added new field for submit requirements. */
+ static final Schema<ChangeData> V65 =
+ new Schema.Builder<ChangeData>().add(V64).add(ChangeField.STORED_SUBMIT_REQUIREMENTS).build();
/**
* Name of the change index to be used when contacting index backends or loading configurations.
diff --git a/java/com/google/gerrit/server/logging/BUILD b/java/com/google/gerrit/server/logging/BUILD
index c60af0d..ee0168c 100644
--- a/java/com/google/gerrit/server/logging/BUILD
+++ b/java/com/google/gerrit/server/logging/BUILD
@@ -9,6 +9,7 @@
deps = [
"//java/com/google/gerrit/common:annotations",
"//java/com/google/gerrit/extensions:api",
+ "//java/com/google/gerrit/server/cancellation",
"//java/com/google/gerrit/server/util/time",
"//lib:gson",
"//lib:guava",
diff --git a/java/com/google/gerrit/server/logging/TraceContext.java b/java/com/google/gerrit/server/logging/TraceContext.java
index 2fc19b5..681dfbc 100644
--- a/java/com/google/gerrit/server/logging/TraceContext.java
+++ b/java/com/google/gerrit/server/logging/TraceContext.java
@@ -24,6 +24,7 @@
import com.google.common.collect.Table;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
+import com.google.gerrit.server.cancellation.RequestStateContext;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
@@ -208,6 +209,7 @@
}
private TraceTimer(Runnable startLogFn, Consumer<Long> doneLogFn) {
+ RequestStateContext.abortIfCancelled();
startLogFn.run();
this.doneLogFn = doneLogFn;
this.stopwatch = Stopwatch.createStarted();
@@ -217,6 +219,7 @@
public void close() {
stopwatch.stop();
doneLogFn.accept(stopwatch.elapsed(TimeUnit.MILLISECONDS));
+ RequestStateContext.abortIfCancelled();
}
}
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesState.java b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
index e7da025..4d6b9cf 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesState.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
@@ -61,14 +61,10 @@
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerByEmailSetEntryProto;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerSetEntryProto;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerStatusUpdateProto;
-import com.google.gerrit.server.cache.proto.Cache.SubmitRequirementResultProto;
import com.google.gerrit.server.cache.serialize.CacheSerializer;
import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
-import com.google.gerrit.server.cache.serialize.entities.SubmitRequirementExpressionResultSerializer;
-import com.google.gerrit.server.cache.serialize.entities.SubmitRequirementSerializer;
import com.google.gerrit.server.index.change.ChangeField.StoredSubmitRecord;
import com.google.gson.Gson;
-import com.google.protobuf.Descriptors.FieldDescriptor;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.List;
@@ -478,11 +474,6 @@
private static final Converter<String, ReviewerStateInternal> REVIEWER_STATE_CONVERTER =
Enums.stringConverter(ReviewerStateInternal.class);
- private static final FieldDescriptor SR_APPLICABILITY_EXPR_RESULT_FIELD =
- SubmitRequirementResultProto.getDescriptor().findFieldByNumber(2);
- private static final FieldDescriptor SR_OVERRIDE_EXPR_RESULT_FIELD =
- SubmitRequirementResultProto.getDescriptor().findFieldByNumber(4);
-
@Override
public byte[] serialize(ChangeNotesState object) {
checkArgument(object.metaId() != null, "meta ID is required in: %s", object);
@@ -539,7 +530,10 @@
object.publishedComments().values().forEach(c -> b.addPublishedComment(GSON.toJson(c)));
object
.submitRequirementsResult()
- .forEach(sr -> b.addSubmitRequirementResult(toSubmitRequirementResultProto(sr)));
+ .forEach(
+ sr ->
+ b.addSubmitRequirementResult(
+ SubmitRequirementProtoConverter.INSTANCE.toProto(sr)));
b.setUpdateCount(object.updateCount());
if (object.mergedOn() != null) {
b.setMergedOnMillis(object.mergedOn().getTime());
@@ -634,53 +628,6 @@
return builder.build();
}
- private static SubmitRequirementResultProto toSubmitRequirementResultProto(
- SubmitRequirementResult r) {
- SubmitRequirementResultProto.Builder builder = SubmitRequirementResultProto.newBuilder();
- builder
- .setSubmitRequirement(SubmitRequirementSerializer.serialize(r.submitRequirement()))
- .setCommit(ObjectIdConverter.create().toByteString(r.patchSetCommitId()));
- if (r.applicabilityExpressionResult().isPresent()) {
- builder.setApplicabilityExpressionResult(
- SubmitRequirementExpressionResultSerializer.serialize(
- r.applicabilityExpressionResult().get()));
- }
- builder.setSubmittabilityExpressionResult(
- SubmitRequirementExpressionResultSerializer.serialize(
- r.submittabilityExpressionResult()));
- if (r.overrideExpressionResult().isPresent()) {
- builder.setOverrideExpressionResult(
- SubmitRequirementExpressionResultSerializer.serialize(
- r.overrideExpressionResult().get()));
- }
- return builder.build();
- }
-
- private static SubmitRequirementResult toSubmitRequirementResult(
- SubmitRequirementResultProto proto) {
- SubmitRequirementResult.Builder builder =
- SubmitRequirementResult.builder()
- .patchSetCommitId(ObjectIdConverter.create().fromByteString(proto.getCommit()))
- .submitRequirement(
- SubmitRequirementSerializer.deserialize(proto.getSubmitRequirement()));
- if (proto.hasField(SR_APPLICABILITY_EXPR_RESULT_FIELD)) {
- builder.applicabilityExpressionResult(
- Optional.of(
- SubmitRequirementExpressionResultSerializer.deserialize(
- proto.getApplicabilityExpressionResult())));
- }
- builder.submittabilityExpressionResult(
- SubmitRequirementExpressionResultSerializer.deserialize(
- proto.getSubmittabilityExpressionResult()));
- if (proto.hasField(SR_OVERRIDE_EXPR_RESULT_FIELD)) {
- builder.overrideExpressionResult(
- Optional.of(
- SubmitRequirementExpressionResultSerializer.deserialize(
- proto.getOverrideExpressionResult())));
- }
- return builder.build();
- }
-
@Override
public ChangeNotesState deserialize(byte[] in) {
ChangeNotesStateProto proto = Protos.parseUnchecked(ChangeNotesStateProto.parser(), in);
@@ -728,7 +675,7 @@
.collect(toImmutableListMultimap(HumanComment::getCommitId, c -> c)))
.submitRequirementsResult(
proto.getSubmitRequirementResultList().stream()
- .map(sr -> toSubmitRequirementResult(sr))
+ .map(sr -> SubmitRequirementProtoConverter.INSTANCE.fromProto(sr))
.collect(toImmutableList()))
.updateCount(proto.getUpdateCount())
.mergedOn(proto.getHasMergedOn() ? new Timestamp(proto.getMergedOnMillis()) : null);
diff --git a/java/com/google/gerrit/server/notedb/StoreSubmitRequirementsOp.java b/java/com/google/gerrit/server/notedb/StoreSubmitRequirementsOp.java
index 47948d7..4d99f93 100644
--- a/java/com/google/gerrit/server/notedb/StoreSubmitRequirementsOp.java
+++ b/java/com/google/gerrit/server/notedb/StoreSubmitRequirementsOp.java
@@ -15,16 +15,26 @@
package com.google.gerrit.server.notedb;
import com.google.gerrit.entities.Change;
+import com.google.gerrit.server.project.SubmitRequirementsEvaluator;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
+import com.google.inject.Inject;
/** A {@link BatchUpdateOp} that stores the evaluated submit requirements of a change in NoteDb. */
public class StoreSubmitRequirementsOp implements BatchUpdateOp {
private final ChangeData.Factory changeDataFactory;
+ private final SubmitRequirementsEvaluator evaluator;
- public StoreSubmitRequirementsOp(ChangeData.Factory changeDataFactory) {
+ public interface Factory {
+ StoreSubmitRequirementsOp create();
+ }
+
+ @Inject
+ public StoreSubmitRequirementsOp(
+ ChangeData.Factory changeDataFactory, SubmitRequirementsEvaluator evaluator) {
this.changeDataFactory = changeDataFactory;
+ this.evaluator = evaluator;
}
@Override
@@ -32,7 +42,7 @@
Change change = ctx.getChange();
ChangeData changeData = changeDataFactory.create(change);
ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId());
- update.putSubmitRequirementResults(changeData.submitRequirements().values());
+ update.putSubmitRequirementResults(evaluator.getResults(changeData).values());
return !changeData.submitRequirements().isEmpty();
}
}
diff --git a/java/com/google/gerrit/server/notedb/SubmitRequirementProtoConverter.java b/java/com/google/gerrit/server/notedb/SubmitRequirementProtoConverter.java
new file mode 100644
index 0000000..416b850
--- /dev/null
+++ b/java/com/google/gerrit/server/notedb/SubmitRequirementProtoConverter.java
@@ -0,0 +1,88 @@
+// 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.notedb;
+
+import com.google.errorprone.annotations.Immutable;
+import com.google.gerrit.entities.SubmitRequirementResult;
+import com.google.gerrit.entities.converter.ProtoConverter;
+import com.google.gerrit.server.cache.proto.Cache.SubmitRequirementResultProto;
+import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
+import com.google.gerrit.server.cache.serialize.entities.SubmitRequirementExpressionResultSerializer;
+import com.google.gerrit.server.cache.serialize.entities.SubmitRequirementSerializer;
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.Parser;
+import java.util.Optional;
+
+@Immutable
+public enum SubmitRequirementProtoConverter
+ implements ProtoConverter<SubmitRequirementResultProto, SubmitRequirementResult> {
+ INSTANCE;
+
+ private static final FieldDescriptor SR_APPLICABILITY_EXPR_RESULT_FIELD =
+ SubmitRequirementResultProto.getDescriptor().findFieldByNumber(2);
+ private static final FieldDescriptor SR_OVERRIDE_EXPR_RESULT_FIELD =
+ SubmitRequirementResultProto.getDescriptor().findFieldByNumber(4);
+
+ @Override
+ public SubmitRequirementResultProto toProto(SubmitRequirementResult r) {
+ SubmitRequirementResultProto.Builder builder = SubmitRequirementResultProto.newBuilder();
+ builder
+ .setSubmitRequirement(SubmitRequirementSerializer.serialize(r.submitRequirement()))
+ .setCommit(ObjectIdConverter.create().toByteString(r.patchSetCommitId()));
+ if (r.applicabilityExpressionResult().isPresent()) {
+ builder.setApplicabilityExpressionResult(
+ SubmitRequirementExpressionResultSerializer.serialize(
+ r.applicabilityExpressionResult().get()));
+ }
+ builder.setSubmittabilityExpressionResult(
+ SubmitRequirementExpressionResultSerializer.serialize(r.submittabilityExpressionResult()));
+ if (r.overrideExpressionResult().isPresent()) {
+ builder.setOverrideExpressionResult(
+ SubmitRequirementExpressionResultSerializer.serialize(
+ r.overrideExpressionResult().get()));
+ }
+ return builder.build();
+ }
+
+ @Override
+ public SubmitRequirementResult fromProto(SubmitRequirementResultProto proto) {
+ SubmitRequirementResult.Builder builder =
+ SubmitRequirementResult.builder()
+ .patchSetCommitId(ObjectIdConverter.create().fromByteString(proto.getCommit()))
+ .submitRequirement(
+ SubmitRequirementSerializer.deserialize(proto.getSubmitRequirement()));
+ if (proto.hasField(SR_APPLICABILITY_EXPR_RESULT_FIELD)) {
+ builder.applicabilityExpressionResult(
+ Optional.of(
+ SubmitRequirementExpressionResultSerializer.deserialize(
+ proto.getApplicabilityExpressionResult())));
+ }
+ builder.submittabilityExpressionResult(
+ SubmitRequirementExpressionResultSerializer.deserialize(
+ proto.getSubmittabilityExpressionResult()));
+ if (proto.hasField(SR_OVERRIDE_EXPR_RESULT_FIELD)) {
+ builder.overrideExpressionResult(
+ Optional.of(
+ SubmitRequirementExpressionResultSerializer.deserialize(
+ proto.getOverrideExpressionResult())));
+ }
+ return builder.build();
+ }
+
+ @Override
+ public Parser<SubmitRequirementResultProto> getParser() {
+ return SubmitRequirementResultProto.parser();
+ }
+}
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluator.java b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluator.java
index 65d9d9e..e66b974 100644
--- a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluator.java
+++ b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluator.java
@@ -14,6 +14,9 @@
package com.google.gerrit.server.project;
+import static com.google.gerrit.server.project.ProjectCache.illegalState;
+
+import com.google.common.collect.ImmutableMap;
import com.google.gerrit.entities.SubmitRequirement;
import com.google.gerrit.entities.SubmitRequirementExpression;
import com.google.gerrit.entities.SubmitRequirementExpressionResult;
@@ -26,16 +29,20 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
+import java.util.Map;
import java.util.Optional;
/** Evaluates submit requirements for different change data. */
@Singleton
public class SubmitRequirementsEvaluator {
private final Provider<ChangeQueryBuilder> changeQueryBuilderProvider;
+ private final ProjectCache projectCache;
@Inject
- private SubmitRequirementsEvaluator(Provider<ChangeQueryBuilder> changeQueryBuilderProvider) {
+ private SubmitRequirementsEvaluator(
+ Provider<ChangeQueryBuilder> changeQueryBuilderProvider, ProjectCache projectCache) {
this.changeQueryBuilderProvider = changeQueryBuilderProvider;
+ this.projectCache = projectCache;
}
/**
@@ -50,6 +57,21 @@
changeQueryBuilderProvider.get().parse(expression.expressionString());
}
+ /**
+ * Evaluate and return all submit requirements for a change. Submit requirements are retrieved for
+ * the project containing the change and parent projects as well.
+ */
+ public Map<SubmitRequirement, SubmitRequirementResult> getResults(ChangeData cd) {
+ ProjectState state = projectCache.get(cd.project()).orElseThrow(illegalState(cd.project()));
+ Map<String, SubmitRequirement> requirements = state.getSubmitRequirements();
+ ImmutableMap.Builder<SubmitRequirement, SubmitRequirementResult> result =
+ ImmutableMap.builderWithExpectedSize(requirements.size());
+ for (SubmitRequirement requirement : requirements.values()) {
+ result.put(requirement, evaluate(requirement, cd));
+ }
+ return result.build();
+ }
+
/** Evaluate a {@link SubmitRequirement} on a given {@link ChangeData}. */
public SubmitRequirementResult evaluate(SubmitRequirement sr, ChangeData cd) {
SubmitRequirementExpressionResult blockingResult =
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index f912250..852387f 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -106,6 +106,7 @@
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
+import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
@@ -932,21 +933,36 @@
return messages;
}
- /** Get all submit requirements for this change, including those from parent projects. */
+ /**
+ * Get all evaluated submit requirements for this change, including those from parent projects.
+ * For closed changes, submit requirements are read from the change notes. For active changes,
+ * submit requirements are evaluated online.
+ *
+ * <p>For changes loaded from the index, the value will be set from index field {@link
+ * com.google.gerrit.server.index.change.ChangeField#STORED_SUBMIT_REQUIREMENTS}.
+ */
public Map<SubmitRequirement, SubmitRequirementResult> submitRequirements() {
if (submitRequirements == null) {
- ProjectState state = projectCache.get(project()).orElseThrow(illegalState(project()));
- Map<String, SubmitRequirement> requirements = state.getSubmitRequirements();
- ImmutableMap.Builder<SubmitRequirement, SubmitRequirementResult> result =
- ImmutableMap.builderWithExpectedSize(requirements.size());
- for (SubmitRequirement requirement : requirements.values()) {
- result.put(requirement, submitRequirementsEvaluator.evaluate(requirement, this));
+ if (!lazyload()) {
+ return Collections.emptyMap();
}
- submitRequirements = result.build();
+ Change c = change();
+ if (c != null && c.isClosed()) {
+ submitRequirements =
+ notes().getSubmitRequirementsResult().stream()
+ .collect(Collectors.toMap(r -> r.submitRequirement(), Function.identity()));
+ } else {
+ submitRequirements = submitRequirementsEvaluator.getResults(this);
+ }
}
return submitRequirements;
}
+ public void setSubmitRequirements(
+ Map<SubmitRequirement, SubmitRequirementResult> submitRequirements) {
+ this.submitRequirements = submitRequirements;
+ }
+
public List<SubmitRecord> submitRecords(SubmitRuleOptions options) {
// If the change is not submitted yet, 'strict' and 'lenient' both have the same result. If the
// change is submitted, SubmitRecord requested with 'strict' will contain just a single entry
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index 363cdca..942f024 100644
--- a/java/com/google/gerrit/server/submit/MergeOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -241,6 +241,7 @@
private final NotifyResolver notifyResolver;
private final RetryHelper retryHelper;
private final ChangeData.Factory changeDataFactory;
+ private final StoreSubmitRequirementsOp.Factory storeSubmitRequirementsOpFactory;
// Changes that were updated by this MergeOp.
private final Map<Change.Id, Change> updatedChanges;
@@ -274,7 +275,8 @@
NotifyResolver notifyResolver,
TopicMetrics topicMetrics,
RetryHelper retryHelper,
- ChangeData.Factory changeDataFactory) {
+ ChangeData.Factory changeDataFactory,
+ StoreSubmitRequirementsOp.Factory storeSubmitRequirementsOpFactory) {
this.cmUtil = cmUtil;
this.batchUpdateFactory = batchUpdateFactory;
this.internalUserFactory = internalUserFactory;
@@ -291,6 +293,7 @@
this.topicMetrics = topicMetrics;
this.changeDataFactory = changeDataFactory;
this.updatedChanges = new HashMap<>();
+ this.storeSubmitRequirementsOpFactory = storeSubmitRequirementsOpFactory;
}
@Override
@@ -665,7 +668,7 @@
Change.Id changeId = entry.getKey();
batchUpdatesByProject
.get(project)
- .addOp(changeId, new StoreSubmitRequirementsOp(changeDataFactory));
+ .addOp(changeId, storeSubmitRequirementsOpFactory.create());
}
try {
submissionExecutor.setAdditionalBatchUpdateListeners(
diff --git a/java/com/google/gerrit/sshd/BUILD b/java/com/google/gerrit/sshd/BUILD
index 0668c1e..f3bd5e1 100644
--- a/java/com/google/gerrit/sshd/BUILD
+++ b/java/com/google/gerrit/sshd/BUILD
@@ -17,6 +17,7 @@
"//java/com/google/gerrit/metrics",
"//java/com/google/gerrit/server",
"//java/com/google/gerrit/server/audit",
+ "//java/com/google/gerrit/server/cancellation",
"//java/com/google/gerrit/server/git/receive",
"//java/com/google/gerrit/server/ioutil",
"//java/com/google/gerrit/server/logging",
diff --git a/java/com/google/gerrit/sshd/SshCommand.java b/java/com/google/gerrit/sshd/SshCommand.java
index 93c6c2c..e58040f 100644
--- a/java/com/google/gerrit/sshd/SshCommand.java
+++ b/java/com/google/gerrit/sshd/SshCommand.java
@@ -14,6 +14,7 @@
package com.google.gerrit.sshd;
+import com.google.common.base.Throwables;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.server.AccessPath;
import com.google.gerrit.server.DynamicOptions;
@@ -28,6 +29,7 @@
import com.google.inject.Inject;
import java.io.IOException;
import java.io.PrintWriter;
+import java.util.Optional;
import org.apache.sshd.server.Environment;
import org.apache.sshd.server.channel.ChannelSession;
import org.eclipse.jgit.lib.Config;
@@ -62,10 +64,18 @@
RequestInfo.builder(RequestInfo.RequestType.SSH, user, traceContext).build();
requestListeners.runEach(l -> l.onRequest(requestInfo));
SshCommand.this.run();
- } catch (RequestCancelledException e) {
- StringBuilder msg = new StringBuilder(e.formatCancellationReason());
- if (e.getCancellationMessage().isPresent()) {
- msg.append(String.format(" (%s)", e.getCancellationMessage().get()));
+ } catch (RuntimeException e) {
+ Optional<RequestCancelledException> requestCancelledException =
+ RequestCancelledException.getFromCausalChain(e);
+ if (!requestCancelledException.isPresent()) {
+ Throwables.throwIfUnchecked(e);
+ }
+ StringBuilder msg =
+ new StringBuilder(requestCancelledException.get().formatCancellationReason());
+ if (requestCancelledException.get().getCancellationMessage().isPresent()) {
+ msg.append(
+ String.format(
+ " (%s)", requestCancelledException.get().getCancellationMessage().get()));
}
stderr.println(msg.toString());
} finally {
diff --git a/java/com/google/gerrit/testing/InMemoryModule.java b/java/com/google/gerrit/testing/InMemoryModule.java
index 06d9453..12da2c1 100644
--- a/java/com/google/gerrit/testing/InMemoryModule.java
+++ b/java/com/google/gerrit/testing/InMemoryModule.java
@@ -134,7 +134,6 @@
cfg.setString("user", null, "name", "Gerrit Code Review");
cfg.setString("user", null, "email", "gerrit@localhost");
cfg.unset("cache", null, "directory");
- cfg.setString("index", null, "type", "lucene");
cfg.setBoolean("index", "lucene", "testInmemory", true);
cfg.setInt("sendemail", null, "threadPoolSize", 0);
cfg.setBoolean("receive", null, "enableSignedPush", false);
@@ -240,7 +239,8 @@
bind(AllChangesIndexer.class).toProvider(Providers.of(null));
bind(AllGroupsIndexer.class).toProvider(Providers.of(null));
- IndexType indexType = new IndexType(cfg.getString("index", null, "type"));
+ String indexTypeCfg = cfg.getString("index", null, "type");
+ IndexType indexType = new IndexType(indexTypeCfg != null ? indexTypeCfg : "fake");
// For custom index types, callers must provide their own module.
if (indexType.isLucene()) {
install(luceneIndexModule());
diff --git a/java/com/google/gerrit/testing/IndexConfig.java b/java/com/google/gerrit/testing/IndexConfig.java
index daef2b3..d68dcad 100644
--- a/java/com/google/gerrit/testing/IndexConfig.java
+++ b/java/com/google/gerrit/testing/IndexConfig.java
@@ -38,7 +38,9 @@
}
public static Config createForLucene() {
- return create();
+ Config cfg = create();
+ cfg.setString("index", null, "type", "lucene");
+ return cfg;
}
public static Config createForElasticsearch() {
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
index f66bc8d..aa8615b 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
@@ -75,6 +75,7 @@
i.emailStrategy = EmailStrategy.DISABLED;
i.emailFormat = EmailFormat.PLAINTEXT;
i.defaultBaseForMerges = DefaultBase.AUTO_MERGE;
+ i.disableKeyboardShortcuts = true;
i.expandInlineDiffs ^= true;
i.highlightAssigneeInChangeTable ^= true;
i.relativeDateInChangeTable ^= true;
@@ -93,6 +94,7 @@
assertThat(o.my).containsExactlyElementsIn(i.my);
assertThat(o.changeTable).containsExactlyElementsIn(i.changeTable);
assertThat(o.theme).isEqualTo(i.theme);
+ assertThat(o.disableKeyboardShortcuts).isEqualTo(i.disableKeyboardShortcuts);
}
@Test
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index c08aa7f..7785776 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -4325,6 +4325,101 @@
}
@Test
+ public void submitRequirement_retrievedFromNoteDbForClosedChanges() throws Exception {
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("code-review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:code-review=+2"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(change.submitRequirements, "code-review", Status.UNSATISFIED);
+
+ voteLabel(changeId, "code-review", 2);
+
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(change.submitRequirements, "code-review", Status.SATISFIED);
+
+ gApi.changes().id(changeId).current().submit();
+
+ // Add new submit requirement
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("verified")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:verified=+1"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ // The new "verified" submit requirement is not returned, since this change is closed
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(change.submitRequirements, "code-review", Status.SATISFIED);
+ }
+
+ @Test
+ public void submitRequirement_backFilledFromIndexForActiveChanges() throws Exception {
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("code-review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:code-review=+2"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ voteLabel(changeId, "code-review", 2);
+
+ // Query the change. ChangeInfo is back-filled from the change index.
+ List<ChangeInfo> changeInfos =
+ gApi.changes()
+ .query()
+ .withQuery("project:{" + project.get() + "} (status:open OR status:closed)")
+ .withOptions(ImmutableSet.of(ListChangesOption.SUBMIT_REQUIREMENTS))
+ .get();
+ assertThat(changeInfos).hasSize(1);
+ assertSubmitRequirementStatus(
+ changeInfos.get(0).submitRequirements, "code-review", Status.SATISFIED);
+ }
+
+ @Test
+ public void submitRequirement_backFilledFromIndexForClosedChanges() throws Exception {
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("code-review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:code-review=+2"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ voteLabel(changeId, "code-review", 2);
+ gApi.changes().id(changeId).current().submit();
+
+ // Query the change. ChangeInfo is back-filled from the change index.
+ List<ChangeInfo> changeInfos =
+ gApi.changes()
+ .query()
+ .withQuery("project:{" + project.get() + "} (status:open OR status:closed)")
+ .withOptions(ImmutableSet.of(ListChangesOption.SUBMIT_REQUIREMENTS))
+ .get();
+ assertThat(changeInfos).hasSize(1);
+ assertSubmitRequirementStatus(
+ changeInfos.get(0).submitRequirements, "code-review", Status.SATISFIED);
+ }
+
+ @Test
public void fourByteEmoji() throws Exception {
// U+1F601 GRINNING FACE WITH SMILING EYES
String smile = new String(Character.toChars(0x1f601));
diff --git a/javatests/com/google/gerrit/acceptance/rest/BUILD b/javatests/com/google/gerrit/acceptance/rest/BUILD
index 84887da..a62d551 100644
--- a/javatests/com/google/gerrit/acceptance/rest/BUILD
+++ b/javatests/com/google/gerrit/acceptance/rest/BUILD
@@ -5,6 +5,7 @@
group = "rest_bindings_collection",
labels = ["rest"],
deps = [
+ "//java/com/google/gerrit/server/cancellation",
"//java/com/google/gerrit/server/logging",
],
)
diff --git a/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java b/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java
index 29d54cc..0713fe6 100644
--- a/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java
@@ -121,6 +121,27 @@
}
@Test
+ public void handleWrappedRequestCancelledException() throws Exception {
+ ProjectCreationValidationListener projectCreationListener =
+ new ProjectCreationValidationListener() {
+ @Override
+ public void validateNewProject(CreateProjectArgs args) throws ValidationException {
+ // Simulate an exceeded deadline by throwing RequestCancelledException.
+ throw new RuntimeException(
+ new RequestCancelledException(
+ RequestStateProvider.Reason.SERVER_DEADLINE_EXCEEDED, "deadline = 10m"));
+ }
+ };
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(projectCreationListener)) {
+ RestResponse response = adminRestSession.put("/projects/" + name("new"));
+ assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
+ assertThat(response.getEntityContent())
+ .isEqualTo("Server Deadline Exceeded\n\ndeadline = 10m");
+ }
+ }
+
+ @Test
public void handleClientDisconnectedForPush() throws Exception {
CommitValidationListener commitValidationListener =
new CommitValidationListener() {
@@ -185,6 +206,27 @@
}
@Test
+ public void handleWrappedRequestCancelledExceptionForPush() throws Exception {
+ CommitValidationListener commitValidationListener =
+ new CommitValidationListener() {
+ @Override
+ public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
+ throws CommitValidationException {
+ // Simulate an exceeded deadline by throwing RequestCancelledException.
+ throw new RuntimeException(
+ new RequestCancelledException(
+ RequestStateProvider.Reason.SERVER_DEADLINE_EXCEEDED, "deadline = 10m"));
+ }
+ };
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(commitValidationListener)) {
+ PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+ PushOneCommit.Result r = push.to("refs/heads/master");
+ r.assertErrorStatus("Server Deadline Exceeded (deadline = 10m)");
+ }
+ }
+
+ @Test
public void handleRequestCancellationWithMessageForPush() throws Exception {
CommitValidationListener commitValidationListener =
new CommitValidationListener() {
diff --git a/javatests/com/google/gerrit/acceptance/server/notedb/ExternalIdNotesUpsertPreprocessorIT.java b/javatests/com/google/gerrit/acceptance/server/notedb/ExternalIdNotesUpsertPreprocessorIT.java
new file mode 100644
index 0000000..64ad900
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/notedb/ExternalIdNotesUpsertPreprocessorIT.java
@@ -0,0 +1,209 @@
+// 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.acceptance.server.notedb;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.server.ServerInitiated;
+import com.google.gerrit.server.account.Accounts;
+import com.google.gerrit.server.account.AccountsUpdate;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdNotes;
+import com.google.gerrit.server.account.externalids.ExternalIdUpsertPreprocessor;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.notedb.Sequences;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.util.ArrayList;
+import java.util.List;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Before;
+import org.junit.Test;
+
+/** Tests {@link ExternalIdUpsertPreprocessor}. */
+@TestPlugin(
+ name = "external-id-update-preprocessor",
+ sysModule =
+ "com.google.gerrit.acceptance.server.notedb.ExternalIdNotesUpsertPreprocessorIT$Module")
+public class ExternalIdNotesUpsertPreprocessorIT extends LightweightPluginDaemonTest {
+ @Inject private Sequences sequences;
+ @Inject private @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider;
+ @Inject private ExternalIdNotes.Factory extIdNotesFactory;
+ @Inject private Accounts accounts;
+
+ public static class Module extends AbstractModule {
+ @Override
+ protected void configure() {
+ bind(ExternalIdUpsertPreprocessor.class)
+ .annotatedWith(Exports.named("TestPreprocessor"))
+ .to(TestPreprocessor.class);
+ }
+ }
+
+ private TestPreprocessor testPreprocessor;
+
+ @Before
+ public void setUp() {
+ testPreprocessor = plugin.getSysInjector().getInstance(TestPreprocessor.class);
+ testPreprocessor.reset();
+ }
+
+ @Test
+ public void insertAccount() throws Exception {
+ Account.Id id = Account.id(sequences.nextAccountId());
+ ExternalId extId = ExternalId.create("foo", "bar", id);
+ accountsUpdateProvider.get().insert("test", id, u -> u.addExternalId(extId));
+ assertThat(testPreprocessor.upserted).containsExactly(extId);
+ }
+
+ @Test
+ public void replaceByKeys() throws Exception {
+ Account.Id id = Account.id(sequences.nextAccountId());
+ ExternalId extId1 = ExternalId.create("foo", "bar1", id);
+ ExternalId extId2 = ExternalId.create("foo", "bar2", id);
+ accountsUpdateProvider.get().insert("test", id, u -> u.addExternalId(extId1));
+
+ testPreprocessor.reset();
+ try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+ MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+ ExternalIdNotes extIdNotes = extIdNotesFactory.load(allUsersRepo);
+ extIdNotes.replaceByKeys(ImmutableSet.of(extId1.key()), ImmutableSet.of(extId2));
+ extIdNotes.commit(md);
+ }
+ assertThat(testPreprocessor.upserted).containsExactly(extId2);
+ }
+
+ @Test
+ public void insert() throws Exception {
+ Account.Id id = Account.id(sequences.nextAccountId());
+ ExternalId extId = ExternalId.create("foo", "bar", id);
+
+ try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+ MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+ ExternalIdNotes extIdNotes = extIdNotesFactory.load(allUsersRepo);
+ extIdNotes.insert(extId);
+ extIdNotes.commit(md);
+ }
+ assertThat(testPreprocessor.upserted).containsExactly(extId);
+ }
+
+ @Test
+ public void upsert() throws Exception {
+ Account.Id id = Account.id(sequences.nextAccountId());
+ ExternalId extId = ExternalId.create("foo", "bar", id);
+
+ try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+ MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+ ExternalIdNotes extIdNotes = extIdNotesFactory.load(allUsersRepo);
+ extIdNotes.upsert(extId);
+ extIdNotes.commit(md);
+ }
+ assertThat(testPreprocessor.upserted).containsExactly(extId);
+ }
+
+ @Test
+ public void replace() throws Exception {
+ Account.Id id = Account.id(sequences.nextAccountId());
+ ExternalId extId1 = ExternalId.create("foo", "bar1", id);
+ ExternalId extId2 = ExternalId.create("foo", "bar2", id);
+ accountsUpdateProvider.get().insert("test", id, u -> u.addExternalId(extId1));
+
+ testPreprocessor.reset();
+ try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+ MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+ ExternalIdNotes extIdNotes = extIdNotesFactory.load(allUsersRepo);
+ extIdNotes.replace(ImmutableSet.of(extId1), ImmutableSet.of(extId2));
+ extIdNotes.commit(md);
+ }
+ assertThat(testPreprocessor.upserted).containsExactly(extId2);
+ }
+
+ @Test
+ public void replace_viaAccountsUpdate() throws Exception {
+ Account.Id id = Account.id(sequences.nextAccountId());
+ ExternalId extId1 = ExternalId.create("foo", "bar", id, "email1@foo", "hash");
+ ExternalId extId2 = ExternalId.create("foo", "bar", id, "email2@foo", "hash");
+ accountsUpdateProvider.get().insert("test", id, u -> u.addExternalId(extId1));
+
+ testPreprocessor.reset();
+ accountsUpdateProvider.get().update("test", id, u -> u.updateExternalId(extId2));
+ assertThat(testPreprocessor.upserted).containsExactly(extId2);
+ }
+
+ @Test
+ public void blockUpsert() throws Exception {
+ Account.Id id = Account.id(sequences.nextAccountId());
+ ExternalId extId = ExternalId.create("foo", "bar", id);
+ testPreprocessor.throwException = true;
+ StorageException e =
+ assertThrows(
+ StorageException.class,
+ () -> accountsUpdateProvider.get().insert("test", id, u -> u.addExternalId(extId)));
+ assertThat(e).hasMessageThat().contains("upsert not good");
+ assertThat(testPreprocessor.upserted).isEmpty();
+ }
+
+ @Test
+ public void blockUpsert_replace() throws Exception {
+ Account.Id id = Account.id(sequences.nextAccountId());
+ ExternalId extId1 = ExternalId.create("foo", "bar", id, "email1@foo", "hash");
+ ExternalId extId2 = ExternalId.create("foo", "bar", id, "email2@foo", "hash");
+ accountsUpdateProvider.get().insert("test", id, u -> u.addExternalId(extId1));
+
+ assertThat(accounts.get(id).get().externalIds()).containsExactly(extId1);
+
+ testPreprocessor.reset();
+ testPreprocessor.throwException = true;
+ try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+ MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+ ExternalIdNotes extIdNotes = extIdNotesFactory.load(allUsersRepo);
+ extIdNotes.replace(ImmutableSet.of(extId1), ImmutableSet.of(extId2));
+ StorageException e = assertThrows(StorageException.class, () -> extIdNotes.commit(md));
+ assertThat(e).hasMessageThat().contains("upsert not good");
+ }
+ assertThat(testPreprocessor.upserted).isEmpty();
+ assertThat(accounts.get(id).get().externalIds()).containsExactly(extId1);
+ }
+
+ @Singleton
+ public static class TestPreprocessor implements ExternalIdUpsertPreprocessor {
+ List<ExternalId> upserted = new ArrayList<>();
+
+ boolean throwException = false;
+
+ @Override
+ public void upsert(ExternalId extId) {
+ assertThat(extId.blobId()).isNotNull();
+ if (throwException) {
+ throw new StorageException("upsert not good");
+ }
+ upserted.add(extId);
+ }
+
+ void reset() {
+ upserted.clear();
+ throwException = false;
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/ssh/BUILD b/javatests/com/google/gerrit/acceptance/ssh/BUILD
index 5634322..ee1b221 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/BUILD
+++ b/javatests/com/google/gerrit/acceptance/ssh/BUILD
@@ -18,6 +18,7 @@
vm_args = ["-Xmx512m"],
deps = [
":util",
+ "//java/com/google/gerrit/server/cancellation",
"//java/com/google/gerrit/server/logging",
"//lib/commons:compress",
],
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SshCancellationIT.java b/javatests/com/google/gerrit/acceptance/ssh/SshCancellationIT.java
index 2cb9637..3e31c16 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/SshCancellationIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/SshCancellationIT.java
@@ -99,4 +99,22 @@
adminSshSession.assertFailure("Server Deadline Exceeded (deadline = 10m)");
}
}
+
+ @Test
+ public void handleWrappedRequestCancelledException() throws Exception {
+ ProjectCreationValidationListener projectCreationListener =
+ new ProjectCreationValidationListener() {
+ @Override
+ public void validateNewProject(CreateProjectArgs args) throws ValidationException {
+ throw new RuntimeException(
+ new RequestCancelledException(
+ RequestStateProvider.Reason.SERVER_DEADLINE_EXCEEDED, "deadline = 10m"));
+ }
+ };
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(projectCreationListener)) {
+ adminSshSession.exec("gerrit create-project " + name("new"));
+ adminSshSession.assertFailure("Server Deadline Exceeded (deadline = 10m)");
+ }
+ }
}
diff --git a/javatests/com/google/gerrit/server/BUILD b/javatests/com/google/gerrit/server/BUILD
index 7ab7ae9..a2825322 100644
--- a/javatests/com/google/gerrit/server/BUILD
+++ b/javatests/com/google/gerrit/server/BUILD
@@ -55,6 +55,7 @@
"//java/com/google/gerrit/server/account/externalids/testing",
"//java/com/google/gerrit/server/cache/serialize",
"//java/com/google/gerrit/server/cache/testing",
+ "//java/com/google/gerrit/server/cancellation",
"//java/com/google/gerrit/server/fixes/testing",
"//java/com/google/gerrit/server/git/receive:ref_cache",
"//java/com/google/gerrit/server/ioutil",
diff --git a/javatests/com/google/gerrit/server/cancellation/RequestStateContextTest.java b/javatests/com/google/gerrit/server/cancellation/RequestStateContextTest.java
new file mode 100644
index 0000000..e2c43eb
--- /dev/null
+++ b/javatests/com/google/gerrit/server/cancellation/RequestStateContextTest.java
@@ -0,0 +1,178 @@
+// 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.cancellation;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.common.collect.ImmutableSet;
+import org.junit.Test;
+
+public class RequestStateContextTest {
+ @Test
+ public void openContext() {
+ assertNoRequestStateProviders();
+
+ RequestStateProvider requestStateProvider1 = new TestRequestStateProvider();
+ try (RequestStateContext requestStateContext =
+ RequestStateContext.open().addRequestStateProvider(requestStateProvider1)) {
+ RequestStateProvider requestStateProvider2 = new TestRequestStateProvider();
+ requestStateContext.addRequestStateProvider(requestStateProvider2);
+ assertRequestStateProviders(ImmutableSet.of(requestStateProvider1, requestStateProvider2));
+ }
+
+ assertNoRequestStateProviders();
+ }
+
+ @Test
+ public void openNestedContexts() {
+ assertNoRequestStateProviders();
+
+ RequestStateProvider requestStateProvider1 = new TestRequestStateProvider();
+ try (RequestStateContext requestStateContext =
+ RequestStateContext.open().addRequestStateProvider(requestStateProvider1)) {
+ RequestStateProvider requestStateProvider2 = new TestRequestStateProvider();
+ requestStateContext.addRequestStateProvider(requestStateProvider2);
+ assertRequestStateProviders(ImmutableSet.of(requestStateProvider1, requestStateProvider2));
+
+ RequestStateProvider requestStateProvider3 = new TestRequestStateProvider();
+ try (RequestStateContext requestStateContext2 =
+ RequestStateContext.open().addRequestStateProvider(requestStateProvider3)) {
+ RequestStateProvider requestStateProvider4 = new TestRequestStateProvider();
+ requestStateContext2.addRequestStateProvider(requestStateProvider4);
+ assertRequestStateProviders(
+ ImmutableSet.of(
+ requestStateProvider1,
+ requestStateProvider2,
+ requestStateProvider3,
+ requestStateProvider4));
+ }
+
+ assertRequestStateProviders(ImmutableSet.of(requestStateProvider1, requestStateProvider2));
+ }
+
+ assertNoRequestStateProviders();
+ }
+
+ @Test
+ public void openNestedContextsWithSameRequestStateProviders() {
+ assertNoRequestStateProviders();
+
+ RequestStateProvider requestStateProvider1 = new TestRequestStateProvider();
+ try (RequestStateContext requestStateContext =
+ RequestStateContext.open().addRequestStateProvider(requestStateProvider1)) {
+ RequestStateProvider requestStateProvider2 = new TestRequestStateProvider();
+ requestStateContext.addRequestStateProvider(requestStateProvider2);
+ assertRequestStateProviders(ImmutableSet.of(requestStateProvider1, requestStateProvider2));
+
+ try (RequestStateContext requestStateContext2 =
+ RequestStateContext.open().addRequestStateProvider(requestStateProvider1)) {
+ requestStateContext2.addRequestStateProvider(requestStateProvider2);
+
+ assertRequestStateProviders(ImmutableSet.of(requestStateProvider1, requestStateProvider2));
+ }
+
+ assertRequestStateProviders(ImmutableSet.of(requestStateProvider1, requestStateProvider2));
+ }
+
+ assertNoRequestStateProviders();
+ }
+
+ @Test
+ public void abortIfCancelled_noRequestStateProvider() {
+ assertNoRequestStateProviders();
+
+ // Calling abortIfCancelled() shouldn't throw an exception.
+ RequestStateContext.abortIfCancelled();
+ }
+
+ @Test
+ public void abortIfCancelled_requestNotCancelled() {
+ try (RequestStateContext requestStateContext =
+ RequestStateContext.open()
+ .addRequestStateProvider(
+ new RequestStateProvider() {
+ @Override
+ public void checkIfCancelled(OnCancelled onCancelled) {}
+ })) {
+ // Calling abortIfCancelled() shouldn't throw an exception.
+ RequestStateContext.abortIfCancelled();
+ }
+ }
+
+ @Test
+ public void abortIfCancelled_requestCancelled() {
+ try (RequestStateContext requestStateContext =
+ RequestStateContext.open()
+ .addRequestStateProvider(
+ new RequestStateProvider() {
+ @Override
+ public void checkIfCancelled(OnCancelled onCancelled) {
+ onCancelled.onCancel(
+ RequestStateProvider.Reason.CLIENT_CLOSED_REQUEST, /* message= */ null);
+ }
+ })) {
+ RequestCancelledException requestCancelledException =
+ assertThrows(
+ RequestCancelledException.class, () -> RequestStateContext.abortIfCancelled());
+ assertThat(requestCancelledException)
+ .hasMessageThat()
+ .isEqualTo("Request cancelled: CLIENT_CLOSED_REQUEST");
+ assertThat(requestCancelledException.getCancellationReason())
+ .isEqualTo(RequestStateProvider.Reason.CLIENT_CLOSED_REQUEST);
+ assertThat(requestCancelledException.getCancellationMessage()).isEmpty();
+ }
+ }
+
+ @Test
+ public void abortIfCancelled_requestCancelled_withMessage() {
+ try (RequestStateContext requestStateContext =
+ RequestStateContext.open()
+ .addRequestStateProvider(
+ new RequestStateProvider() {
+ @Override
+ public void checkIfCancelled(OnCancelled onCancelled) {
+ onCancelled.onCancel(
+ RequestStateProvider.Reason.SERVER_DEADLINE_EXCEEDED, "deadline = 10m");
+ }
+ })) {
+ RequestCancelledException requestCancelledException =
+ assertThrows(
+ RequestCancelledException.class, () -> RequestStateContext.abortIfCancelled());
+ assertThat(requestCancelledException)
+ .hasMessageThat()
+ .isEqualTo("Request cancelled: SERVER_DEADLINE_EXCEEDED (deadline = 10m)");
+ assertThat(requestCancelledException.getCancellationReason())
+ .isEqualTo(RequestStateProvider.Reason.SERVER_DEADLINE_EXCEEDED);
+ assertThat(requestCancelledException.getCancellationMessage()).hasValue("deadline = 10m");
+ }
+ }
+
+ private void assertNoRequestStateProviders() {
+ assertRequestStateProviders(ImmutableSet.of());
+ }
+
+ private void assertRequestStateProviders(
+ ImmutableSet<RequestStateProvider> expectedRequestStateProviders) {
+ assertThat(RequestStateContext.getRequestStateProviders())
+ .containsExactlyElementsIn(expectedRequestStateProviders);
+ }
+
+ private static class TestRequestStateProvider implements RequestStateProvider {
+ @Override
+ public void checkIfCancelled(OnCancelled onCancelled) {}
+ }
+}
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index 4d0bb1f..5f9c3c5 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -106,9 +106,6 @@
"elements/change/gr-reply-dialog/gr-reply-dialog_html.ts",
"elements/change/gr-reviewer-list/gr-reviewer-list_html.ts",
"elements/change/gr-thread-list/gr-thread-list_html.ts",
- "elements/checks/gr-hovercard-run_html.ts",
- "elements/core/gr-main-header/gr-main-header_html.ts",
- "elements/core/gr-search-bar/gr-search-bar_html.ts",
"elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_html.ts",
"elements/diff/gr-diff-builder/gr-diff-builder-element_html.ts",
"elements/diff/gr-diff-host/gr-diff-host_html.ts",
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
index af565cb..a090aee 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
@@ -150,18 +150,25 @@
this._query = (text: string) => this._getProjectBranchesSuggestions(text);
}
+ containsDuplicateProject(changes: ChangeInfo[]) {
+ const projects: {[projectName: string]: boolean} = {};
+ for (let i = 0; i < changes.length; i++) {
+ const change = changes[i];
+ if (projects[change.project]) {
+ return true;
+ }
+ projects[change.project] = true;
+ }
+ return false;
+ }
+
updateChanges(changes: ChangeInfo[]) {
this.changes = changes;
this._statuses = {};
- const projects: {[projectName: string]: boolean} = {};
- this._duplicateProjectChanges = false;
changes.forEach(change => {
this.selectedChangeIds.add(change.id);
- if (projects[change.project]) {
- this._duplicateProjectChanges = true;
- }
- projects[change.project] = true;
});
+ this._duplicateProjectChanges = this.containsDuplicateProject(changes);
this._changesCount = changes.length;
this._showCherryPickTopic = changes.length > 1;
}
@@ -185,6 +192,10 @@
if (this.selectedChangeIds.has(changeId))
this.selectedChangeIds.delete(changeId);
else this.selectedChangeIds.add(changeId);
+ const changes = this.changes.filter(change =>
+ this.selectedChangeIds.has(change.id)
+ );
+ this._duplicateProjectChanges = this.containsDuplicateProject(changes);
}
_computeTopicErrorMessage(duplicateProjectChanges: boolean) {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.js
index 536a4ab..1034674 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.js
@@ -130,6 +130,8 @@
});
test('deselecting a change removes it from being cherry picked', () => {
+ const duplicateChangesStub = sinon.stub(element,
+ 'containsDuplicateProject');
element.branch = 'master';
const executeChangeActionStub = stubRestApi(
'executeChangeAction').returns(Promise.resolve([]));
@@ -142,6 +144,7 @@
querySelector('gr-dialog').$.confirm);
flush();
assert.equal(executeChangeActionStub.callCount, 1);
+ assert.isTrue(duplicateChangesStub.called);
});
test('deselecting all change shows error message', () => {
diff --git a/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts b/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts
index 1ae3e2b..2316a05 100644
--- a/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts
+++ b/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts
@@ -112,6 +112,10 @@
if (!run) return true;
return !run.checkLink && !run.checkDescription;
}
+
+ _convertUndefined(value?: string) {
+ return value ?? '';
+ }
}
declare global {
diff --git a/polygerrit-ui/app/elements/checks/gr-hovercard-run_html.ts b/polygerrit-ui/app/elements/checks/gr-hovercard-run_html.ts
index 277bd16..5b2e24a 100644
--- a/polygerrit-ui/app/elements/checks/gr-hovercard-run_html.ts
+++ b/polygerrit-ui/app/elements/checks/gr-hovercard-run_html.ts
@@ -130,7 +130,7 @@
<div hidden$="[[!run.statusLink]]" class="row">
<div class="title">Status</div>
<div>
- <a href="[[run.statusLink]]" target="_blank"
+ <a href="[[_convertUndefined(run.statusLink)]]" target="_blank"
><iron-icon
aria-label="external link to check status"
class="small link"
@@ -202,7 +202,7 @@
<div hidden$="[[!run.checkLink]]" class="row">
<div class="title">Documentation</div>
<div>
- <a href="[[run.checkLink]]" target="_blank"
+ <a href="[[_convertUndefined(run.checkLink)]]" target="_blank"
><iron-icon
aria-label="external link to check documentation"
class="small link"
@@ -216,10 +216,7 @@
</div>
<template is="dom-repeat" items="[[computeActions(run)]]">
<div class="action">
- <gr-checks-action
- event-target="[[_target]]"
- action="[[item]]"
- ></gr-checks-action>
+ <gr-checks-action action="[[item]]"></gr-checks-action>
</div>
</template>
</div>
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
index f7e61bf..fd7e25e 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
@@ -110,7 +110,7 @@
}
@property({type: String, notify: true})
- searchQuery?: string;
+ searchQuery = '';
@property({type: Boolean, reflectToAttribute: true})
loggedIn?: boolean;
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 e30e75e..841089e 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
@@ -180,7 +180,7 @@
accountSuggestions: SuggestionProvider = () => Promise.resolve([]);
@property({type: String})
- _inputVal?: string;
+ _inputVal = '';
@property({type: Number})
_threshold = 1;
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.js b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts
similarity index 66%
rename from polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.js
rename to polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts
index ae7e10c..71d378e 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.js
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts
@@ -15,17 +15,28 @@
* limitations under the License.
*/
-import '../../../test/common-test-setup-karma.js';
-import './gr-search-bar.js';
-import '../../../scripts/util.js';
-import {TestKeyboardShortcutBinder, stubRestApi} from '../../../test/test-utils.js';
-import {Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
-import {_testOnly_clearDocsBaseUrlCache} from '../../../utils/url-util.js';
+import '../../../test/common-test-setup-karma';
+import './gr-search-bar';
+import '../../../scripts/util';
+import {GrSearchBar} from './gr-search-bar';
+import {
+ TestKeyboardShortcutBinder,
+ stubRestApi,
+} from '../../../test/test-utils';
+import {Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
+import {_testOnly_clearDocsBaseUrlCache} from '../../../utils/url-util';
+import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
+import {
+ createChangeConfig,
+ createGerritInfo,
+ createServerInfo,
+} from '../../../test/test-data-generators';
+import {MergeabilityComputationBehavior} from '../../../constants/constants';
const basicFixture = fixtureFromElement('gr-search-bar');
suite('gr-search-bar tests', () => {
- let element;
+ let element: GrSearchBar;
suiteSetup(() => {
const kb = TestKeyboardShortcutBinder.push();
@@ -46,26 +57,34 @@
assert.equal(element._inputVal, 'foo');
});
- const getActiveElement = () => (document.activeElement.shadowRoot ?
- document.activeElement.shadowRoot.activeElement :
- document.activeElement);
+ const getActiveElement = () =>
+ document.activeElement!.shadowRoot
+ ? document.activeElement!.shadowRoot.activeElement
+ : document.activeElement;
test('enter in search input fires event', done => {
element.addEventListener('handle-search', () => {
assert.notEqual(getActiveElement(), element.$.searchInput);
- assert.notEqual(getActiveElement(), element.$.searchButton);
done();
});
element.value = 'test';
- MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
- null, 'enter');
+ MockInteractions.pressAndReleaseKeyOn(
+ element.$.searchInput.$.input,
+ 13,
+ null,
+ 'enter'
+ );
});
test('input blurred after commit', () => {
const blurSpy = sinon.spy(element.$.searchInput.$.input, 'blur');
element.$.searchInput.text = 'fate/stay';
- MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
- null, 'enter');
+ MockInteractions.pressAndReleaseKeyOn(
+ element.$.searchInput.$.input,
+ 13,
+ null,
+ 'enter'
+ );
assert.isTrue(blurSpy.called);
});
@@ -73,8 +92,12 @@
const searchSpy = sinon.spy();
element.addEventListener('handle-search', searchSpy);
element.value = '';
- MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
- null, 'enter');
+ MockInteractions.pressAndReleaseKeyOn(
+ element.$.searchInput.$.input,
+ 13,
+ null,
+ 'enter'
+ );
assert.isFalse(searchSpy.called);
});
@@ -82,8 +105,12 @@
const searchSpy = sinon.spy();
element.addEventListener('handle-search', searchSpy);
element.value = 'added:';
- MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
- null, 'enter');
+ MockInteractions.pressAndReleaseKeyOn(
+ element.$.searchInput.$.input,
+ 13,
+ null,
+ 'enter'
+ );
assert.isFalse(searchSpy.called);
});
@@ -91,8 +118,12 @@
const searchSpy = sinon.spy();
element.addEventListener('handle-search', searchSpy);
element.value = 'age:1week';
- MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
- null, 'enter');
+ MockInteractions.pressAndReleaseKeyOn(
+ element.$.searchInput.$.input,
+ 13,
+ null,
+ 'enter'
+ );
assert.isTrue(searchSpy.called);
});
@@ -100,8 +131,12 @@
const searchSpy = sinon.spy();
element.addEventListener('handle-search', searchSpy);
element.value = 'random:1week';
- MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
- null, 'enter');
+ MockInteractions.pressAndReleaseKeyOn(
+ element.$.searchInput.$.input,
+ 13,
+ null,
+ 'enter'
+ );
assert.isTrue(searchSpy.called);
});
@@ -109,8 +144,12 @@
const searchSpy = sinon.spy();
element.addEventListener('handle-search', searchSpy);
element.value = 'random:';
- MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
- null, 'enter');
+ MockInteractions.pressAndReleaseKeyOn(
+ element.$.searchInput.$.input,
+ 13,
+ null,
+ 'enter'
+ );
assert.isTrue(searchSpy.called);
});
@@ -129,21 +168,23 @@
});
test('Autocompletes accounts', () => {
- sinon.stub(element, 'accountSuggestions').callsFake(() =>
- Promise.resolve([{text: 'owner:fred@goog.co'}])
- );
+ sinon
+ .stub(element, 'accountSuggestions')
+ .callsFake(() => Promise.resolve([{text: 'owner:fred@goog.co'}]));
return element._getSearchSuggestions('owner:fr').then(s => {
assert.equal(s[0].value, 'owner:fred@goog.co');
});
});
test('Autocompletes groups', done => {
- sinon.stub(element, 'groupSuggestions').callsFake(() =>
- Promise.resolve([
- {text: 'ownerin:Polygerrit'},
- {text: 'ownerin:gerrit'},
- ])
- );
+ sinon
+ .stub(element, 'groupSuggestions')
+ .callsFake(() =>
+ Promise.resolve([
+ {text: 'ownerin:Polygerrit'},
+ {text: 'ownerin:gerrit'},
+ ])
+ );
element._getSearchSuggestions('ownerin:pol').then(s => {
assert.equal(s[0].value, 'ownerin:Polygerrit');
done();
@@ -151,13 +192,15 @@
});
test('Autocompletes projects', done => {
- sinon.stub(element, 'projectSuggestions').callsFake(() =>
- Promise.resolve([
- {text: 'project:Polygerrit'},
- {text: 'project:gerrit'},
- {text: 'project:gerrittest'},
- ])
- );
+ sinon
+ .stub(element, 'projectSuggestions')
+ .callsFake(() =>
+ Promise.resolve([
+ {text: 'project:Polygerrit'},
+ {text: 'project:gerrit'},
+ {text: 'project:gerrittest'},
+ ])
+ );
element._getSearchSuggestions('project:pol').then(s => {
assert.equal(s[0].value, 'project:Polygerrit');
done();
@@ -193,11 +236,15 @@
].forEach(mergeability => {
suite(`mergeability as ${mergeability}`, () => {
setup(done => {
- stubRestApi('getConfig').returns(Promise.resolve({
- change: {
- mergeability_computation_behavior: mergeability,
- },
- }));
+ stubRestApi('getConfig').returns(
+ Promise.resolve({
+ ...createServerInfo(),
+ change: {
+ ...createChangeConfig(),
+ mergeability_computation_behavior: mergeability as MergeabilityComputationBehavior,
+ },
+ })
+ );
element = basicFixture.instantiate();
flush(done);
@@ -218,11 +265,15 @@
suite('doc url', () => {
setup(done => {
- stubRestApi('getConfig').returns(Promise.resolve({
- gerrit: {
- doc_url: 'https://doc.com/',
- },
- }));
+ stubRestApi('getConfig').returns(
+ Promise.resolve({
+ ...createServerInfo(),
+ gerrit: {
+ ...createGerritInfo(),
+ doc_url: 'https://doc.com/',
+ },
+ })
+ );
_testOnly_clearDocsBaseUrlCache();
element = basicFixture.instantiate();
@@ -232,18 +283,17 @@
test('compute help doc url with correct path', () => {
assert.equal(element.docBaseUrl, 'https://doc.com/');
assert.equal(
- element._computeHelpDocLink(element.docBaseUrl),
- 'https://doc.com/user-search.html'
+ element._computeHelpDocLink(element.docBaseUrl),
+ 'https://doc.com/user-search.html'
);
});
test('compute help doc url fallback to gerrit url', () => {
assert.equal(
- element._computeHelpDocLink(),
- 'https://gerrit-review.googlesource.com/documentation/' +
+ element._computeHelpDocLink(null),
+ 'https://gerrit-review.googlesource.com/documentation/' +
'user-search.html'
);
});
});
});
-
diff --git a/polygerrit-ui/app/services/comments/comments-model.ts b/polygerrit-ui/app/services/comments/comments-model.ts
index b26ec9b..bbc7b1a 100644
--- a/polygerrit-ui/app/services/comments/comments-model.ts
+++ b/polygerrit-ui/app/services/comments/comments-model.ts
@@ -111,7 +111,7 @@
if (!drafts[draft.path]) drafts[draft.path] = [] as DraftInfo[];
else drafts[draft.path] = [...drafts[draft.path]];
const index = drafts[draft.path].findIndex(
- d => d.__draftID === draft.__draftID || d.id === draft.id
+ d => (d.__draftID && d.__draftID === draft.__draftID) || d.id === draft.id
);
if (index !== -1) {
drafts[draft.path][index] = draft;
@@ -127,7 +127,7 @@
nextState.drafts = {...nextState.drafts};
const drafts = nextState.drafts;
const index = (drafts[draft.path] || []).findIndex(
- d => d.__draftID === draft.__draftID || d.id === draft.id
+ d => (d.__draftID && d.__draftID === draft.__draftID) || d.id === draft.id
);
if (index === -1) return;
drafts[draft.path] = [...drafts[draft.path]];
diff --git a/polygerrit-ui/app/styles/themes/app-theme.ts b/polygerrit-ui/app/styles/themes/app-theme.ts
index 134003b..1996800 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.ts
+++ b/polygerrit-ui/app/styles/themes/app-theme.ts
@@ -128,20 +128,20 @@
--error-foreground: var(--red-700);
--error-background: var(--red-50);
- --error-background-hover: linear-gradient(var(--red-700-04), var(--red-700-04), var(--red-50));
- --error-background-focus: linear-gradient(var(--red-700-12), var(--red-700-12), var(--red-50));
+ --error-background-hover: linear-gradient(var(--red-700-04), var(--red-700-04)), var(--red-50);
+ --error-background-focus: linear-gradient(var(--red-700-12), var(--red-700-12)), var(--red-50);
--error-ripple: var(--red-700-10);
--warning-foreground: var(--orange-700);
--warning-background: var(--orange-50);
- --warning-background-hover: linear-gradient(var(--orange-700-04), var(--orange-700-04), var(--orange-50));
- --warning-background-focus: linear-gradient(var(--orange-700-12), var(--orange-700-12), var(--orange-50));
+ --warning-background-hover: linear-gradient(var(--orange-700-04), var(--orange-700-04)), var(--orange-50);
+ --warning-background-focus: linear-gradient(var(--orange-700-12), var(--orange-700-12)), var(--orange-50);
--warning-ripple: var(--orange-700-10);
--info-foreground: var(--blue-700);
--info-background: var(--blue-50);
- --info-background-hover: linear-gradient(var(--blue-700-04), var(--blue-700-04), var(--blue-50));
- --info-background-focus: linear-gradient(var(--blue-700-12), var(--blue-700-12), var(--blue-50));
+ --info-background-hover: linear-gradient(var(--blue-700-04), var(--blue-700-04)), var(--blue-50);
+ --info-background-focus: linear-gradient(var(--blue-700-12), var(--blue-700-12)), var(--blue-50);
--info-ripple: var(--blue-700-10);
--primary-button-text-color: white;
@@ -154,14 +154,14 @@
--success-foreground: var(--green-700);
--success-background: var(--green-50);
- --success-background-hover: linear-gradient(var(--green-700-04), var(--green-700-04), var(--green-50));
- --success-background-focus: linear-gradient(var(--green-700-12), var(--green-700-12), var(--green-50));
+ --success-background-hover: linear-gradient(var(--green-700-04), var(--green-700-04)), var(--green-50);
+ --success-background-focus: linear-gradient(var(--green-700-12), var(--green-700-12)), var(--green-50);
--success-ripple: var(--green-700-10);
--gray-foreground: var(--gray-700);
--gray-background: var(--gray-100);
- --gray-background-hover: linear-gradient(var(--gray-700-04), var(--gray-700-04), var(--gray-100));
- --gray-background-focus: linear-gradient(var(--gray-700-12), var(--gray-700-12), var(--gray-100));
+ --gray-background-hover: linear-gradient(var(--gray-700-04), var(--gray-700-04)), var(--gray-100);
+ --gray-background-focus: linear-gradient(var(--gray-700-12), var(--gray-700-12)), var(--gray-100);
--gray-ripple: var(--gray-700-10);
--disabled-foreground: var(--gray-800-38);
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.ts b/polygerrit-ui/app/styles/themes/dark-theme.ts
index 69256b2..926b02d 100644
--- a/polygerrit-ui/app/styles/themes/dark-theme.ts
+++ b/polygerrit-ui/app/styles/themes/dark-theme.ts
@@ -38,20 +38,20 @@
--error-foreground: var(--red-200);
--error-background: var(--red-tonal);
- --error-background-hover: linear-gradient(var(--white-04), var(--white-04), var(--red-tonal));
- --error-background-focus: linear-gradient(var(--white-12), var(--white-12), var(--red-tonal));
+ --error-background-hover: linear-gradient(var(--white-04), var(--white-04)), var(--red-tonal);
+ --error-background-focus: linear-gradient(var(--white-12), var(--white-12)), var(--red-tonal);
--error-ripple: var(--white-10);
--warning-foreground: var(--orange-200);
--warning-background: var(--orange-tonal);
- --warning-background-hover: linear-gradient(var(--white-04), var(--white-04), var(--orange-tonal));
- --warning-background-focus: linear-gradient(var(--white-12), var(--white-12), var(--orange-tonal));
+ --warning-background-hover: linear-gradient(var(--white-04), var(--white-04)), var(--orange-tonal);
+ --warning-background-focus: linear-gradient(var(--white-12), var(--white-12)), var(--orange-tonal);
--warning-ripple: var(--white-10);
--info-foreground: var(--blue-200);
--info-background: var(--blue-tonal);
- --info-background-hover: linear-gradient(var(--white-04), var(--white-04), var(--blue-tonal));
- --info-background-focus: linear-gradient(var(--white-12), var(--white-12), var(--blue-tonal));
+ --info-background-hover: linear-gradient(var(--white-04), var(--white-04)), var(--blue-tonal);
+ --info-background-focus: linear-gradient(var(--white-12), var(--white-12)), var(--blue-tonal);
--info-ripple: var(--white-10);
--primary-button-text-color: black;
@@ -64,14 +64,14 @@
--success-foreground: var(--green-200);
--success-background: var(--green-tonal);
- --success-background-hover: linear-gradient(var(--white-04), var(--white-04), var(--green-tonal));
- --success-background-focus: linear-gradient(var(--white-12), var(--white-12), var(--green-tonal));
+ --success-background-hover: linear-gradient(var(--white-04), var(--white-04)), var(--green-tonal);
+ --success-background-focus: linear-gradient(var(--white-12), var(--white-12)), var(--green-tonal);
--success-ripple: var(--white-10);
--gray-foreground: var(--gray-300);
--gray-background: var(--gray-tonal);
- --gray-background-hover: linear-gradient(var(--white-04), var(--white-04), var(--gray-tonal));
- --gray-background-focus: linear-gradient(var(--white-12), var(--white-12), var(--gray-tonal));
+ --gray-background-hover: linear-gradient(var(--white-04), var(--white-04)), var(--gray-tonal);
+ --gray-background-focus: linear-gradient(var(--white-12), var(--white-12)), var(--gray-tonal);
--gray-ripple: var(--white-10);
--disabled-foreground: var(--gray-200-38);
diff --git a/polygerrit-ui/app/utils/display-name-util_test.js b/polygerrit-ui/app/utils/display-name-util_test.js
deleted file mode 100644
index 9bb68dc..0000000
--- a/polygerrit-ui/app/utils/display-name-util_test.js
+++ /dev/null
@@ -1,200 +0,0 @@
-/**
- * @license
- * Copyright (C) 2019 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.
- */
-
-import '../test/common-test-setup-karma.js';
-import {getDisplayName, getUserName, getGroupDisplayName, getAccountDisplayName, _testOnly_accountEmail} from './display-name-util.js';
-
-suite('display-name-utils tests', () => {
- // eslint-disable-next-line no-unused-vars
- const config = {
- user: {
- anonymous_coward_name: 'Anonymous Coward',
- },
- };
-
- test('getDisplayName name only', () => {
- const account = {
- name: 'test-name',
- };
- assert.equal(getDisplayName(config, account),
- 'test-name');
- });
-
- test('getDisplayName prefer displayName', () => {
- const account = {
- name: 'test-name',
- display_name: 'better-name',
- };
- assert.equal(getDisplayName(config, account),
- 'better-name');
- });
-
- test('getDisplayName prefer username default', () => {
- const account = {
- name: 'test-name',
- username: 'user-name',
- };
- const config = {
- accounts: {
- default_display_name: 'USERNAME',
- },
- };
- assert.equal(getDisplayName(config, account),
- 'user-name');
- });
-
- test('getDisplayName firstNameOnly', () => {
- const account = {
- name: 'firstname lastname',
- };
- assert.equal(getDisplayName(config, account, true), 'firstname');
- });
-
- test('getDisplayName prefer first name default', () => {
- const account = {
- name: 'firstname lastname',
- };
- const config = {
- accounts: {
- default_display_name: 'FIRST_NAME',
- },
- };
- assert.equal(getDisplayName(config, account),
- 'firstname');
- });
-
- test('getDisplayName ignore leading whitespace for first name', () => {
- const account = {
- name: ' firstname lastname',
- };
- const config = {
- accounts: {
- default_display_name: 'FIRST_NAME',
- },
- };
- assert.equal(getDisplayName(config, account),
- 'firstname');
- });
-
- test('getDisplayName full name default', () => {
- const account = {
- name: 'firstname lastname',
- };
- const config = {
- accounts: {
- default_display_name: 'FULL_NAME',
- },
- };
- assert.equal(getDisplayName(config, account),
- 'firstname lastname');
- });
-
- test('getDisplayName name only', () => {
- const account = {
- name: 'test-name',
- };
- assert.deepEqual(getUserName(config, account),
- 'test-name');
- });
-
- test('getUserName username only', () => {
- const account = {
- username: 'test-user',
- };
- assert.deepEqual(getUserName(config, account),
- 'test-user');
- });
-
- test('getUserName email only', () => {
- const account = {
- email: 'test-user@test-url.com',
- };
- assert.deepEqual(getUserName(config, account),
- 'test-user@test-url.com');
- });
-
- test('getUserName returns not Anonymous Coward as the anon name', () => {
- assert.deepEqual(getUserName(config, null),
- 'Anonymous');
- });
-
- test('getUserName for the config returning the anon name', () => {
- const config = {
- user: {
- anonymous_coward_name: 'Test Anon',
- },
- };
- assert.deepEqual(getUserName(config, null),
- 'Test Anon');
- });
-
- test('getAccountDisplayName - account with name only', () => {
- assert.equal(
- getAccountDisplayName(config,
- {name: 'Some user name'}),
- 'Some user name');
- });
-
- test('getAccountDisplayName - account with email only', () => {
- assert.equal(
- getAccountDisplayName(config,
- {email: 'my@example.com'}),
- 'my@example.com <my@example.com>');
- });
-
- test('getAccountDisplayName - account with name and status', () => {
- assert.equal(
- getAccountDisplayName(config, {
- name: 'Some name',
- status: 'OOO',
- }),
- 'Some name (OOO)');
- });
-
- test('getAccountDisplayName - account with name and email', () => {
- assert.equal(
- getAccountDisplayName(config, {
- name: 'Some name',
- email: 'my@example.com',
- }),
- 'Some name <my@example.com>');
- });
-
- test('getAccountDisplayName - account with name, email and status', () => {
- assert.equal(
- getAccountDisplayName(config, {
- name: 'Some name',
- email: 'my@example.com',
- status: 'OOO',
- }),
- 'Some name <my@example.com> (OOO)');
- });
-
- test('getGroupDisplayName', () => {
- assert.equal(
- getGroupDisplayName({name: 'Some user name'}),
- 'Some user name (group)');
- });
-
- test('_accountEmail', () => {
- assert.equal(
- _testOnly_accountEmail('email@gerritreview.com'),
- '<email@gerritreview.com>');
- assert.equal(_testOnly_accountEmail(undefined), '');
- });
-});
-
diff --git a/polygerrit-ui/app/utils/display-name-util_test.ts b/polygerrit-ui/app/utils/display-name-util_test.ts
new file mode 100644
index 0000000..e6d4704
--- /dev/null
+++ b/polygerrit-ui/app/utils/display-name-util_test.ts
@@ -0,0 +1,225 @@
+/**
+ * @license
+ * Copyright (C) 2019 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.
+ */
+
+import {
+ AccountInfo,
+ DefaultDisplayNameConfig,
+ EmailAddress,
+ GroupName,
+ ServerInfo,
+} from '../api/rest-api';
+import '../test/common-test-setup-karma';
+import {
+ getDisplayName,
+ getUserName,
+ getGroupDisplayName,
+ getAccountDisplayName,
+ _testOnly_accountEmail,
+} from './display-name-util';
+import {
+ createAccountsConfig,
+ createGroupInfo,
+ createServerInfo,
+} from '../test/test-data-generators';
+
+suite('display-name-utils tests', () => {
+ const config: ServerInfo = {
+ ...createServerInfo(),
+ user: {
+ anonymous_coward_name: 'Anonymous Coward',
+ },
+ };
+
+ test('getDisplayName name only', () => {
+ const account = {
+ name: 'test-name',
+ };
+ assert.equal(getDisplayName(config, account), 'test-name');
+ });
+
+ test('getDisplayName prefer displayName', () => {
+ const account = {
+ name: 'test-name',
+ display_name: 'better-name',
+ };
+ assert.equal(getDisplayName(config, account), 'better-name');
+ });
+
+ test('getDisplayName prefer username default', () => {
+ const account = {
+ name: 'test-name',
+ username: 'user-name',
+ };
+ const config: ServerInfo = {
+ ...createServerInfo(),
+ accounts: {
+ ...createAccountsConfig(),
+ default_display_name: DefaultDisplayNameConfig.USERNAME,
+ },
+ };
+ assert.equal(getDisplayName(config, account), 'user-name');
+ });
+
+ test('getDisplayName firstNameOnly', () => {
+ const account = {
+ name: 'firstname lastname',
+ };
+ assert.equal(getDisplayName(config, account, true), 'firstname');
+ });
+
+ test('getDisplayName prefer first name default', () => {
+ const account = {
+ name: 'firstname lastname',
+ };
+ const config: ServerInfo = {
+ ...createServerInfo(),
+ accounts: {
+ ...createAccountsConfig(),
+ default_display_name: DefaultDisplayNameConfig.FIRST_NAME,
+ },
+ };
+ assert.equal(getDisplayName(config, account), 'firstname');
+ });
+
+ test('getDisplayName ignore leading whitespace for first name', () => {
+ const account = {
+ name: ' firstname lastname',
+ };
+ const config: ServerInfo = {
+ ...createServerInfo(),
+ accounts: {
+ ...createAccountsConfig(),
+ default_display_name: DefaultDisplayNameConfig.FIRST_NAME,
+ },
+ };
+ assert.equal(getDisplayName(config, account), 'firstname');
+ });
+
+ test('getDisplayName full name default', () => {
+ const account = {
+ name: 'firstname lastname',
+ };
+ const config: ServerInfo = {
+ ...createServerInfo(),
+ accounts: {
+ ...createAccountsConfig(),
+ default_display_name: DefaultDisplayNameConfig.FULL_NAME,
+ },
+ };
+ assert.equal(getDisplayName(config, account), 'firstname lastname');
+ });
+
+ test('getDisplayName name only', () => {
+ const account = {
+ name: 'test-name',
+ };
+ assert.deepEqual(getUserName(config, account), 'test-name');
+ });
+
+ test('getUserName username only', () => {
+ const account = {
+ username: 'test-user',
+ };
+ assert.deepEqual(getUserName(config, account), 'test-user');
+ });
+
+ test('getUserName email only', () => {
+ const account: AccountInfo = {
+ email: 'test-user@test-url.com' as EmailAddress,
+ };
+ assert.deepEqual(getUserName(config, account), 'test-user@test-url.com');
+ });
+
+ test('getUserName returns not Anonymous Coward as the anon name', () => {
+ assert.deepEqual(getUserName(config, undefined), 'Anonymous');
+ });
+
+ test('getUserName for the config returning the anon name', () => {
+ const config: ServerInfo = {
+ ...createServerInfo(),
+ user: {
+ anonymous_coward_name: 'Test Anon',
+ },
+ };
+ assert.deepEqual(getUserName(config, undefined), 'Test Anon');
+ });
+
+ test('getAccountDisplayName - account with name only', () => {
+ assert.equal(
+ getAccountDisplayName(config, {name: 'Some user name'}),
+ 'Some user name'
+ );
+ });
+
+ test('getAccountDisplayName - account with email only', () => {
+ assert.equal(
+ getAccountDisplayName(config, {
+ email: 'my@example.com' as EmailAddress,
+ }),
+ 'my@example.com <my@example.com>'
+ );
+ });
+
+ test('getAccountDisplayName - account with name and status', () => {
+ assert.equal(
+ getAccountDisplayName(config, {
+ name: 'Some name',
+ status: 'OOO',
+ }),
+ 'Some name (OOO)'
+ );
+ });
+
+ test('getAccountDisplayName - account with name and email', () => {
+ assert.equal(
+ getAccountDisplayName(config, {
+ name: 'Some name',
+ email: 'my@example.com' as EmailAddress,
+ }),
+ 'Some name <my@example.com>'
+ );
+ });
+
+ test('getAccountDisplayName - account with name, email and status', () => {
+ assert.equal(
+ getAccountDisplayName(config, {
+ name: 'Some name',
+ email: 'my@example.com' as EmailAddress,
+ status: 'OOO',
+ }),
+ 'Some name <my@example.com> (OOO)'
+ );
+ });
+
+ test('getGroupDisplayName', () => {
+ assert.equal(
+ getGroupDisplayName({
+ ...createGroupInfo(),
+ name: 'Some user name' as GroupName,
+ }),
+ 'Some user name (group)'
+ );
+ });
+
+ test('_accountEmail', () => {
+ assert.equal(
+ _testOnly_accountEmail('email@gerritreview.com'),
+ '<email@gerritreview.com>'
+ );
+ assert.equal(_testOnly_accountEmail(undefined), '');
+ });
+});