Merge "Recalculate duplicate projects once user selects cherry pick changes"
diff --git a/Documentation/linux-quickstart.txt b/Documentation/linux-quickstart.txt
index e34071f..c45de05 100644
--- a/Documentation/linux-quickstart.txt
+++ b/Documentation/linux-quickstart.txt
@@ -104,7 +104,7 @@
Now that you have a simple version of Gerrit running, use the installation to
explore the user interface and learn about Gerrit. For more detailed
installation instructions, see
-link:[Standalone Daemon Installation Guide](install.html).
+link:install.html[Standalone Daemon Installation Guide].
GERRIT
------
diff --git a/Documentation/repository-maintenance.txt b/Documentation/repository-maintenance.txt
index 1672436..4bf84b5 100644
--- a/Documentation/repository-maintenance.txt
+++ b/Documentation/repository-maintenance.txt
@@ -28,7 +28,7 @@
Unlike a typical server database, access to Git repositories is not
marshalled through a single process or a set of inter communicating
-processes. Unfortuntatlely the design of the on-disk layout of a Git
+processes. Unfortunately the design of the on-disk layout of a Git
repository does not allow for 100% race free operations when accessed by
multiple actors concurrently. These design shortcomings are more likely
to impact the operations of busy repositories since racy conditions are
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/acceptance/InMemoryTestingDatabaseModule.java b/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
index de9a43d..e4b0eea 100644
--- a/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
+++ b/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
@@ -21,7 +21,9 @@
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.server.config.FileBasedGlobalPluginConfigProvider;
import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.GlobalPluginConfigProvider;
import com.google.gerrit.server.config.SitePath;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.config.TrackingFooters;
@@ -54,6 +56,7 @@
@Override
protected void configure() {
bind(Config.class).annotatedWith(GerritServerConfig.class).toInstance(cfg);
+ bind(GlobalPluginConfigProvider.class).to(FileBasedGlobalPluginConfigProvider.class);
bind(Path.class).annotatedWith(SitePath.class).toInstance(sitePath);
if (repoManager != null) {
diff --git a/java/com/google/gerrit/entities/LabelTypes.java b/java/com/google/gerrit/entities/LabelTypes.java
index 1c38c59..55a9976 100644
--- a/java/com/google/gerrit/entities/LabelTypes.java
+++ b/java/com/google/gerrit/entities/LabelTypes.java
@@ -20,6 +20,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
public class LabelTypes {
protected List<LabelType> labelTypes;
@@ -36,12 +37,12 @@
return labelTypes;
}
- public LabelType byLabel(LabelId labelId) {
- return byLabel().get(labelId.get().toLowerCase());
+ public Optional<LabelType> byLabel(LabelId labelId) {
+ return Optional.ofNullable(byLabel().get(labelId.get().toLowerCase()));
}
- public LabelType byLabel(String labelName) {
- return byLabel().get(labelName.toLowerCase());
+ public Optional<LabelType> byLabel(String labelName) {
+ return Optional.ofNullable(byLabel().get(labelName.toLowerCase()));
}
private Map<String, LabelType> byLabel() {
diff --git a/java/com/google/gerrit/entities/Project.java b/java/com/google/gerrit/entities/Project.java
index ef3cbeb..617b827 100644
--- a/java/com/google/gerrit/entities/Project.java
+++ b/java/com/google/gerrit/entities/Project.java
@@ -150,6 +150,7 @@
return builder;
}
+ @Nullable
public String getName() {
return getNameKey() != null ? getNameKey().get() : null;
}
@@ -183,7 +184,7 @@
@Override
public final String toString() {
- return Optional.of(getName()).orElse("<null>");
+ return Optional.ofNullable(getName()).orElse("<null>");
}
public abstract Builder toBuilder();
diff --git a/java/com/google/gerrit/entities/SubmitRequirement.java b/java/com/google/gerrit/entities/SubmitRequirement.java
index df03fd5..13e0b53 100644
--- a/java/com/google/gerrit/entities/SubmitRequirement.java
+++ b/java/com/google/gerrit/entities/SubmitRequirement.java
@@ -15,6 +15,8 @@
package com.google.gerrit.entities;
import com.google.auto.value.AutoValue;
+import com.google.gson.Gson;
+import com.google.gson.TypeAdapter;
import java.util.Optional;
/** Entity describing a requirement that should be met for a change to become submittable. */
@@ -62,6 +64,10 @@
return new AutoValue_SubmitRequirement.Builder();
}
+ public static TypeAdapter<SubmitRequirement> typeAdapter(Gson gson) {
+ return new AutoValue_SubmitRequirement.GsonTypeAdapter(gson);
+ }
+
@AutoValue.Builder
public abstract static class Builder {
diff --git a/java/com/google/gerrit/entities/SubmitRequirementExpression.java b/java/com/google/gerrit/entities/SubmitRequirementExpression.java
index c978347..2af1379 100644
--- a/java/com/google/gerrit/entities/SubmitRequirementExpression.java
+++ b/java/com/google/gerrit/entities/SubmitRequirementExpression.java
@@ -17,6 +17,8 @@
import com.google.auto.value.AutoValue;
import com.google.common.base.Strings;
import com.google.gerrit.common.Nullable;
+import com.google.gson.Gson;
+import com.google.gson.TypeAdapter;
import java.util.Optional;
/** Describe a applicability, blocking or override expression of a {@link SubmitRequirement}. */
@@ -41,4 +43,8 @@
/** Returns the underlying String representing this {@link SubmitRequirementExpression}. */
public abstract String expressionString();
+
+ public static TypeAdapter<SubmitRequirementExpression> typeAdapter(Gson gson) {
+ return new AutoValue_SubmitRequirementExpression.GsonTypeAdapter(gson);
+ }
}
diff --git a/java/com/google/gerrit/entities/SubmitRequirementExpressionResult.java b/java/com/google/gerrit/entities/SubmitRequirementExpressionResult.java
index f7a883e..58eb4ac 100644
--- a/java/com/google/gerrit/entities/SubmitRequirementExpressionResult.java
+++ b/java/com/google/gerrit/entities/SubmitRequirementExpressionResult.java
@@ -16,57 +16,78 @@
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
+import com.google.gson.Gson;
+import com.google.gson.TypeAdapter;
import java.util.Optional;
/** Result of evaluating a submit requirement expression on a given Change. */
@AutoValue
public abstract class SubmitRequirementExpressionResult {
- /**
- * Entity detailing the result of evaluating a Submit requirement expression. Contains an empty
- * {@link Optional} if {@link #status()} is equal to {@link Status#ERROR}.
- */
- public abstract Optional<PredicateResult> predicateResult();
+ /** Submit requirement expression for which this result is evaluated. */
+ public abstract SubmitRequirementExpression expression();
+ /** Status of evaluation. */
+ public abstract Status status();
+
+ /**
+ * Optional error message. Populated if the evaluator fails to evaluate the expression for a
+ * certain change.
+ */
public abstract Optional<String> errorMessage();
- public Status status() {
- if (predicateResult().isPresent()) {
- return predicateResult().get().status() ? Status.PASS : Status.FAIL;
- }
- return Status.ERROR;
- }
-
- public static SubmitRequirementExpressionResult create(PredicateResult predicateResult) {
- return new AutoValue_SubmitRequirementExpressionResult(
- Optional.of(predicateResult), Optional.empty());
- }
-
- public static SubmitRequirementExpressionResult error(String errorMessage) {
- return new AutoValue_SubmitRequirementExpressionResult(
- Optional.empty(), Optional.of(errorMessage));
- }
+ /**
+ * List leaf predicates that are fulfilled, for example the expression
+ *
+ * <p><i>label:code-review=+2 and branch:refs/heads/master</i>
+ *
+ * <p>has two leaf predicates:
+ *
+ * <ul>
+ * <li>label:code-review=+2
+ * <li>branch:refs/heads/master
+ * </ul>
+ *
+ * This method will return the leaf predicates that were fulfilled, for example if only the first
+ * predicate was fulfilled, the returned list will be equal to ["label:code-review=+2"].
+ */
+ public abstract ImmutableList<String> passingAtoms();
/**
- * Returns a list of leaf predicate results whose {@link PredicateResult#status()} is true. If
- * {@link #status()} is equal to {@link Status#ERROR}, an empty list is returned.
+ * List of leaf predicates that are not fulfilled. See {@link #passingAtoms()} for more details.
*/
- public ImmutableList<PredicateResult> getPassingAtoms() {
- if (predicateResult().isPresent()) {
- return predicateResult().get().getAtoms(/* status= */ true);
- }
- return ImmutableList.of();
+ public abstract ImmutableList<String> failingAtoms();
+
+ public static SubmitRequirementExpressionResult create(
+ SubmitRequirementExpression expression, PredicateResult predicateResult) {
+ return create(
+ expression,
+ predicateResult.status() ? Status.PASS : Status.FAIL,
+ predicateResult.getPassingAtoms(),
+ predicateResult.getFailingAtoms());
}
- /**
- * Returns a list of leaf predicate results whose {@link PredicateResult#status()} is false. If
- * {@link #status()} is equal to {@link Status#ERROR}, an empty list is returned.
- */
- public ImmutableList<PredicateResult> getFailingAtoms() {
- if (predicateResult().isPresent()) {
- return predicateResult().get().getAtoms(/* status= */ false);
- }
- return ImmutableList.of();
+ public static SubmitRequirementExpressionResult create(
+ SubmitRequirementExpression expression,
+ Status status,
+ ImmutableList<String> passingAtoms,
+ ImmutableList<String> failingAtoms) {
+ return new AutoValue_SubmitRequirementExpressionResult(
+ expression, status, Optional.empty(), passingAtoms, failingAtoms);
+ }
+
+ public static SubmitRequirementExpressionResult error(
+ SubmitRequirementExpression expression, String errorMessage) {
+ return new AutoValue_SubmitRequirementExpressionResult(
+ expression,
+ Status.ERROR,
+ Optional.of(errorMessage),
+ ImmutableList.of(),
+ ImmutableList.of());
+ }
+
+ public static TypeAdapter<SubmitRequirementExpressionResult> typeAdapter(Gson gson) {
+ return new AutoValue_SubmitRequirementExpressionResult.GsonTypeAdapter(gson);
}
public enum Status {
@@ -103,11 +124,25 @@
/** true if the predicate is passing for a given change. */
abstract boolean status();
+ /** Returns a list of leaf predicate results whose {@link PredicateResult#status()} is true. */
+ ImmutableList<String> getPassingAtoms() {
+ return getAtoms(/* status= */ true).stream()
+ .map(PredicateResult::predicateString)
+ .collect(ImmutableList.toImmutableList());
+ }
+
+ /** Returns a list of leaf predicate results whose {@link PredicateResult#status()} is false. */
+ ImmutableList<String> getFailingAtoms() {
+ return getAtoms(/* status= */ false).stream()
+ .map(PredicateResult::predicateString)
+ .collect(ImmutableList.toImmutableList());
+ }
+
/**
* Returns the list of leaf {@link PredicateResult} whose {@link #status()} is equal to the
* {@code status} parameter.
*/
- ImmutableList<PredicateResult> getAtoms(boolean status) {
+ private ImmutableList<PredicateResult> getAtoms(boolean status) {
ImmutableList.Builder<PredicateResult> atomsList = ImmutableList.builder();
getAtomsRecursively(atomsList, status);
return atomsList.build();
diff --git a/java/com/google/gerrit/entities/SubmitRequirementResult.java b/java/com/google/gerrit/entities/SubmitRequirementResult.java
index e28c86f..e1d5f39 100644
--- a/java/com/google/gerrit/entities/SubmitRequirementResult.java
+++ b/java/com/google/gerrit/entities/SubmitRequirementResult.java
@@ -16,11 +16,17 @@
import com.google.auto.value.AutoValue;
import com.google.auto.value.extension.memoized.Memoized;
+import com.google.gson.Gson;
+import com.google.gson.TypeAdapter;
import java.util.Optional;
+import org.eclipse.jgit.lib.ObjectId;
/** Result of evaluating a {@link SubmitRequirement} on a given Change. */
@AutoValue
public abstract class SubmitRequirementResult {
+ /** Submit requirement for which this result is evaluated. */
+ public abstract SubmitRequirement submitRequirement();
+
/** Result of evaluating a {@link SubmitRequirement#applicabilityExpression()} on a change. */
public abstract Optional<SubmitRequirementExpressionResult> applicabilityExpressionResult();
@@ -32,6 +38,9 @@
/** Result of evaluating a {@link SubmitRequirement#overrideExpression()} ()} on a change. */
public abstract Optional<SubmitRequirementExpressionResult> overrideExpressionResult();
+ /** SHA-1 of the patchset commit ID for which the submit requirement was evaluated. */
+ public abstract ObjectId patchSetCommitId();
+
@Memoized
public Status status() {
if (assertError(submittabilityExpressionResult())
@@ -53,6 +62,10 @@
return new AutoValue_SubmitRequirementResult.Builder();
}
+ public static TypeAdapter<SubmitRequirementResult> typeAdapter(Gson gson) {
+ return new AutoValue_SubmitRequirementResult.GsonTypeAdapter(gson);
+ }
+
public enum Status {
/** Submit requirement is fulfilled. */
SATISFIED,
@@ -84,6 +97,7 @@
@AutoValue.Builder
public abstract static class Builder {
+ public abstract Builder submitRequirement(SubmitRequirement submitRequirement);
public abstract Builder applicabilityExpressionResult(
Optional<SubmitRequirementExpressionResult> value);
@@ -93,6 +107,8 @@
public abstract Builder overrideExpressionResult(
Optional<SubmitRequirementExpressionResult> value);
+ public abstract Builder patchSetCommitId(ObjectId value);
+
public abstract SubmitRequirementResult build();
}
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 39db61d..7e6ab58 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -46,6 +46,7 @@
import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED;
+import static javax.servlet.http.HttpServletResponse.SC_REQUEST_TIMEOUT;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
@@ -112,6 +113,7 @@
import com.google.gerrit.server.RequestListener;
import com.google.gerrit.server.audit.ExtendedHttpAuditEvent;
import com.google.gerrit.server.cache.PerThreadCache;
+import com.google.gerrit.server.cancellation.RequestCancelledException;
import com.google.gerrit.server.change.ChangeFinder;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.config.GerritServerConfig;
@@ -225,6 +227,7 @@
public static final String XD_METHOD = "$m";
public static final int SC_UNPROCESSABLE_ENTITY = 422;
public static final int SC_TOO_MANY_REQUESTS = 429;
+ public static final int SC_CLIENT_CLOSED_REQUEST = 499;
private static final int HEAP_EST_SIZE = 10 * 8 * 1024; // Presize 10 blocks.
private static final String PLAIN_TEXT = "text/plain";
@@ -709,6 +712,25 @@
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;
diff --git a/java/com/google/gerrit/server/CommentsUtil.java b/java/com/google/gerrit/server/CommentsUtil.java
index 344549e..b18f499 100644
--- a/java/com/google/gerrit/server/CommentsUtil.java
+++ b/java/com/google/gerrit/server/CommentsUtil.java
@@ -438,7 +438,7 @@
// unignore the test in PortedCommentsIT.
Map<String, FileDiffOutput> modifiedFiles =
diffOperations.listModifiedFilesAgainstParent(
- change.getProject(), patchset.commitId(), /* parentNum= */ null);
+ change.getProject(), patchset.commitId(), /* parentNum= */ 0);
return modifiedFiles.isEmpty()
? null
: modifiedFiles.values().iterator().next().oldCommitId();
diff --git a/java/com/google/gerrit/server/PatchSetUtil.java b/java/com/google/gerrit/server/PatchSetUtil.java
index d60bc8f..326ddf4 100644
--- a/java/com/google/gerrit/server/PatchSetUtil.java
+++ b/java/com/google/gerrit/server/PatchSetUtil.java
@@ -151,8 +151,10 @@
ApprovalsUtil approvalsUtil = approvalsUtilProvider.get();
for (PatchSetApproval ap : approvalsUtil.byPatchSet(notes, change.currentPatchSetId())) {
- LabelType type = projectState.getLabelTypes(notes).byLabel(ap.label());
- if (type != null && ap.value() == 1 && type.getFunction() == LabelFunction.PATCH_SET_LOCK) {
+ Optional<LabelType> type = projectState.getLabelTypes(notes).byLabel(ap.label());
+ if (type.isPresent()
+ && ap.value() == 1
+ && type.get().getFunction() == LabelFunction.PATCH_SET_LOCK) {
return true;
}
}
diff --git a/java/com/google/gerrit/server/account/AccountsUpdate.java b/java/com/google/gerrit/server/account/AccountsUpdate.java
index 7bd2c53..93738b0 100644
--- a/java/com/google/gerrit/server/account/AccountsUpdate.java
+++ b/java/com/google/gerrit/server/account/AccountsUpdate.java
@@ -512,7 +512,10 @@
BatchRefUpdate batchRefUpdate = allUsersRepo.getRefDatabase().newBatchUpdate();
- String externalIdUpdateMessage = "Batch update for " + updatedAccounts.size() + " accounts";
+ String externalIdUpdateMessage =
+ updatedAccounts.size() == 1
+ ? Iterables.getOnlyElement(updatedAccounts).message
+ : "Batch update for " + updatedAccounts.size() + " accounts";
for (UpdatedAccount updatedAccount : updatedAccounts) {
// These updates are all for different refs (because batches never update the same account
// more than once), so there can be multiple commits in the same batch, all with the same base
diff --git a/java/com/google/gerrit/server/approval/ApprovalInference.java b/java/com/google/gerrit/server/approval/ApprovalInference.java
index 1efbd37..8d409e5 100644
--- a/java/com/google/gerrit/server/approval/ApprovalInference.java
+++ b/java/com/google/gerrit/server/approval/ApprovalInference.java
@@ -52,6 +52,7 @@
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
+import java.util.Optional;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.revwalk.RevWalk;
@@ -397,14 +398,14 @@
if (resultByUser.contains(psa.label(), psa.accountId())) {
continue;
}
- LabelType type = labelTypes.byLabel(psa.labelId());
+ Optional<LabelType> type = labelTypes.byLabel(psa.labelId());
// Only compute modified files if there is a relevant label, since this is expensive.
if (modifiedFiles == null
- && type != null
- && type.isCopyAllScoresIfListOfFilesDidNotChange()) {
+ && type.isPresent()
+ && type.get().isCopyAllScoresIfListOfFilesDidNotChange()) {
modifiedFiles = listModifiedFiles(project, ps, priorPatchSet);
}
- if (type == null) {
+ if (!type.isPresent()) {
logger.atFine().log(
"approval %d on label %s of patch set %d of change %d cannot be copied"
+ " to patch set %d because the label no longer exists on project %s",
@@ -416,8 +417,8 @@
project.getName());
continue;
}
- if (!canCopyBasedOnBooleanLabelConfigs(project, psa, ps.id(), kind, type, modifiedFiles)
- && !canCopyBasedOnCopyCondition(notes, psa, ps.id(), type, kind)) {
+ if (!canCopyBasedOnBooleanLabelConfigs(project, psa, ps.id(), kind, type.get(), modifiedFiles)
+ && !canCopyBasedOnCopyCondition(notes, psa, ps.id(), type.get(), kind)) {
continue;
}
resultByUser.put(psa.label(), psa.accountId(), psa.copyWithPatchSet(ps.id()));
diff --git a/java/com/google/gerrit/server/approval/ApprovalsUtil.java b/java/com/google/gerrit/server/approval/ApprovalsUtil.java
index b1e85e9..a1cdd99 100644
--- a/java/com/google/gerrit/server/approval/ApprovalsUtil.java
+++ b/java/com/google/gerrit/server/approval/ApprovalsUtil.java
@@ -61,6 +61,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Optional;
import java.util.Set;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.revwalk.RevWalk;
@@ -299,8 +300,12 @@
List<PatchSetApproval> cells = new ArrayList<>(approvals.size());
Date ts = update.getWhen();
for (Map.Entry<String, Short> vote : approvals.entrySet()) {
- LabelType lt = labelTypes.byLabel(vote.getKey());
- cells.add(newApproval(ps.id(), user, lt.getLabelId(), vote.getValue(), ts).build());
+ Optional<LabelType> lt = labelTypes.byLabel(vote.getKey());
+ if (!lt.isPresent()) {
+ throw new BadRequestException(
+ String.format("label \"%s\" is not a configured label", vote.getKey()));
+ }
+ cells.add(newApproval(ps.id(), user, lt.get().getLabelId(), vote.getValue(), ts).build());
}
for (PatchSetApproval psa : cells) {
update.putApproval(psa.label(), psa.value());
@@ -310,11 +315,11 @@
public static void checkLabel(LabelTypes labelTypes, String name, Short value)
throws BadRequestException {
- LabelType label = labelTypes.byLabel(name);
- if (label == null) {
+ Optional<LabelType> label = labelTypes.byLabel(name);
+ if (!label.isPresent()) {
throw new BadRequestException(String.format("label \"%s\" is not a configured label", name));
}
- if (label.getValue(value) == null) {
+ if (label.get().getValue(value) == null) {
throw new BadRequestException(
String.format("label \"%s\": %d is not a valid value", name, value));
}
diff --git a/java/com/google/gerrit/server/cache/serialize/entities/SubmitRequirementExpressionResultSerializer.java b/java/com/google/gerrit/server/cache/serialize/entities/SubmitRequirementExpressionResultSerializer.java
new file mode 100644
index 0000000..4e997b4
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/serialize/entities/SubmitRequirementExpressionResultSerializer.java
@@ -0,0 +1,45 @@
+// 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.cache.serialize.entities;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.SubmitRequirementExpression;
+import com.google.gerrit.entities.SubmitRequirementExpressionResult;
+import com.google.gerrit.server.cache.proto.Cache.SubmitRequirementExpressionResultProto;
+
+/**
+ * Serializer of a {@link SubmitRequirementExpressionResult} to {@link
+ * SubmitRequirementExpressionResultProto}.
+ */
+public class SubmitRequirementExpressionResultSerializer {
+ public static SubmitRequirementExpressionResult deserialize(
+ SubmitRequirementExpressionResultProto proto) {
+ return SubmitRequirementExpressionResult.create(
+ SubmitRequirementExpression.create(proto.getExpression()),
+ SubmitRequirementExpressionResult.Status.valueOf(proto.getStatus()),
+ proto.getPassingAtomsList().stream().collect(ImmutableList.toImmutableList()),
+ proto.getFailingAtomsList().stream().collect(ImmutableList.toImmutableList()));
+ }
+
+ public static SubmitRequirementExpressionResultProto serialize(
+ SubmitRequirementExpressionResult r) {
+ return SubmitRequirementExpressionResultProto.newBuilder()
+ .setExpression(r.expression().expressionString())
+ .setStatus(r.status().name())
+ .addAllPassingAtoms(r.passingAtoms())
+ .addAllFailingAtoms(r.failingAtoms())
+ .build();
+ }
+}
diff --git a/java/com/google/gerrit/server/cancellation/RequestCancelledException.java b/java/com/google/gerrit/server/cancellation/RequestCancelledException.java
new file mode 100644
index 0000000..3c668fb
--- /dev/null
+++ b/java/com/google/gerrit/server/cancellation/RequestCancelledException.java
@@ -0,0 +1,68 @@
+// 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.gerrit.common.Nullable;
+import java.util.Optional;
+import org.apache.commons.lang.WordUtils;
+
+/** Exception to signal that the current request is cancelled and should be aborted. */
+public class RequestCancelledException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ private final RequestStateProvider.Reason cancellationReason;
+ private final Optional<String> cancellationMessage;
+
+ /**
+ * Create a {@code RequestCancelledException}.
+ *
+ * @param cancellationReason the reason why the request is cancelled
+ * @param cancellationMessage an optional message providing details about the cancellation
+ */
+ public RequestCancelledException(
+ RequestStateProvider.Reason cancellationReason, @Nullable String cancellationMessage) {
+ super(createMessage(cancellationReason, cancellationMessage));
+ this.cancellationReason = cancellationReason;
+ this.cancellationMessage = Optional.ofNullable(cancellationMessage);
+ }
+
+ private static String createMessage(
+ RequestStateProvider.Reason cancellationReason, @Nullable String message) {
+ StringBuilder messageBuilder = new StringBuilder();
+ messageBuilder.append(String.format("Request cancelled: %s", cancellationReason.name()));
+ if (message != null) {
+ messageBuilder.append(String.format(" (%s)", message));
+ }
+ return messageBuilder.toString();
+ }
+
+ /** Returns the reason why the request is cancelled. */
+ public RequestStateProvider.Reason getCancellationReason() {
+ return cancellationReason;
+ }
+
+ /** Returns the cancellation reason as a user-readable string. */
+ public String formatCancellationReason() {
+ return WordUtils.capitalizeFully(cancellationReason.name().replaceAll("_", " "));
+ }
+
+ /**
+ * Returns a message providing details about the cancellation, or {@link Optional#empty()} if none
+ * is available.
+ */
+ public Optional<String> getCancellationMessage() {
+ return cancellationMessage;
+ }
+}
diff --git a/java/com/google/gerrit/server/cancellation/RequestStateProvider.java b/java/com/google/gerrit/server/cancellation/RequestStateProvider.java
new file mode 100644
index 0000000..e1716eb
--- /dev/null
+++ b/java/com/google/gerrit/server/cancellation/RequestStateProvider.java
@@ -0,0 +1,59 @@
+// 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.gerrit.common.Nullable;
+
+/** Interface that provides information about the state of the current request. */
+public interface RequestStateProvider {
+ /**
+ * Checks whether the current request is cancelled.
+ *
+ * <p>Invoked by Gerrit to check whether the current request is cancelled and should be aborted.
+ *
+ * <p>If the current request is cancelled {@link OnCancelled#onCancel(Reason, String)} is invoked
+ * on the provided callback.
+ *
+ * @param onCancelled callback that should be invoked if the request is cancelled
+ */
+ void checkIfCancelled(OnCancelled onCancelled);
+
+ /** Callback interface to be invoked if a request is cancelled. */
+ interface OnCancelled {
+ /**
+ * Callback that is invoked if the request is cancelled.
+ *
+ * @param reason the reason for the cancellation of the request
+ * @param message an optional message providing details about the cancellation
+ */
+ void onCancel(Reason reason, @Nullable String message);
+ }
+
+ /** Reason why a request is cancelled. */
+ enum Reason {
+ /** The client got disconnected or has cancelled the request. */
+ CLIENT_CLOSED_REQUEST,
+
+ /** The deadline that the client provided for the request exceeded. */
+ CLIENT_PROVIDED_DEADLINE_EXCEEDED,
+
+ /**
+ * A server-side deadline for the request exceeded.
+ *
+ * <p>Server-side deadlines are usually configurable, but may also be hard-coded.
+ */
+ SERVER_DEADLINE_EXCEEDED;
+ }
+}
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index e9c9946..ff8d8cb 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -63,7 +63,6 @@
import com.google.gerrit.entities.SubmitRequirement;
import com.google.gerrit.entities.SubmitRequirementExpression;
import com.google.gerrit.entities.SubmitRequirementExpressionResult;
-import com.google.gerrit.entities.SubmitRequirementExpressionResult.PredicateResult;
import com.google.gerrit.entities.SubmitRequirementResult;
import com.google.gerrit.entities.SubmitTypeRecord;
import com.google.gerrit.exceptions.StorageException;
@@ -411,10 +410,8 @@
SubmitRequirementExpressionInfo info = new SubmitRequirementExpressionInfo();
info.expression = expression.expressionString();
info.fulfilled = result.status().equals(SubmitRequirementExpressionResult.Status.PASS);
- info.passingAtoms =
- result.getPassingAtoms().stream().map(PredicateResult::predicateString).collect(toList());
- info.failingAtoms =
- result.getFailingAtoms().stream().map(PredicateResult::predicateString).collect(toList());
+ info.passingAtoms = result.passingAtoms();
+ info.failingAtoms = result.failingAtoms();
return info;
}
diff --git a/java/com/google/gerrit/server/change/FileInfoJson.java b/java/com/google/gerrit/server/change/FileInfoJson.java
index ad6f9c7..ab557dc 100644
--- a/java/com/google/gerrit/server/change/FileInfoJson.java
+++ b/java/com/google/gerrit/server/change/FileInfoJson.java
@@ -46,7 +46,8 @@
*
* @param change a Gerrit change.
* @param objectId a commit SHA-1 identifying a patchset commit.
- * @param parentNum an integer identifying the parent number used for comparison.
+ * @param parentNum 1-based integer identifying the parent number used for comparison. If zero,
+ * the only parent will be used or the auto-merge if {@code newCommit} is a merge commit.
* @return a mapping of the file paths to their related diff information.
*/
default Map<String, FileInfo> getFileInfoMap(Change change, ObjectId objectId, int parentNum)
@@ -74,7 +75,8 @@
*
* @param project a project identifying a repository.
* @param objectId a commit SHA-1 identifying a patchset commit.
- * @param parentNum an integer identifying the parent number used for comparison.
+ * @param parentNum 1-based integer identifying the parent number used for comparison. If zero,
+ * the only parent will be used or the auto-merge if {@code newCommit} is a merge commit.
* @return a mapping of the file paths to their related diff information.
*/
Map<String, FileInfo> getFileInfoMap(Project.NameKey project, ObjectId objectId, int parentNum)
diff --git a/java/com/google/gerrit/server/change/FileInfoJsonExperimentImpl.java b/java/com/google/gerrit/server/change/FileInfoJsonExperimentImpl.java
index 3f7ce68..81f014d 100644
--- a/java/com/google/gerrit/server/change/FileInfoJsonExperimentImpl.java
+++ b/java/com/google/gerrit/server/change/FileInfoJsonExperimentImpl.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.change;
+import com.google.common.annotations.VisibleForTesting;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
@@ -33,7 +34,9 @@
* FileInfoJsonNewImpl}.
*/
public class FileInfoJsonExperimentImpl implements FileInfoJson {
- private final String NEW_DIFF_CACHE_FEATURE = "GerritBackendRequestFeature__use_new_diff_cache";
+ @VisibleForTesting
+ public static final String NEW_DIFF_CACHE_FEATURE =
+ "GerritBackendRequestFeature__use_new_diff_cache";
private final FileInfoJsonOldImpl oldImpl;
private final FileInfoJsonNewImpl newImpl;
diff --git a/java/com/google/gerrit/server/change/FileInfoJsonNewImpl.java b/java/com/google/gerrit/server/change/FileInfoJsonNewImpl.java
index 1ca2c93..7277404 100644
--- a/java/com/google/gerrit/server/change/FileInfoJsonNewImpl.java
+++ b/java/com/google/gerrit/server/change/FileInfoJsonNewImpl.java
@@ -47,8 +47,11 @@
throws ResourceConflictException, PatchListNotAvailableException {
try {
if (base == null) {
+ // Setting parentNum=0 requests the default parent, which is the only parent for
+ // single-parent commits, or the auto-merge otherwise
return asFileInfo(
- diffs.listModifiedFilesAgainstParent(change.getProject(), objectId, null));
+ diffs.listModifiedFilesAgainstParent(
+ change.getProject(), objectId, /* parentNum= */ 0));
}
return asFileInfo(diffs.listModifiedFiles(change.getProject(), base.commitId(), objectId));
} catch (DiffNotAvailableException e) {
@@ -63,7 +66,7 @@
throws ResourceConflictException, PatchListNotAvailableException {
try {
Map<String, FileDiffOutput> modifiedFiles =
- diffs.listModifiedFilesAgainstParent(project, objectId, parent + 1);
+ diffs.listModifiedFilesAgainstParent(project, objectId, parent);
return asFileInfo(modifiedFiles);
} catch (DiffNotAvailableException e) {
convertException(e);
diff --git a/java/com/google/gerrit/server/change/FileInfoJsonOldImpl.java b/java/com/google/gerrit/server/change/FileInfoJsonOldImpl.java
index 55d162a..0570296 100644
--- a/java/com/google/gerrit/server/change/FileInfoJsonOldImpl.java
+++ b/java/com/google/gerrit/server/change/FileInfoJsonOldImpl.java
@@ -60,10 +60,10 @@
Project.NameKey project, ObjectId objectId, int parentNum)
throws ResourceConflictException, PatchListNotAvailableException {
PatchListKey key =
- parentNum == -1
+ parentNum == 0
? PatchListKey.againstDefaultBase(objectId, Whitespace.IGNORE_NONE)
: PatchListKey.againstParentNum(
- parentNum + 1, objectId, DiffPreferencesInfo.Whitespace.IGNORE_NONE);
+ parentNum, objectId, DiffPreferencesInfo.Whitespace.IGNORE_NONE);
return toFileInfoMap(project, key);
}
diff --git a/java/com/google/gerrit/server/change/LabelNormalizer.java b/java/com/google/gerrit/server/change/LabelNormalizer.java
index 30343d4..b5527d7 100644
--- a/java/com/google/gerrit/server/change/LabelNormalizer.java
+++ b/java/com/google/gerrit/server/change/LabelNormalizer.java
@@ -33,6 +33,7 @@
import com.google.inject.Singleton;
import java.util.Collection;
import java.util.List;
+import java.util.Optional;
/**
* Normalizes votes on labels according to project config.
@@ -101,12 +102,12 @@
unchanged.add(psa);
continue;
}
- LabelType label = labelTypes.byLabel(psa.labelId());
- if (label == null) {
+ Optional<LabelType> label = labelTypes.byLabel(psa.labelId());
+ if (!label.isPresent()) {
deleted.add(psa);
continue;
}
- PatchSetApproval copy = applyTypeFloor(label, psa);
+ PatchSetApproval copy = applyTypeFloor(label.get(), psa);
if (copy.value() != psa.value()) {
updated.add(copy);
} else {
diff --git a/java/com/google/gerrit/server/change/LabelsJson.java b/java/com/google/gerrit/server/change/LabelsJson.java
index acff03c..5ce121b 100644
--- a/java/com/google/gerrit/server/change/LabelsJson.java
+++ b/java/com/google/gerrit/server/change/LabelsJson.java
@@ -57,6 +57,7 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
@@ -103,9 +104,9 @@
for (SubmitRecord rec : submitRecords(cd)) {
if (rec.labels != null) {
for (SubmitRecord.Label r : rec.labels) {
- LabelType type = labelTypes.byLabel(r.label);
- if (type != null && (!isMerged || type.isAllowPostSubmit())) {
- toCheck.put(type.getName(), type);
+ Optional<LabelType> type = labelTypes.byLabel(r.label);
+ if (type.isPresent() && (!isMerged || type.get().isAllowPostSubmit())) {
+ toCheck.put(type.get().getName(), type.get());
}
}
}
@@ -120,18 +121,18 @@
continue;
}
for (SubmitRecord.Label r : rec.labels) {
- LabelType type = labelTypes.byLabel(r.label);
- if (type == null || (isMerged && !type.isAllowPostSubmit())) {
+ Optional<LabelType> type = labelTypes.byLabel(r.label);
+ if (!type.isPresent() || (isMerged && !type.get().isAllowPostSubmit())) {
continue;
}
- for (LabelValue v : type.getValues()) {
- boolean ok = can.contains(new LabelPermission.WithValue(type, v));
+ for (LabelValue v : type.get().getValues()) {
+ boolean ok = can.contains(new LabelPermission.WithValue(type.get(), v));
if (isMerged) {
if (labels == null) {
labels = currentLabels(filterApprovalsBy, cd);
}
- short prev = labels.getOrDefault(type.getName(), (short) 0);
+ short prev = labels.getOrDefault(type.get().getName(), (short) 0);
ok &= v.getValue() >= prev;
}
if (ok) {
@@ -176,21 +177,21 @@
setAllApprovals(accountLoader, cd, labels);
}
for (Map.Entry<String, LabelWithStatus> e : labels.entrySet()) {
- LabelType type = labelTypes.byLabel(e.getKey());
- if (type == null) {
+ Optional<LabelType> type = labelTypes.byLabel(e.getKey());
+ if (!type.isPresent()) {
continue;
}
if (standard) {
for (PatchSetApproval psa : cd.currentApprovals()) {
- if (type.matches(psa)) {
+ if (type.get().matches(psa)) {
short val = psa.value();
Account.Id accountId = psa.accountId();
- setLabelScores(accountLoader, type, e.getValue(), val, accountId);
+ setLabelScores(accountLoader, type.get(), e.getValue(), val, accountId);
}
}
}
if (detailed) {
- setLabelValues(type, e.getValue());
+ setLabelValues(type.get(), e.getValue());
}
}
return labels;
@@ -261,9 +262,9 @@
MultimapBuilder.hashKeys().hashSetValues().build();
for (PatchSetApproval a : cd.currentApprovals()) {
allUsers.add(a.accountId());
- LabelType type = labelTypes.byLabel(a.labelId());
- if (type != null) {
- labelNames.add(type.getName());
+ Optional<LabelType> type = labelTypes.byLabel(a.labelId());
+ if (type.isPresent()) {
+ labelNames.add(type.get().getName());
// Not worth the effort to distinguish between votable/non-votable for 0
// values on closed changes, since they can't vote anyway.
current.put(a.accountId(), a);
@@ -292,8 +293,8 @@
if (detailed) {
labels.entrySet().stream()
- .filter(e -> labelTypes.byLabel(e.getKey()) != null)
- .forEach(e -> setLabelValues(labelTypes.byLabel(e.getKey()), e.getValue()));
+ .filter(e -> labelTypes.byLabel(e.getKey()).isPresent())
+ .forEach(e -> setLabelValues(labelTypes.byLabel(e.getKey()).get(), e.getValue()));
}
for (Account.Id accountId : allUsers) {
@@ -308,16 +309,16 @@
}
}
for (PatchSetApproval psa : current.get(accountId)) {
- LabelType type = labelTypes.byLabel(psa.labelId());
- if (type == null) {
+ Optional<LabelType> type = labelTypes.byLabel(psa.labelId());
+ if (!type.isPresent()) {
continue;
}
short val = psa.value();
- ApprovalInfo info = byLabel.get(type.getName());
+ ApprovalInfo info = byLabel.get(type.get().getName());
if (info != null) {
info.value = Integer.valueOf(val);
- info.permittedVotingRange = pvr.getOrDefault(type.getName(), null);
+ info.permittedVotingRange = pvr.getOrDefault(type.get().getName(), null);
info.date = psa.granted();
info.tag = psa.tag().orElse(null);
if (psa.postSubmit()) {
@@ -328,7 +329,7 @@
continue;
}
- setLabelScores(accountLoader, type, labels.get(type.getName()), val, accountId);
+ setLabelScores(accountLoader, type.get(), labels.get(type.get().getName()), val, accountId);
}
}
return labels;
@@ -428,24 +429,24 @@
PermissionBackend.ForChange perm = permissionBackend.absentUser(accountId).change(cd);
Map<String, VotingRangeInfo> pvr = getPermittedVotingRanges(permittedLabels(accountId, cd));
for (Map.Entry<String, LabelWithStatus> e : labels.entrySet()) {
- LabelType lt = labelTypes.byLabel(e.getKey());
- if (lt == null) {
+ Optional<LabelType> lt = labelTypes.byLabel(e.getKey());
+ if (!lt.isPresent()) {
// Ignore submit record for undefined label; likely the submit rule
// author didn't intend for the label to show up in the table.
continue;
}
Integer value;
- VotingRangeInfo permittedVotingRange = pvr.getOrDefault(lt.getName(), null);
+ VotingRangeInfo permittedVotingRange = pvr.getOrDefault(lt.get().getName(), null);
String tag = null;
Timestamp date = null;
- PatchSetApproval psa = current.get(accountId, lt.getName());
+ PatchSetApproval psa = current.get(accountId, lt.get().getName());
if (psa != null) {
value = Integer.valueOf(psa.value());
if (value == 0) {
// This may be a dummy approval that was inserted when the reviewer
// was added. Explicitly check whether the user can vote on this
// label.
- value = perm.test(new LabelPermission(lt)) ? 0 : null;
+ value = perm.test(new LabelPermission(lt.get())) ? 0 : null;
}
tag = psa.tag().orElse(null);
date = psa.granted();
@@ -456,7 +457,7 @@
// Either the user cannot vote on this label, or they were added as a
// reviewer but have not responded yet. Explicitly check whether the
// user can vote on this label.
- value = perm.test(new LabelPermission(lt)) ? 0 : null;
+ value = perm.test(new LabelPermission(lt.get())) ? 0 : null;
}
addApproval(
e.getValue().label(),
diff --git a/java/com/google/gerrit/server/change/ReviewerJson.java b/java/com/google/gerrit/server/change/ReviewerJson.java
index d5b74a8..6189708 100644
--- a/java/com/google/gerrit/server/change/ReviewerJson.java
+++ b/java/com/google/gerrit/server/change/ReviewerJson.java
@@ -38,6 +38,7 @@
import com.google.inject.Singleton;
import java.util.Collection;
import java.util.List;
+import java.util.Optional;
import java.util.TreeMap;
@Singleton
@@ -107,10 +108,8 @@
out.approvals = new TreeMap<>(labelTypes.nameComparator());
for (PatchSetApproval ca : approvals) {
- LabelType at = labelTypes.byLabel(ca.labelId());
- if (at != null) {
- out.approvals.put(at.getName(), formatValue(ca.value()));
- }
+ Optional<LabelType> at = labelTypes.byLabel(ca.labelId());
+ at.ifPresent(lt -> out.approvals.put(lt.getName(), formatValue(ca.value())));
}
// Add dummy approvals for all permitted labels for the user even if they
@@ -125,13 +124,13 @@
}
for (SubmitRecord.Label label : rec.labels) {
String name = label.label;
- LabelType type = labelTypes.byLabel(name);
- if (out.approvals.containsKey(name) || type == null) {
+ Optional<LabelType> type = labelTypes.byLabel(name);
+ if (out.approvals.containsKey(name) || !type.isPresent()) {
continue;
}
try {
- perm.check(new LabelPermission(type));
+ perm.check(new LabelPermission(type.get()));
out.approvals.put(name, formatValue((short) 0));
} catch (AuthException e) {
// Do nothing.
diff --git a/java/com/google/gerrit/server/config/FileBasedGlobalPluginConfigProvider.java b/java/com/google/gerrit/server/config/FileBasedGlobalPluginConfigProvider.java
new file mode 100644
index 0000000..098d2c2
--- /dev/null
+++ b/java/com/google/gerrit/server/config/FileBasedGlobalPluginConfigProvider.java
@@ -0,0 +1,57 @@
+// 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.config;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.nio.file.Path;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+
+@Singleton
+public class FileBasedGlobalPluginConfigProvider implements GlobalPluginConfigProvider {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private final SitePaths site;
+
+ @Inject
+ FileBasedGlobalPluginConfigProvider(SitePaths site) {
+ this.site = site;
+ }
+
+ @Override
+ public Config get(String pluginName) {
+ Path pluginConfigFile = site.etc_dir.resolve(pluginName + ".config");
+ FileBasedConfig cfg = new FileBasedConfig(pluginConfigFile.toFile(), FS.DETECTED);
+ if (!cfg.getFile().exists()) {
+ logger.atInfo().log("No %s; assuming defaults", pluginConfigFile.toAbsolutePath());
+ return cfg;
+ }
+
+ try {
+ cfg.load();
+ } catch (ConfigInvalidException e) {
+ // This is an error in user input, don't spam logs with a stack trace.
+ logger.atWarning().log(
+ "Failed to load %s: %s", pluginConfigFile.toAbsolutePath(), e.getMessage());
+ } catch (IOException e) {
+ logger.atWarning().withCause(e).log("Failed to load %s", pluginConfigFile.toAbsolutePath());
+ }
+ return cfg;
+ }
+}
diff --git a/java/com/google/gerrit/server/config/GerritServerConfigModule.java b/java/com/google/gerrit/server/config/GerritServerConfigModule.java
index 27d1d58..da85834 100644
--- a/java/com/google/gerrit/server/config/GerritServerConfigModule.java
+++ b/java/com/google/gerrit/server/config/GerritServerConfigModule.java
@@ -66,6 +66,7 @@
bind(Config.class)
.annotatedWith(GerritServerConfig.class)
.toProvider(GerritServerConfigProvider.class);
+ bind(GlobalPluginConfigProvider.class).to(FileBasedGlobalPluginConfigProvider.class);
bind(SecureStore.class).toProvider(SecureStoreProvider.class).in(SINGLETON);
bind(Boolean.class)
.annotatedWith(GerritIsReplica.class)
diff --git a/java/com/google/gerrit/server/config/GlobalPluginConfigProvider.java b/java/com/google/gerrit/server/config/GlobalPluginConfigProvider.java
new file mode 100644
index 0000000..847708a
--- /dev/null
+++ b/java/com/google/gerrit/server/config/GlobalPluginConfigProvider.java
@@ -0,0 +1,21 @@
+// 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.config;
+
+import org.eclipse.jgit.lib.Config;
+
+public interface GlobalPluginConfigProvider {
+ Config get(String pluginName);
+}
diff --git a/java/com/google/gerrit/server/config/PluginConfigFactory.java b/java/com/google/gerrit/server/config/PluginConfigFactory.java
index c49e928..bd4b661 100644
--- a/java/com/google/gerrit/server/config/PluginConfigFactory.java
+++ b/java/com/google/gerrit/server/config/PluginConfigFactory.java
@@ -16,7 +16,6 @@
import static com.google.gerrit.server.project.ProjectCache.noSuchProject;
-import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Project;
import com.google.gerrit.server.plugins.Plugin;
import com.google.gerrit.server.plugins.ReloadPluginListener;
@@ -28,22 +27,15 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
-import java.io.IOException;
-import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
-import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.util.FS;
@Singleton
public class PluginConfigFactory implements ReloadPluginListener {
- private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
private static final String EXTENSION = ".config";
- private final SitePaths site;
+ private final GlobalPluginConfigProvider globalPluginConfigProvider;
private final Provider<Config> cfgProvider;
private final ProjectCache projectCache;
private final ProjectState.Factory projectStateFactory;
@@ -54,12 +46,12 @@
@Inject
PluginConfigFactory(
- SitePaths site,
@GerritServerConfig Provider<Config> cfgProvider,
+ GlobalPluginConfigProvider globalPluginConfigProvider,
ProjectCache projectCache,
ProjectState.Factory projectStateFactory,
SecureStore secureStore) {
- this.site = site;
+ this.globalPluginConfigProvider = globalPluginConfigProvider;
this.cfgProvider = cfgProvider;
this.projectCache = projectCache;
this.projectStateFactory = projectStateFactory;
@@ -211,25 +203,9 @@
return pluginConfigs.get(pluginName);
}
- Path pluginConfigFile = site.etc_dir.resolve(pluginName + ".config");
- FileBasedConfig cfg = new FileBasedConfig(pluginConfigFile.toFile(), FS.DETECTED);
+ Config cfg = globalPluginConfigProvider.get(pluginName);
GlobalPluginConfig pluginConfig = new GlobalPluginConfig(pluginName, cfg, secureStore);
pluginConfigs.put(pluginName, pluginConfig);
- if (!cfg.getFile().exists()) {
- logger.atInfo().log("No %s; assuming defaults", pluginConfigFile.toAbsolutePath());
- return pluginConfig;
- }
-
- try {
- cfg.load();
- } catch (ConfigInvalidException e) {
- // This is an error in user input, don't spam logs with a stack trace.
- logger.atWarning().log(
- "Failed to load %s: %s", pluginConfigFile.toAbsolutePath(), e.getMessage());
- } catch (IOException e) {
- logger.atWarning().withCause(e).log("Failed to load %s", pluginConfigFile.toAbsolutePath());
- }
-
return pluginConfig;
}
diff --git a/java/com/google/gerrit/server/events/EventFactory.java b/java/com/google/gerrit/server/events/EventFactory.java
index 3a4dcff..1bb694a 100644
--- a/java/com/google/gerrit/server/events/EventFactory.java
+++ b/java/com/google/gerrit/server/events/EventFactory.java
@@ -71,6 +71,7 @@
import java.util.Collection;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.Set;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
@@ -399,7 +400,7 @@
try {
Map<String, FileDiffOutput> modifiedFiles =
diffOperations.listModifiedFilesAgainstParent(
- change.getProject(), patchSet.commitId(), /* parent= */ null);
+ change.getProject(), patchSet.commitId(), /* parent= */ 0);
for (FileDiffOutput diff : modifiedFiles.values()) {
if (patchSetAttribute.files == null) {
@@ -456,7 +457,7 @@
Map<String, FileDiffOutput> modifiedFiles =
diffOperations.listModifiedFilesAgainstParent(
- change.getProject(), patchSet.commitId(), /* parent= */ null);
+ change.getProject(), patchSet.commitId(), /* parent= */ 0);
for (FileDiffOutput fileDiff : modifiedFiles.values()) {
p.sizeDeletions += fileDiff.deletions();
p.sizeInsertions += fileDiff.insertions();
@@ -535,10 +536,8 @@
a.grantedOn = approval.granted().getTime() / 1000L;
a.oldValue = null;
- LabelType lt = labelTypes.byLabel(approval.labelId());
- if (lt != null) {
- a.description = lt.getName();
- }
+ Optional<LabelType> lt = labelTypes.byLabel(approval.labelId());
+ lt.ifPresent(l -> a.description = l.getName());
return a;
}
diff --git a/java/com/google/gerrit/server/events/StreamEventsApiListener.java b/java/com/google/gerrit/server/events/StreamEventsApiListener.java
index 439f53e..edd1928 100644
--- a/java/com/google/gerrit/server/events/StreamEventsApiListener.java
+++ b/java/com/google/gerrit/server/events/StreamEventsApiListener.java
@@ -23,7 +23,6 @@
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
-import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.LabelTypes;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
@@ -202,10 +201,7 @@
a.oldValue = Short.toString(oldApprovals.get(approval.getKey()));
}
}
- LabelType lt = labelTypes.byLabel(approval.getKey());
- if (lt != null) {
- a.description = lt.getName();
- }
+ labelTypes.byLabel(approval.getKey()).ifPresent(lt -> a.description = lt.getName());
if (approval.getValue() != null) {
a.value = Short.toString(approval.getValue());
}
diff --git a/java/com/google/gerrit/server/git/MergeUtil.java b/java/com/google/gerrit/server/git/MergeUtil.java
index 1da14f8..a5ea24d 100644
--- a/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/java/com/google/gerrit/server/git/MergeUtil.java
@@ -600,11 +600,11 @@
} else if (isVerified(a.labelId())) {
tag = "Tested-by";
} else {
- final LabelType lt = project.getLabelTypes().byLabel(a.labelId());
- if (lt == null) {
+ final Optional<LabelType> lt = project.getLabelTypes().byLabel(a.labelId());
+ if (!lt.isPresent()) {
continue;
}
- tag = lt.getName();
+ tag = lt.get().getName();
}
if (!contains(footers, new FooterKey(tag), identbuf.toString())) {
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 454df66..d074f1e 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -114,6 +114,7 @@
import com.google.gerrit.server.RequestListener;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.approval.ApprovalsUtil;
+import com.google.gerrit.server.cancellation.RequestCancelledException;
import com.google.gerrit.server.change.AttentionSetUnchangedOp;
import com.google.gerrit.server.change.ChangeInserter;
import com.google.gerrit.server.change.NotifyResolver;
@@ -640,8 +641,17 @@
Task commandProgress = progress.beginSubTask("refs", UNKNOWN);
commands =
commands.stream().map(c -> wrapReceiveCommand(c, commandProgress)).collect(toList());
- processCommandsUnsafe(commands, progress);
- rejectRemaining(commands, INTERNAL_SERVER_ERROR);
+
+ 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()));
+ }
+ rejectRemaining(commands, msg.toString());
+ }
// This sends error messages before the 'done' string of the progress monitor is sent.
// Currently, the test framework relies on this ordering to understand if pushes completed
diff --git a/java/com/google/gerrit/server/git/receive/ReplaceOp.java b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
index f00b48eb..b2a31b9 100644
--- a/java/com/google/gerrit/server/git/receive/ReplaceOp.java
+++ b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
@@ -465,10 +465,10 @@
continue;
}
- LabelType lt = projectState.getLabelTypes().byLabel(a.labelId());
- if (lt != null) {
- current.put(lt.getName(), a);
- }
+ projectState
+ .getLabelTypes()
+ .byLabel(a.labelId())
+ .ifPresent(l -> current.put(l.getName(), a));
}
}
return current;
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index 810cd4d..1ee12fe 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -605,7 +605,7 @@
for (PatchSetApproval a : cd.currentApprovals()) {
if (a.value() != 0 && !a.isLegacySubmit()) {
allApprovals.add(formatLabel(a.label(), a.value(), a.accountId()));
- LabelType labelType = cd.getLabelTypes().byLabel(a.labelId());
+ Optional<LabelType> labelType = cd.getLabelTypes().byLabel(a.labelId());
allApprovals.addAll(getMaxMinAnyLabels(a.label(), a.value(), labelType, a.accountId()));
if (owners && cd.change().getOwner().equals(a.accountId())) {
allApprovals.add(formatLabel(a.label(), a.value(), ChangeQueryBuilder.OWNER_ACCOUNT_ID));
@@ -622,13 +622,15 @@
}
private static List<String> getMaxMinAnyLabels(
- String label, short labelVal, LabelType labelType, @Nullable Account.Id accountId) {
+ String label, short labelVal, Optional<LabelType> labelType, @Nullable Account.Id accountId) {
List<String> labels = new ArrayList<>();
- if (labelVal == labelType.getMaxPositive()) {
- labels.add(formatLabel(label, MagicLabelValue.MAX.name(), accountId));
- }
- if (labelVal == labelType.getMaxNegative()) {
- labels.add(formatLabel(label, MagicLabelValue.MIN.name(), accountId));
+ if (labelType.isPresent()) {
+ if (labelVal == labelType.get().getMaxPositive()) {
+ labels.add(formatLabel(label, MagicLabelValue.MAX.name(), accountId));
+ }
+ if (labelVal == labelType.get().getMaxNegative()) {
+ labels.add(formatLabel(label, MagicLabelValue.MIN.name(), accountId));
+ }
}
labels.add(formatLabel(label, MagicLabelValue.ANY.name(), accountId));
return labels;
diff --git a/java/com/google/gerrit/server/mail/send/MergedSender.java b/java/com/google/gerrit/server/mail/send/MergedSender.java
index 6af2345..56528df 100644
--- a/java/com/google/gerrit/server/mail/send/MergedSender.java
+++ b/java/com/google/gerrit/server/mail/send/MergedSender.java
@@ -79,14 +79,14 @@
Table<Account.Id, String, PatchSetApproval> pos = HashBasedTable.create();
Table<Account.Id, String, PatchSetApproval> neg = HashBasedTable.create();
for (PatchSetApproval ca : args.approvalsUtil.byPatchSet(changeData.notes(), patchSet.id())) {
- LabelType lt = labelTypes.byLabel(ca.labelId());
- if (lt == null) {
+ Optional<LabelType> lt = labelTypes.byLabel(ca.labelId());
+ if (!lt.isPresent()) {
continue;
}
if (ca.value() > 0) {
- pos.put(ca.accountId(), lt.getName(), ca);
+ pos.put(ca.accountId(), lt.get().getName(), ca);
} else if (ca.value() < 0) {
- neg.put(ca.accountId(), lt.getName(), ca);
+ neg.put(ca.accountId(), lt.get().getName(), ca);
}
}
diff --git a/java/com/google/gerrit/server/notedb/ChangeNoteJson.java b/java/com/google/gerrit/server/notedb/ChangeNoteJson.java
index 483b2e9..4c41a12 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNoteJson.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNoteJson.java
@@ -14,9 +14,17 @@
package com.google.gerrit.server.notedb;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.EntitiesAdapterFactory;
+import com.google.gerrit.json.EnumTypeAdapterFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import java.io.IOException;
import java.sql.Timestamp;
@Singleton
@@ -26,6 +34,11 @@
static Gson newGson() {
return new GsonBuilder()
.registerTypeAdapter(Timestamp.class, new CommentTimestampAdapter().nullSafe())
+ .registerTypeAdapterFactory(new EnumTypeAdapterFactory())
+ .registerTypeAdapterFactory(EntitiesAdapterFactory.create())
+ .registerTypeAdapter(
+ new TypeLiteral<ImmutableList<String>>() {}.getType(),
+ new ImmutableListAdapter().nullSafe())
.setPrettyPrinting()
.create();
}
@@ -33,4 +46,27 @@
public Gson getGson() {
return gson;
}
+
+ static class ImmutableListAdapter extends TypeAdapter<ImmutableList<String>> {
+
+ @Override
+ public void write(JsonWriter out, ImmutableList<String> value) throws IOException {
+ out.beginArray();
+ for (String v : value) {
+ out.value(v);
+ }
+ out.endArray();
+ }
+
+ @Override
+ public ImmutableList<String> read(JsonReader in) throws IOException {
+ ImmutableList.Builder<String> builder = ImmutableList.builder();
+ in.beginArray();
+ while (in.hasNext()) {
+ builder.add(in.nextString());
+ }
+ in.endArray();
+ return builder.build();
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotes.java b/java/com/google/gerrit/server/notedb/ChangeNotes.java
index 5daf28c..6684493 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -50,6 +50,7 @@
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.entities.RobotComment;
import com.google.gerrit.entities.SubmitRecord;
+import com.google.gerrit.entities.SubmitRequirementResult;
import com.google.gerrit.server.AssigneeStatusUpdate;
import com.google.gerrit.server.ReviewerByEmailSet;
import com.google.gerrit.server.ReviewerSet;
@@ -413,6 +414,16 @@
}
/**
+ * Returns the evaluated submit requirements for the change. We only intend to store submit
+ * requirements in NoteDb for closed changes, hence the result will be an empty list for active
+ * changes, or a list of submit requirements results otherwise. For closed changes, the results
+ * represent the state of evaluating submit requirements for this change when it was merged.
+ */
+ public ImmutableList<SubmitRequirementResult> getSubmitRequirementsResult() {
+ return state.submitRequirementsResult();
+ }
+
+ /**
* @return an ImmutableSet of Account.Ids of all users that have been assigned to this change. The
* order of the set is the order in which they were assigned.
*/
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index 8be0d82..2a53c29 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -68,6 +68,7 @@
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.entities.SubmitRecord;
+import com.google.gerrit.entities.SubmitRequirementResult;
import com.google.gerrit.metrics.Timer0;
import com.google.gerrit.server.AssigneeStatusUpdate;
import com.google.gerrit.server.ReviewerByEmailSet;
@@ -125,6 +126,7 @@
private final List<AssigneeStatusUpdate> assigneeUpdates;
private final List<SubmitRecord> submitRecords;
private final ListMultimap<ObjectId, HumanComment> humanComments;
+ private final List<SubmitRequirementResult> submitRequirementResults;
private final Map<PatchSet.Id, PatchSet.Builder> patchSets;
private final Set<PatchSet.Id> deletedPatchSets;
private final Map<PatchSet.Id, PatchSetState> patchSetStates;
@@ -187,6 +189,7 @@
submitRecords = Lists.newArrayListWithExpectedSize(1);
allChangeMessages = new ArrayList<>();
humanComments = MultimapBuilder.hashKeys().arrayListValues().build();
+ submitRequirementResults = new ArrayList<>();
patchSets = new HashMap<>();
deletedPatchSets = new HashSet<>();
patchSetStates = new HashMap<>();
@@ -259,6 +262,7 @@
submitRecords,
buildAllMessages(),
humanComments,
+ submitRequirementResults,
firstNonNull(isPrivate, false),
firstNonNull(workInProgress, false),
firstNonNull(hasReviewStarted, true),
@@ -774,6 +778,9 @@
for (HumanComment c : e.getValue().getEntities()) {
humanComments.put(e.getKey(), c);
}
+ for (SubmitRequirementResult sr : e.getValue().getSubmitRequirementsResult()) {
+ submitRequirementResults.add(sr);
+ }
}
for (PatchSet.Builder b : patchSets.values()) {
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesState.java b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
index 33bc039..e7da025 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesState.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
@@ -44,6 +44,7 @@
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.SubmitRecord;
+import com.google.gerrit.entities.SubmitRequirementResult;
import com.google.gerrit.entities.converter.ChangeMessageProtoConverter;
import com.google.gerrit.entities.converter.PatchSetApprovalProtoConverter;
import com.google.gerrit.entities.converter.PatchSetProtoConverter;
@@ -60,10 +61,14 @@
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;
@@ -128,6 +133,7 @@
List<SubmitRecord> submitRecords,
List<ChangeMessage> changeMessages,
ListMultimap<ObjectId, HumanComment> publishedComments,
+ List<SubmitRequirementResult> submitRequirementResults,
boolean isPrivate,
boolean workInProgress,
boolean reviewStarted,
@@ -181,6 +187,7 @@
.submitRecords(submitRecords)
.changeMessages(changeMessages)
.publishedComments(publishedComments)
+ .submitRequirementsResult(submitRequirementResults)
.updateCount(updateCount)
.mergedOn(mergedOn)
.build();
@@ -326,6 +333,8 @@
abstract ImmutableListMultimap<ObjectId, HumanComment> publishedComments();
+ abstract ImmutableList<SubmitRequirementResult> submitRequirementsResult();
+
abstract int updateCount();
@Nullable
@@ -404,6 +413,7 @@
.submitRecords(ImmutableList.of())
.changeMessages(ImmutableList.of())
.publishedComments(ImmutableListMultimap.of())
+ .submitRequirementsResult(ImmutableList.of())
.updateCount(0);
}
@@ -445,6 +455,9 @@
abstract Builder publishedComments(ListMultimap<ObjectId, HumanComment> publishedComments);
+ abstract Builder submitRequirementsResult(
+ List<SubmitRequirementResult> submitRequirementsResult);
+
abstract Builder updateCount(int updateCount);
abstract Builder mergedOn(Timestamp mergedOn);
@@ -465,6 +478,11 @@
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);
@@ -519,6 +537,9 @@
.changeMessages()
.forEach(m -> b.addChangeMessage(ChangeMessageProtoConverter.INSTANCE.toProto(m)));
object.publishedComments().values().forEach(c -> b.addPublishedComment(GSON.toJson(c)));
+ object
+ .submitRequirementsResult()
+ .forEach(sr -> b.addSubmitRequirementResult(toSubmitRequirementResultProto(sr)));
b.setUpdateCount(object.updateCount());
if (object.mergedOn() != null) {
b.setMergedOnMillis(object.mergedOn().getTime());
@@ -613,6 +634,53 @@
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);
@@ -658,6 +726,10 @@
proto.getPublishedCommentList().stream()
.map(r -> GSON.fromJson(r, HumanComment.class))
.collect(toImmutableListMultimap(HumanComment::getCommitId, c -> c)))
+ .submitRequirementsResult(
+ proto.getSubmitRequirementResultList().stream()
+ .map(sr -> toSubmitRequirementResult(sr))
+ .collect(toImmutableList()))
.updateCount(proto.getUpdateCount())
.mergedOn(proto.getHasMergedOn() ? new Timestamp(proto.getMergedOnMillis()) : null);
return b.build();
diff --git a/java/com/google/gerrit/server/notedb/ChangeRevisionNote.java b/java/com/google/gerrit/server/notedb/ChangeRevisionNote.java
index bf2cf07..44475db 100644
--- a/java/com/google/gerrit/server/notedb/ChangeRevisionNote.java
+++ b/java/com/google/gerrit/server/notedb/ChangeRevisionNote.java
@@ -16,7 +16,9 @@
import static java.nio.charset.StandardCharsets.UTF_8;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.entities.HumanComment;
+import com.google.gerrit.entities.SubmitRequirementResult;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -34,6 +36,8 @@
private final HumanComment.Status status;
private String pushCert;
+ private ImmutableList<SubmitRequirementResult> submitRequirementsResult;
+
ChangeRevisionNote(
ChangeNoteJson noteJson, ObjectReader reader, ObjectId noteId, HumanComment.Status status) {
super(reader, noteId);
@@ -41,6 +45,11 @@
this.status = status;
}
+ public ImmutableList<SubmitRequirementResult> getSubmitRequirementsResult() {
+ checkParsed();
+ return submitRequirementsResult;
+ }
+
public String getPushCert() {
checkParsed();
return pushCert;
@@ -52,20 +61,24 @@
MutableInteger p = new MutableInteger();
p.value = offset;
- HumanCommentsRevisionNoteData data = parseJson(noteJson, raw, p.value);
+ ChangeRevisionNoteData data = parseJson(noteJson, raw, p.value);
if (status == HumanComment.Status.PUBLISHED) {
pushCert = data.pushCert;
} else {
pushCert = null;
}
+ this.submitRequirementsResult =
+ data.submitRequirementResults == null
+ ? ImmutableList.of()
+ : ImmutableList.copyOf(data.submitRequirementResults);
return data.comments;
}
- private HumanCommentsRevisionNoteData parseJson(ChangeNoteJson noteUtil, byte[] raw, int offset)
+ private ChangeRevisionNoteData parseJson(ChangeNoteJson noteUtil, byte[] raw, int offset)
throws IOException {
try (InputStream is = new ByteArrayInputStream(raw, offset, raw.length - offset);
Reader r = new InputStreamReader(is, UTF_8)) {
- return noteUtil.getGson().fromJson(r, HumanCommentsRevisionNoteData.class);
+ return noteUtil.getGson().fromJson(r, ChangeRevisionNoteData.class);
}
}
}
diff --git a/java/com/google/gerrit/server/notedb/HumanCommentsRevisionNoteData.java b/java/com/google/gerrit/server/notedb/ChangeRevisionNoteData.java
similarity index 78%
rename from java/com/google/gerrit/server/notedb/HumanCommentsRevisionNoteData.java
rename to java/com/google/gerrit/server/notedb/ChangeRevisionNoteData.java
index e570412..8e33023 100644
--- a/java/com/google/gerrit/server/notedb/HumanCommentsRevisionNoteData.java
+++ b/java/com/google/gerrit/server/notedb/ChangeRevisionNoteData.java
@@ -15,14 +15,17 @@
package com.google.gerrit.server.notedb;
import com.google.gerrit.entities.HumanComment;
+import com.google.gerrit.entities.SubmitRequirementResult;
import java.util.List;
/**
* Holds the raw data of a RevisionNote.
*
- * <p>It is intended for deserialization from JSON only. It is used for human comments only.
+ * <p>It is intended for deserialization from JSON only. It is used for human comments. Submit
+ * requirements are also stored but only for closed changes.
*/
-class HumanCommentsRevisionNoteData {
+class ChangeRevisionNoteData {
String pushCert;
List<HumanComment> comments;
+ List<SubmitRequirementResult> submitRequirementResults;
}
diff --git a/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 8b9d2856..971e0a8 100644
--- a/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -65,6 +65,7 @@
import com.google.gerrit.entities.RobotComment;
import com.google.gerrit.entities.SubmissionId;
import com.google.gerrit.entities.SubmitRecord;
+import com.google.gerrit.entities.SubmitRequirementResult;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.server.CurrentUser;
@@ -78,6 +79,7 @@
import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
@@ -129,6 +131,7 @@
private final Map<Account.Id, ReviewerStateInternal> reviewers = new LinkedHashMap<>();
private final Map<Address, ReviewerStateInternal> reviewersByEmail = new LinkedHashMap<>();
private final List<HumanComment> comments = new ArrayList<>();
+ private final List<SubmitRequirementResult> submitRequirementResults = new ArrayList<>();
private String commitSubject;
private String subject;
@@ -302,6 +305,10 @@
this.psDescription = psDescription;
}
+ public void putSubmitRequirementResults(Collection<SubmitRequirementResult> rs) {
+ submitRequirementResults.addAll(rs);
+ }
+
public void putComment(HumanComment.Status status, HumanComment c) {
verifyComment(c);
createDraftUpdateIfNull();
@@ -488,7 +495,7 @@
/** @return the tree id for the updated tree */
private ObjectId storeRevisionNotes(RevWalk rw, ObjectInserter inserter, ObjectId curr)
throws ConfigInvalidException, IOException {
- if (comments.isEmpty() && pushCert == null) {
+ if (submitRequirementResults.isEmpty() && comments.isEmpty() && pushCert == null) {
return null;
}
RevisionNoteMap<ChangeRevisionNote> rnm = getRevisionNoteMap(rw, curr);
@@ -498,6 +505,9 @@
c.tag = tag;
cache.get(c.getCommitId()).putComment(c);
}
+ for (SubmitRequirementResult sr : submitRequirementResults) {
+ cache.get(sr.patchSetCommitId()).putSubmitRequirementResult(sr);
+ }
if (pushCert != null) {
checkState(commit != null);
cache.get(ObjectId.fromString(commit)).setPushCertificate(pushCert);
diff --git a/java/com/google/gerrit/server/notedb/RevisionNoteBuilder.java b/java/com/google/gerrit/server/notedb/RevisionNoteBuilder.java
index 3c1d359..7998476 100644
--- a/java/com/google/gerrit/server/notedb/RevisionNoteBuilder.java
+++ b/java/com/google/gerrit/server/notedb/RevisionNoteBuilder.java
@@ -22,16 +22,20 @@
import com.google.common.collect.Maps;
import com.google.common.collect.MultimapBuilder;
import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.SubmitRequirementResult;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
+import java.util.ArrayList;
import java.util.Collections;
+import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.stream.Collectors;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.ObjectId;
@@ -60,11 +64,16 @@
}
}
+ /** Submit requirements are sorted w.r.t. their names before storing in NoteDb. */
+ private final Comparator<SubmitRequirementResult> SUBMIT_REQUIREMENT_RESULT_COMPARATOR =
+ Comparator.comparing(sr -> sr.submitRequirement().name());
+
final byte[] baseRaw;
private final List<? extends Comment> baseComments;
final Map<Comment.Key, Comment> put;
private final Set<Comment.Key> delete;
+ private List<SubmitRequirementResult> submitRequirementResults;
private String pushCert;
private RevisionNoteBuilder(RevisionNote<? extends Comment> base) {
@@ -81,6 +90,7 @@
put = new HashMap<>();
pushCert = null;
}
+ submitRequirementResults = new ArrayList<>();
delete = new HashSet<>();
}
@@ -99,6 +109,10 @@
put.put(comment.key, comment);
}
+ void putSubmitRequirementResult(SubmitRequirementResult result) {
+ submitRequirementResults.add(result);
+ }
+
void deleteComment(Comment.Key key) {
checkArgument(!put.containsKey(key), "cannot both delete and put %s", key);
delete.add(key);
@@ -126,13 +140,19 @@
private void buildNoteJson(ChangeNoteJson noteUtil, OutputStream out) throws IOException {
ListMultimap<Integer, Comment> comments = buildCommentMap();
- if (comments.isEmpty() && pushCert == null) {
+ if (submitRequirementResults.isEmpty() && comments.isEmpty() && pushCert == null) {
return;
}
RevisionNoteData data = new RevisionNoteData();
data.comments = COMMENT_ORDER.sortedCopy(comments.values());
data.pushCert = pushCert;
+ if (!submitRequirementResults.isEmpty()) {
+ data.submitRequirementResults =
+ submitRequirementResults.stream()
+ .sorted(SUBMIT_REQUIREMENT_RESULT_COMPARATOR)
+ .collect(Collectors.toList());
+ }
try (OutputStreamWriter osw = new OutputStreamWriter(out, UTF_8)) {
noteUtil.getGson().toJson(data, osw);
diff --git a/java/com/google/gerrit/server/notedb/RevisionNoteData.java b/java/com/google/gerrit/server/notedb/RevisionNoteData.java
index da15b34..c8770f1 100644
--- a/java/com/google/gerrit/server/notedb/RevisionNoteData.java
+++ b/java/com/google/gerrit/server/notedb/RevisionNoteData.java
@@ -15,15 +15,17 @@
package com.google.gerrit.server.notedb;
import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.SubmitRequirementResult;
import java.util.List;
/**
* Holds the raw data of a RevisionNote.
*
* <p>It is intended for serialization to JSON only. It is used for human comments and robot
- * comments.
+ * comments, as well as for storing submit requirements.
*/
class RevisionNoteData {
String pushCert;
List<Comment> comments;
+ List<SubmitRequirementResult> submitRequirementResults;
}
diff --git a/java/com/google/gerrit/server/notedb/StoreSubmitRequirementsOp.java b/java/com/google/gerrit/server/notedb/StoreSubmitRequirementsOp.java
new file mode 100644
index 0000000..47948d7
--- /dev/null
+++ b/java/com/google/gerrit/server/notedb/StoreSubmitRequirementsOp.java
@@ -0,0 +1,38 @@
+// 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.gerrit.entities.Change;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.update.BatchUpdateOp;
+import com.google.gerrit.server.update.ChangeContext;
+
+/** A {@link BatchUpdateOp} that stores the evaluated submit requirements of a change in NoteDb. */
+public class StoreSubmitRequirementsOp implements BatchUpdateOp {
+ private final ChangeData.Factory changeDataFactory;
+
+ public StoreSubmitRequirementsOp(ChangeData.Factory changeDataFactory) {
+ this.changeDataFactory = changeDataFactory;
+ }
+
+ @Override
+ public boolean updateChange(ChangeContext ctx) throws Exception {
+ Change change = ctx.getChange();
+ ChangeData changeData = changeDataFactory.create(change);
+ ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId());
+ update.putSubmitRequirementResults(changeData.submitRequirements().values());
+ return !changeData.submitRequirements().isEmpty();
+ }
+}
diff --git a/java/com/google/gerrit/server/patch/DiffOperations.java b/java/com/google/gerrit/server/patch/DiffOperations.java
index 7213581..d2da736 100644
--- a/java/com/google/gerrit/server/patch/DiffOperations.java
+++ b/java/com/google/gerrit/server/patch/DiffOperations.java
@@ -46,8 +46,8 @@
*
* @param project a project name representing a git repository.
* @param newCommit 20 bytes SHA-1 of the new commit used in the diff.
- * @param parentNum integer specifying which parent to use as base. If null, the only parent will
- * be used or the auto-merge if {@code newCommit} is a merge commit.
+ * @param parentNum 1-based integer specifying which parent to use as base. If zero, the only
+ * parent will be used or the auto-merge if {@code newCommit} is a merge commit.
* @return map of file paths to the file diffs. The map key is the new file path for all {@link
* ChangeType} file diffs except {@link ChangeType#DELETED} entries where the map key contains
* the old file path. The map entries are not sorted by key.
@@ -56,8 +56,7 @@
* an internal error occurred in Git while evaluating the diff.
*/
Map<String, FileDiffOutput> listModifiedFilesAgainstParent(
- Project.NameKey project, ObjectId newCommit, @Nullable Integer parentNum)
- throws DiffNotAvailableException;
+ Project.NameKey project, ObjectId newCommit, int parentNum) throws DiffNotAvailableException;
/**
* Returns the list of added, deleted or modified files between two commits (patchsets). The
@@ -85,8 +84,8 @@
*
* @param project a project name representing a git repository.
* @param newCommit 20 bytes SHA-1 of the new commit used in the diff.
- * @param parentNum integer specifying which parent to use as base. If null, the only parent will
- * be used or the auto-merge if {@code newCommit} is a merge commit.
+ * @param parentNum 1-based integer specifying which parent to use as base. If zero, the only
+ * parent will be used or the auto-merge if {@code newCommit} is a merge commit.
* @param fileName the file name for which the diff should be evaluated.
* @param whitespace preference controlling whitespace effect in diff computation.
* @return the diff for the single file between the two commits.
@@ -96,7 +95,7 @@
FileDiffOutput getModifiedFileAgainstParent(
Project.NameKey project,
ObjectId newCommit,
- @Nullable Integer parentNum,
+ int parentNum,
String fileName,
@Nullable DiffPreferencesInfo.Whitespace whitespace)
throws DiffNotAvailableException;
diff --git a/java/com/google/gerrit/server/patch/DiffOperationsImpl.java b/java/com/google/gerrit/server/patch/DiffOperationsImpl.java
index f500796..3423b32 100644
--- a/java/com/google/gerrit/server/patch/DiffOperationsImpl.java
+++ b/java/com/google/gerrit/server/patch/DiffOperationsImpl.java
@@ -91,8 +91,7 @@
@Override
public Map<String, FileDiffOutput> listModifiedFilesAgainstParent(
- Project.NameKey project, ObjectId newCommit, @Nullable Integer parent)
- throws DiffNotAvailableException {
+ Project.NameKey project, ObjectId newCommit, int parent) throws DiffNotAvailableException {
try {
DiffParameters diffParams = computeDiffParameters(project, newCommit, parent);
return getModifiedFiles(diffParams);
@@ -120,7 +119,7 @@
public FileDiffOutput getModifiedFileAgainstParent(
Project.NameKey project,
ObjectId newCommit,
- @Nullable Integer parent,
+ int parent,
String fileName,
@Nullable DiffPreferencesInfo.Whitespace whitespace)
throws DiffNotAvailableException {
@@ -376,7 +375,7 @@
Project.NameKey project, ObjectId newCommit, Integer parent) throws IOException {
DiffParameters.Builder result =
DiffParameters.builder().project(project).newCommit(newCommit).parent(parent);
- if (parent != null) {
+ if (parent > 0) {
result.baseCommit(baseCommitUtil.getBaseCommit(project, newCommit, parent));
result.comparisonType(ComparisonType.againstParent(parent));
return result.build();
diff --git a/java/com/google/gerrit/server/patch/PatchScriptFactory.java b/java/com/google/gerrit/server/patch/PatchScriptFactory.java
index 0c648b5..fbb6559 100644
--- a/java/com/google/gerrit/server/patch/PatchScriptFactory.java
+++ b/java/com/google/gerrit/server/patch/PatchScriptFactory.java
@@ -172,7 +172,7 @@
this.fileName = fileName;
this.psa = patchSetA;
- this.parentNum = -1;
+ this.parentNum = 0;
this.psb = patchSetB;
this.diffPrefs = diffPrefs;
this.currentUser = currentUser;
@@ -223,7 +223,7 @@
this.runNewDiffCache = cfg.getBoolean("cache", "diff_cache", "runNewDiffCache_GetDiff", false);
changeId = patchSetB.changeId();
- checkArgument(parentNum >= 0, "parentNum must be >= 0");
+ checkArgument(parentNum > 0, "parentNum must be > 0");
}
@Override
@@ -326,11 +326,7 @@
FileDiffOutput fileDiffOutput =
aId == null
? diffOperations.getModifiedFileAgainstParent(
- notes.getProjectName(),
- bId,
- parentNum == -1 ? null : parentNum + 1,
- fileName,
- diffPrefs.ignoreWhitespace)
+ notes.getProjectName(), bId, parentNum, fileName, diffPrefs.ignoreWhitespace)
: diffOperations.getModifiedFile(
notes.getProjectName(), aId, bId, fileName, diffPrefs.ignoreWhitespace);
return newBuilder().toPatchScriptNew(git, fileDiffOutput);
@@ -395,7 +391,7 @@
if (psa == null) {
return Optional.empty();
}
- checkState(parentNum < 0, "expected no parentNum when psa is present");
+ checkState(parentNum == 0, "expected no parentNum when psa is present");
checkArgument(psa.get() != 0, "edit not supported for left side");
return Optional.of(getCommitId(psa));
}
@@ -409,10 +405,10 @@
}
private PatchListKey keyFor(ObjectId aId, ObjectId bId, Whitespace whitespace) {
- if (parentNum < 0) {
+ if (parentNum == 0) {
return PatchListKey.againstCommit(aId, bId, whitespace);
}
- return PatchListKey.againstParentNum(parentNum + 1, bId, whitespace);
+ return PatchListKey.againstParentNum(parentNum, bId, whitespace);
}
private PatchList listFor(PatchListKey key) throws PatchListNotAvailableException {
diff --git a/java/com/google/gerrit/server/patch/SubmitWithStickyApprovalDiff.java b/java/com/google/gerrit/server/patch/SubmitWithStickyApprovalDiff.java
index e33b261..62cfa47 100644
--- a/java/com/google/gerrit/server/patch/SubmitWithStickyApprovalDiff.java
+++ b/java/com/google/gerrit/server/patch/SubmitWithStickyApprovalDiff.java
@@ -18,6 +18,7 @@
import com.google.gerrit.common.data.PatchScript;
import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.Patch;
import com.google.gerrit.entities.Patch.ChangeType;
import com.google.gerrit.entities.PatchSet;
@@ -43,6 +44,7 @@
import java.io.IOException;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.stream.Collectors;
import org.eclipse.jgit.diff.Edit;
import org.eclipse.jgit.lib.Config;
@@ -242,10 +244,9 @@
if (!patchSetApproval.label().equals(LabelId.CODE_REVIEW)) {
continue;
}
- if (!projectState
- .getLabelTypes(notes)
- .byLabel(patchSetApproval.labelId())
- .isMaxPositive(patchSetApproval)) {
+ Optional<LabelType> lt =
+ projectState.getLabelTypes(notes).byLabel(patchSetApproval.labelId());
+ if (!lt.isPresent() || !lt.get().isMaxPositive(patchSetApproval)) {
continue;
}
if (patchSetApproval.patchSetId().get() > maxPatchSetId.get()) {
diff --git a/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheImpl.java b/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheImpl.java
index b779bf7..e4fd728 100644
--- a/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheImpl.java
+++ b/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheImpl.java
@@ -18,11 +18,14 @@
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ListMultimap;
import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Patch.ChangeType;
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.patch.DiffNotAvailableException;
@@ -37,6 +40,7 @@
import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
@@ -82,7 +86,7 @@
.valueSerializer(GitModifiedFilesCacheImpl.ValueSerializer.INSTANCE)
.maximumWeight(10 << 20)
.weigher(ModifiedFilesWeigher.class)
- .version(1)
+ .version(2)
.loader(ModifiedFilesLoader.class);
}
};
@@ -139,7 +143,7 @@
.bTree(bTree)
.renameScore(key.renameScore())
.build();
- List<ModifiedFile> modifiedFiles = gitCache.get(gitKey);
+ List<ModifiedFile> modifiedFiles = mergeRewrittenEntries(gitCache.get(gitKey));
if (key.aCommit().equals(ObjectId.zeroId())) {
return ImmutableList.copyOf(modifiedFiles);
}
@@ -202,5 +206,61 @@
// value as the set of file paths shouldn't contain it.
return touchedFilePaths.contains(oldFilePath) || touchedFilePaths.contains(newFilePath);
}
+
+ /**
+ * Return the {@code modifiedFiles} input list while merging {@link ChangeType#ADDED} and {@link
+ * ChangeType#DELETED} entries for the same file into a single {@link ChangeType#REWRITE} entry.
+ *
+ * <p>Background: In some cases, JGit returns two diff entries (ADDED + DELETED) for the same
+ * file path. This happens e.g. when a file's mode is changed between patchsets, for example
+ * converting a symlink file to a regular file. We identify this case and return a single
+ * modified file with changeType = {@link ChangeType#REWRITE}.
+ */
+ private static List<ModifiedFile> mergeRewrittenEntries(List<ModifiedFile> modifiedFiles) {
+ List<ModifiedFile> result = new ArrayList<>();
+
+ // Handle ADDED and DELETED entries separately.
+ ListMultimap<String, ModifiedFile> byPath = ArrayListMultimap.create();
+ modifiedFiles.stream()
+ .filter(ModifiedFilesLoader::isAddedOrDeleted)
+ .forEach(
+ f -> {
+ if (f.oldPath().isPresent()) {
+ byPath.get(f.oldPath().get()).add(f);
+ }
+ if (f.newPath().isPresent()) {
+ byPath.get(f.newPath().get()).add(f);
+ }
+ });
+ for (String path : byPath.keySet()) {
+ List<ModifiedFile> entries = byPath.get(path);
+ if (entries.size() == 1) {
+ result.add(entries.get(0));
+ } else if (entries.size() == 2) {
+ result.add(getAddedEntry(entries).toBuilder().changeType(ChangeType.REWRITE).build());
+ } else {
+ // JGit error. Not expected to happen.
+ logger.atWarning().log(
+ "Found %d ADDED and DELETED entries for the same file path: %s."
+ + " Adding the first entry only to the result.",
+ entries.size(), entries);
+ result.add(entries.get(0));
+ }
+ }
+
+ // Add the remaining non ADDED/DELETED entries to the result
+ modifiedFiles.stream().filter(f -> !isAddedOrDeleted(f)).forEach(result::add);
+ return result;
+ }
+
+ private static boolean isAddedOrDeleted(ModifiedFile f) {
+ return f.changeType() == ChangeType.ADDED || f.changeType() == ChangeType.DELETED;
+ }
+
+ private static ModifiedFile getAddedEntry(List<ModifiedFile> modifiedFiles) {
+ return modifiedFiles.get(0).changeType() == ChangeType.ADDED
+ ? modifiedFiles.get(0)
+ : modifiedFiles.get(1);
+ }
}
}
diff --git a/java/com/google/gerrit/server/patch/gitdiff/ModifiedFile.java b/java/com/google/gerrit/server/patch/gitdiff/ModifiedFile.java
index 9512094..f4e7ca3 100644
--- a/java/com/google/gerrit/server/patch/gitdiff/ModifiedFile.java
+++ b/java/com/google/gerrit/server/patch/gitdiff/ModifiedFile.java
@@ -51,6 +51,8 @@
return new AutoValue_ModifiedFile.Builder();
}
+ public abstract Builder toBuilder();
+
/** Computes this object's weight, which is its size in bytes. */
public int weight() {
int weight = 1; // the changeType field
diff --git a/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiff.java b/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiff.java
index 2f2d29b..a502a46 100644
--- a/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiff.java
+++ b/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiff.java
@@ -187,6 +187,10 @@
return result;
}
+ public String getDefaultPath() {
+ return oldPath().isPresent() ? oldPath().get() : newPath().get();
+ }
+
public static Builder builder() {
return new AutoValue_GitFileDiff.Builder();
}
diff --git a/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java b/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java
index 2ce6925..77b8938 100644
--- a/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java
+++ b/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java
@@ -24,7 +24,11 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.MultimapBuilder;
+import com.google.common.collect.Multimaps;
import com.google.common.collect.Streams;
+import com.google.gerrit.entities.Patch;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
import com.google.gerrit.server.cache.CacheModule;
@@ -41,6 +45,7 @@
import com.google.inject.Singleton;
import com.google.inject.name.Named;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -105,6 +110,9 @@
private final LoadingCache<GitFileDiffCacheKey, GitFileDiff> cache;
+ private static final ImmutableSet<Patch.ChangeType> ADDED_AND_DELETED =
+ ImmutableSet.of(Patch.ChangeType.ADDED, Patch.ChangeType.DELETED);
+
@Inject
public GitFileDiffCacheImpl(
@Named(GIT_DIFF) LoadingCache<GitFileDiffCacheKey, GitFileDiff> cache) {
@@ -163,7 +171,7 @@
}
@Override
- public GitFileDiff load(GitFileDiffCacheKey key) throws IOException {
+ public GitFileDiff load(GitFileDiffCacheKey key) throws IOException, DiffNotAvailableException {
try (TraceTimer timer =
TraceContext.newTimer(
"Loading a single key from git file diff cache",
@@ -177,7 +185,8 @@
@Override
public Map<GitFileDiffCacheKey, GitFileDiff> loadAll(
- Iterable<? extends GitFileDiffCacheKey> keys) throws IOException {
+ Iterable<? extends GitFileDiffCacheKey> keys)
+ throws IOException, DiffNotAvailableException {
try (TraceTimer timer =
TraceContext.newTimer("Loading multiple keys from git file diff cache")) {
ImmutableMap.Builder<GitFileDiffCacheKey, GitFileDiff> result =
@@ -215,13 +224,14 @@
*/
private Map<GitFileDiffCacheKey, GitFileDiff> loadAllImpl(
Repository repo, ObjectReader reader, DiffOptions options, List<GitFileDiffCacheKey> keys)
- throws IOException {
+ throws IOException, DiffNotAvailableException {
ImmutableMap.Builder<GitFileDiffCacheKey, GitFileDiff> result =
ImmutableMap.builderWithExpectedSize(keys.size());
Map<GitFileDiffCacheKey, String> filePaths =
keys.stream().collect(Collectors.toMap(identity(), GitFileDiffCacheKey::newFilePath));
DiffFormatter formatter = createDiffFormatter(options, repo, reader);
- Map<String, DiffEntry> diffEntries = loadDiffEntries(formatter, options, filePaths.values());
+ ListMultimap<String, DiffEntry> diffEntries =
+ loadDiffEntries(formatter, options, filePaths.values());
for (GitFileDiffCacheKey key : filePaths.keySet()) {
String newFilePath = filePaths.get(key);
if (!diffEntries.containsKey(newFilePath)) {
@@ -233,14 +243,25 @@
newFilePath));
continue;
}
- DiffEntry diffEntry = diffEntries.get(newFilePath);
- GitFileDiff gitFileDiff = createGitFileDiff(diffEntry, formatter, key);
- result.put(key, gitFileDiff);
+ List<DiffEntry> entries = diffEntries.get(newFilePath);
+ if (entries.size() == 1) {
+ result.put(key, createGitFileDiff(entries.get(0), formatter, key));
+ } else {
+ // Handle when JGit returns two {Added, Deleted} entries for the same file. This happens,
+ // for example, when a file's mode is changed between patchsets (e.g. converting a
+ // symlink to a regular file). We combine both diff entries into a single entry with
+ // {changeType = Rewrite}.
+ List<GitFileDiff> gitDiffs = new ArrayList<>();
+ for (DiffEntry entry : diffEntries.get(newFilePath)) {
+ gitDiffs.add(createGitFileDiff(entry, formatter, key));
+ }
+ result.put(key, createRewriteEntry(gitDiffs));
+ }
}
return result.build();
}
- private static Map<String, DiffEntry> loadDiffEntries(
+ private static ListMultimap<String, DiffEntry> loadDiffEntries(
DiffFormatter diffFormatter, DiffOptions diffOptions, Collection<String> filePaths)
throws IOException {
Set<String> filePathsSet = ImmutableSet.copyOf(filePaths);
@@ -251,7 +272,11 @@
return diffEntries.stream()
.filter(d -> filePathsSet.contains(pathExtractor.apply(d)))
- .collect(Collectors.toMap(d -> pathExtractor.apply(d), identity()));
+ .collect(
+ Multimaps.toMultimap(
+ d -> pathExtractor.apply(d),
+ identity(),
+ MultimapBuilder.treeKeys().arrayListValues()::build));
}
private static DiffFormatter createDiffFormatter(
@@ -334,6 +359,39 @@
}
}
+ /**
+ * Create a single {@link GitFileDiff} with {@link Patch.ChangeType} equals {@link
+ * Patch.ChangeType#REWRITE}, assuming the input list contains two entries with types {@link
+ * Patch.ChangeType#ADDED} and {@link Patch.ChangeType#DELETED}.
+ *
+ * @param gitDiffs input list of exactly two {@link GitFileDiff} for same file path.
+ * @return a single {@link GitFileDiff} with change type equals {@link Patch.ChangeType#REWRITE}.
+ * @throws DiffNotAvailableException if input list contains git diffs with change types other than
+ * {ADDED, DELETED}. This is a JGit error.
+ */
+ private static GitFileDiff createRewriteEntry(List<GitFileDiff> gitDiffs)
+ throws DiffNotAvailableException {
+ if (gitDiffs.size() != 2) {
+ throw new DiffNotAvailableException(
+ String.format(
+ "JGit error: found %d dff entries for same file path %s",
+ gitDiffs.size(), gitDiffs.get(0).getDefaultPath()));
+ }
+ if (!ImmutableSet.of(gitDiffs.get(0).changeType(), gitDiffs.get(1).changeType())
+ .equals(ADDED_AND_DELETED)) {
+ // This is an illegal state. JGit is not supposed to return this, so we throw an exception.
+ throw new DiffNotAvailableException(
+ String.format(
+ "JGit error: unexpected change types %s and %s for same file path %s",
+ gitDiffs.get(0).changeType(),
+ gitDiffs.get(1).changeType(),
+ gitDiffs.get(0).getDefaultPath()));
+ }
+ GitFileDiff addedEntry =
+ gitDiffs.get(0).changeType() == Patch.ChangeType.ADDED ? gitDiffs.get(0) : gitDiffs.get(1);
+ return addedEntry.toBuilder().changeType(Patch.ChangeType.REWRITE).build();
+ }
+
/** An entity representing the options affecting the diff computation. */
@AutoValue
abstract static class DiffOptions {
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluator.java b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluator.java
index 1d154dd..65d9d9e 100644
--- a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluator.java
+++ b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluator.java
@@ -66,6 +66,8 @@
: Optional.empty();
return SubmitRequirementResult.builder()
+ .submitRequirement(sr)
+ .patchSetCommitId(cd.currentPatchSet().commitId())
.submittabilityExpressionResult(blockingResult)
.applicabilityExpressionResult(applicabilityResult)
.overrideExpressionResult(overrideResult)
@@ -79,9 +81,9 @@
Predicate<ChangeData> predicate =
changeQueryBuilderProvider.get().parse(expression.expressionString());
PredicateResult predicateResult = evaluatePredicateTree(predicate, changeData);
- return SubmitRequirementExpressionResult.create(predicateResult);
+ return SubmitRequirementExpressionResult.create(expression, predicateResult);
} catch (QueryParseException e) {
- return SubmitRequirementExpressionResult.error(e.getMessage());
+ return SubmitRequirementExpressionResult.error(expression, e.getMessage());
}
}
diff --git a/java/com/google/gerrit/server/query/approval/ApprovalContext.java b/java/com/google/gerrit/server/query/approval/ApprovalContext.java
index 4c2c7e8..3bf072a 100644
--- a/java/com/google/gerrit/server/query/approval/ApprovalContext.java
+++ b/java/com/google/gerrit/server/query/approval/ApprovalContext.java
@@ -44,11 +44,12 @@
"approval and target must be the same change. got: %s, %s",
psa.patchSetId(),
id);
- checkState(
- psa.patchSetId().get() + 1 == id.get(),
- "approvals can only be copied to the next consecutive patch set. got: %s, %s",
- psa.patchSetId(),
- id);
+ // TODO(ekempin): Use checkState to verify that psa.patchSetId().get() + 1 == id.get() so that
+ // it's ensured that approvals are only copied to the next consecutive patch set. To add back
+ // this verification https://gerrit-review.googlesource.com/c/gerrit/+/312633 can be reverted.
+ // As explained in the commit message of this change doing this check is only possible if there
+ // are no changes with gaps in patch set numbers. Since it's planned to fix-up old changes with
+ // gaps in patch set numbers, this todo is a reminder to add back the check once this is done.
return new AutoValue_ApprovalContext(psa, id, changeNotes, changeKind);
}
}
diff --git a/java/com/google/gerrit/server/query/approval/MagicValuePredicate.java b/java/com/google/gerrit/server/query/approval/MagicValuePredicate.java
index 2924e6e..326620d 100644
--- a/java/com/google/gerrit/server/query/approval/MagicValuePredicate.java
+++ b/java/com/google/gerrit/server/query/approval/MagicValuePredicate.java
@@ -23,6 +23,7 @@
import com.google.inject.assistedinject.Assisted;
import java.util.Collection;
import java.util.Objects;
+import java.util.Optional;
/** Predicate that matches patch set approvals we want to copy based on the value. */
public class MagicValuePredicate extends ApprovalPredicate {
@@ -47,19 +48,23 @@
@Override
public boolean match(ApprovalContext ctx) {
+ Optional<LabelType> lt =
+ getLabelType(ctx.changeNotes().getProjectName(), ctx.patchSetApproval().labelId());
short pValue;
switch (value) {
case ANY:
return true;
case MIN:
- pValue =
- getLabelType(ctx.changeNotes().getProjectName(), ctx.patchSetApproval().labelId())
- .getMaxNegative();
+ if (!lt.isPresent()) {
+ return false;
+ }
+ pValue = lt.get().getMaxNegative();
break;
case MAX:
- pValue =
- getLabelType(ctx.changeNotes().getProjectName(), ctx.patchSetApproval().labelId())
- .getMaxPositive();
+ if (!lt.isPresent()) {
+ return false;
+ }
+ pValue = lt.get().getMaxPositive();
break;
default:
throw new IllegalArgumentException("unrecognized label value: " + value);
@@ -67,7 +72,7 @@
return pValue == ctx.patchSetApproval().value();
}
- private LabelType getLabelType(Project.NameKey project, LabelId labelId) {
+ private Optional<LabelType> getLabelType(Project.NameKey project, LabelId labelId) {
return projectCache
.get(project)
.orElseThrow(() -> new IllegalStateException(project + " absent"))
diff --git a/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java b/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
index 30d5e2f..ade615c 100644
--- a/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
@@ -91,8 +91,8 @@
}
protected static LabelType type(LabelTypes types, String toFind) {
- if (types.byLabel(toFind) != null) {
- return types.byLabel(toFind);
+ if (types.byLabel(toFind).isPresent()) {
+ return types.byLabel(toFind).get();
}
for (LabelType lt : types.getLabelTypes()) {
diff --git a/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java b/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java
index e3c58e47..2c56322 100644
--- a/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java
@@ -87,8 +87,8 @@
}
protected static LabelType type(LabelTypes types, String toFind) {
- if (types.byLabel(toFind) != null) {
- return types.byLabel(toFind);
+ if (types.byLabel(toFind).isPresent()) {
+ return types.byLabel(toFind).get();
}
for (LabelType lt : types.getLabelTypes()) {
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteVote.java b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
index 84424a8..2c358d0 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteVote.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
@@ -194,7 +194,7 @@
for (PatchSetApproval a :
approvalsUtil.byPatchSetUser(
ctx.getNotes(), psId, accountId, ctx.getRevWalk(), ctx.getRepoView().getConfig())) {
- if (labelTypes.byLabel(a.labelId()) == null) {
+ if (!labelTypes.byLabel(a.labelId()).isPresent()) {
continue; // Ignore undefined labels.
} else if (!a.label().equals(label)) {
// Populate map for non-matching labels, needed by VoteDeleted.
diff --git a/java/com/google/gerrit/server/restapi/change/Files.java b/java/com/google/gerrit/server/restapi/change/Files.java
index 1efe378..320e57d 100644
--- a/java/com/google/gerrit/server/restapi/change/Files.java
+++ b/java/com/google/gerrit/server/restapi/change/Files.java
@@ -183,7 +183,7 @@
r =
Response.ok(
fileInfoJson.getFileInfoMap(
- resource.getChange(), resource.getPatchSet().commitId(), parentNum - 1));
+ resource.getChange(), resource.getPatchSet().commitId(), parentNum));
} else {
r = Response.ok(fileInfoJson.getFileInfoMap(resource.getChange(), resource.getPatchSet()));
}
@@ -280,11 +280,11 @@
Map<String, FileDiffOutput> oldList =
diffOperations.listModifiedFilesAgainstParent(
- project, patchSet.commitId(), /* parentNum= */ null);
+ project, patchSet.commitId(), /* parentNum= */ 0);
Map<String, FileDiffOutput> curList =
diffOperations.listModifiedFilesAgainstParent(
- project, resource.getPatchSet().commitId(), /* parentNum= */ null);
+ project, resource.getPatchSet().commitId(), /* parentNum= */ 0);
int sz = paths.size();
List<String> pathList = Lists.newArrayListWithCapacity(sz);
diff --git a/java/com/google/gerrit/server/restapi/change/GetDiff.java b/java/com/google/gerrit/server/restapi/change/GetDiff.java
index 2169d57..dd951a8 100644
--- a/java/com/google/gerrit/server/restapi/change/GetDiff.java
+++ b/java/com/google/gerrit/server/restapi/change/GetDiff.java
@@ -74,6 +74,7 @@
@Option(name = "--base", metaVar = "REVISION")
String base;
+ /** 1-based index of the parent's position in the commit object. */
@Option(name = "--parent", metaVar = "parent-number")
int parentNum;
@@ -143,7 +144,7 @@
} else if (parentNum > 0) {
psf =
patchScriptFactoryFactory.create(
- notes, fileName, parentNum - 1, pId, prefs, currentUser.get());
+ notes, fileName, parentNum, pId, prefs, currentUser.get());
} else {
psf = patchScriptFactoryFactory.create(notes, fileName, null, pId, prefs, currentUser.get());
}
diff --git a/java/com/google/gerrit/server/restapi/change/Move.java b/java/com/google/gerrit/server/restapi/change/Move.java
index 9263971..8c21841 100644
--- a/java/com/google/gerrit/server/restapi/change/Move.java
+++ b/java/com/google/gerrit/server/restapi/change/Move.java
@@ -64,6 +64,7 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
+import java.util.Optional;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
@@ -265,11 +266,13 @@
approvalsUtil.byPatchSet(
ctx.getNotes(), psId, ctx.getRevWalk(), ctx.getRepoView().getConfig())) {
ProjectState projectState = projectCache.get(project).orElseThrow(illegalState(project));
- LabelType type = projectState.getLabelTypes(ctx.getNotes()).byLabel(psa.labelId());
+ Optional<LabelType> type =
+ projectState.getLabelTypes(ctx.getNotes()).byLabel(psa.labelId());
// Only keep veto votes, defined as votes where:
// 1- the label function allows minimum values to block submission.
// 2- the vote holds the minimum value.
- if (type == null || (type.isMaxNegative(psa) && type.getFunction().isBlock())) {
+ if (!type.isPresent()
+ || (type.get().isMaxNegative(psa) && type.get().getFunction().isBlock())) {
continue;
}
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 6816361..4dbb6ee 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -22,7 +22,6 @@
import static com.google.gerrit.server.permissions.LabelPermission.ForUser.ON_BEHALF_OF;
import static com.google.gerrit.server.project.ProjectCache.illegalState;
import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
@@ -502,8 +501,8 @@
Iterator<Map.Entry<String, Short>> itr = in.labels.entrySet().iterator();
while (itr.hasNext()) {
Map.Entry<String, Short> ent = itr.next();
- LabelType type = labelTypes.byLabel(ent.getKey());
- if (type == null) {
+ Optional<LabelType> type = labelTypes.byLabel(ent.getKey());
+ if (!type.isPresent()) {
logger.atFine().log("label %s not found", ent.getKey());
if (strictLabels) {
throw new BadRequestException(
@@ -518,15 +517,15 @@
logger.atFine().log(
"skipping on behalf of permission check for label %s"
+ " because caller is an internal user",
- type.getName());
+ type.get().getName());
} else {
try {
- perm.check(new LabelPermission.WithValue(ON_BEHALF_OF, type, ent.getValue()));
+ perm.check(new LabelPermission.WithValue(ON_BEHALF_OF, type.get(), ent.getValue()));
} catch (AuthException e) {
throw new AuthException(
String.format(
"not permitted to modify label \"%s\" on behalf of \"%s\"",
- type.getName(), in.onBehalfOf),
+ type.get().getName(), in.onBehalfOf),
e);
}
}
@@ -558,8 +557,8 @@
Iterator<Map.Entry<String, Short>> itr = labels.entrySet().iterator();
while (itr.hasNext()) {
Map.Entry<String, Short> ent = itr.next();
- LabelType lt = labelTypes.byLabel(ent.getKey());
- if (lt == null) {
+ Optional<LabelType> lt = labelTypes.byLabel(ent.getKey());
+ if (!lt.isPresent()) {
logger.atFine().log("label %s not found", ent.getKey());
if (strictLabels) {
throw new BadRequestException(
@@ -576,7 +575,7 @@
continue;
}
- if (lt.getValue(ent.getValue()) == null) {
+ if (lt.get().getValue(ent.getValue()) == null) {
logger.atFine().log("label value %s not found", ent.getValue());
if (strictLabels) {
throw new BadRequestException(
@@ -590,10 +589,10 @@
short val = ent.getValue();
try {
- perm.check(new LabelPermission.WithValue(lt, val));
+ perm.check(new LabelPermission.WithValue(lt.get(), val));
} catch (AuthException e) {
throw new AuthException(
- String.format("Applying label \"%s\": %d is restricted", lt.getName(), val), e);
+ String.format("Applying label \"%s\": %d is restricted", lt.get().getName(), val), e);
}
}
}
@@ -1356,7 +1355,10 @@
ChangeUpdate update = ctx.getUpdate(psId);
for (Map.Entry<String, Short> ent : allApprovals.entrySet()) {
String name = ent.getKey();
- LabelType lt = requireNonNull(labelTypes.byLabel(name), name);
+ LabelType lt =
+ labelTypes
+ .byLabel(name)
+ .orElseThrow(() -> new IllegalStateException("no label config for " + name));
PatchSetApproval c = current.remove(lt.getName());
String normName = lt.getName();
@@ -1448,7 +1450,10 @@
List<String> disallowed = new ArrayList<>(labelTypes.getLabelTypes().size());
for (PatchSetApproval psa : del) {
- LabelType lt = requireNonNull(labelTypes.byLabel(psa.label()));
+ LabelType lt =
+ labelTypes
+ .byLabel(psa.label())
+ .orElseThrow(() -> new IllegalStateException("no label config for " + psa.label()));
String normName = lt.getName();
if (!lt.isAllowPostSubmit()) {
disallowed.add(normName);
@@ -1460,7 +1465,10 @@
}
for (PatchSetApproval psa : ups) {
- LabelType lt = requireNonNull(labelTypes.byLabel(psa.label()));
+ LabelType lt =
+ labelTypes
+ .byLabel(psa.label())
+ .orElseThrow(() -> new IllegalStateException("no label config for " + psa.label()));
String normName = lt.getName();
if (!lt.isAllowPostSubmit()) {
disallowed.add(normName);
@@ -1508,9 +1516,9 @@
continue;
}
- LabelType lt = labelTypes.byLabel(a.labelId());
- if (lt != null) {
- current.put(lt.getName(), a);
+ Optional<LabelType> lt = labelTypes.byLabel(a.labelId());
+ if (lt.isPresent()) {
+ current.put(lt.get().getName(), a);
} else {
del.add(a);
}
diff --git a/java/com/google/gerrit/server/restapi/project/FilesInCommitCollection.java b/java/com/google/gerrit/server/restapi/project/FilesInCommitCollection.java
index 7bee2f2..6d054bd 100644
--- a/java/com/google/gerrit/server/restapi/project/FilesInCommitCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/FilesInCommitCollection.java
@@ -77,6 +77,10 @@
}
public static final class ListFiles implements RestReadView<CommitResource> {
+ /**
+ * The 1-based parent number. If zero, the default base commit will be used, which is the only
+ * parent for commits having one parent or the auto-merge commit otherwise.
+ */
@Option(name = "--parent", metaVar = "parent-number")
int parentNum;
@@ -97,8 +101,7 @@
throws ResourceConflictException, PatchListNotAvailableException {
RevCommit commit = resource.getCommit();
return Response.ok(
- fileInfoJson.getFileInfoMap(
- resource.getProjectState().getNameKey(), commit, parentNum - 1));
+ fileInfoJson.getFileInfoMap(resource.getProjectState().getNameKey(), commit, parentNum));
}
}
}
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index 2b4fb3b..363cdca 100644
--- a/java/com/google/gerrit/server/submit/MergeOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -67,6 +67,7 @@
import com.google.gerrit.server.logging.RequestId;
import com.google.gerrit.server.logging.TraceContext;
import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.StoreSubmitRequirementsOp;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.SubmitRuleOptions;
@@ -96,6 +97,8 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.lib.Constants;
@@ -654,6 +657,16 @@
toSubmit, updateOrderCalculator, submoduleCommits, subscriptionGraph, dryrun);
this.allProjects = updateOrderCalculator.getProjectsInOrder();
List<BatchUpdate> batchUpdates = orm.batchUpdates(allProjects);
+ // Group batch updates by project
+ Map<Project.NameKey, BatchUpdate> batchUpdatesByProject =
+ batchUpdates.stream().collect(Collectors.toMap(b -> b.getProject(), Function.identity()));
+ for (Map.Entry<Change.Id, ChangeData> entry : cs.changesById().entrySet()) {
+ Project.NameKey project = entry.getValue().project();
+ Change.Id changeId = entry.getKey();
+ batchUpdatesByProject
+ .get(project)
+ .addOp(changeId, new StoreSubmitRequirementsOp(changeDataFactory));
+ }
try {
submissionExecutor.setAdditionalBatchUpdateListeners(
ImmutableList.of(new SubmitStrategyListener(submitInput, strategies, commitStatus)));
diff --git a/java/com/google/gerrit/server/submit/SubscriptionGraph.java b/java/com/google/gerrit/server/submit/SubscriptionGraph.java
index 26c8ac9..dfbbf81 100644
--- a/java/com/google/gerrit/server/submit/SubscriptionGraph.java
+++ b/java/com/google/gerrit/server/submit/SubscriptionGraph.java
@@ -332,6 +332,8 @@
MergeOpRepoManager orm)
throws IOException {
Collection<SubmoduleSubscription> ret = new ArrayList<>();
+ if (RefNames.isGerritRef(srcBranch.branch())) return ret;
+
Project.NameKey srcProject = srcBranch.project();
for (SubscribeSection s :
projectCache
diff --git a/java/com/google/gerrit/sshd/SshCommand.java b/java/com/google/gerrit/sshd/SshCommand.java
index c94b25c..93c6c2c 100644
--- a/java/com/google/gerrit/sshd/SshCommand.java
+++ b/java/com/google/gerrit/sshd/SshCommand.java
@@ -19,6 +19,7 @@
import com.google.gerrit.server.DynamicOptions;
import com.google.gerrit.server.RequestInfo;
import com.google.gerrit.server.RequestListener;
+import com.google.gerrit.server.cancellation.RequestCancelledException;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.logging.PerformanceLogContext;
import com.google.gerrit.server.logging.PerformanceLogger;
@@ -61,6 +62,12 @@
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()));
+ }
+ stderr.println(msg.toString());
} finally {
stdout.flush();
stderr.flush();
diff --git a/java/com/google/gerrit/testing/InMemoryModule.java b/java/com/google/gerrit/testing/InMemoryModule.java
index f28f60f..06d9453 100644
--- a/java/com/google/gerrit/testing/InMemoryModule.java
+++ b/java/com/google/gerrit/testing/InMemoryModule.java
@@ -54,6 +54,7 @@
import com.google.gerrit.server.config.CanonicalWebUrlModule;
import com.google.gerrit.server.config.CanonicalWebUrlProvider;
import com.google.gerrit.server.config.DefaultUrlFormatter;
+import com.google.gerrit.server.config.FileBasedGlobalPluginConfigProvider;
import com.google.gerrit.server.config.GerritGlobalModule;
import com.google.gerrit.server.config.GerritInstanceIdModule;
import com.google.gerrit.server.config.GerritInstanceNameModule;
@@ -62,6 +63,7 @@
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.GerritServerId;
import com.google.gerrit.server.config.GerritServerIdProvider;
+import com.google.gerrit.server.config.GlobalPluginConfigProvider;
import com.google.gerrit.server.config.SendEmailExecutor;
import com.google.gerrit.server.config.SitePath;
import com.google.gerrit.server.config.TrackingFooters;
@@ -195,6 +197,7 @@
bind(Path.class).annotatedWith(SitePath.class).toInstance(Paths.get("."));
bind(Config.class).annotatedWith(GerritServerConfig.class).toInstance(cfg);
bind(GerritOptions.class).toInstance(new GerritOptions(false, false));
+ bind(GlobalPluginConfigProvider.class).to(FileBasedGlobalPluginConfigProvider.class);
bind(GitRepositoryManager.class).to(InMemoryRepositoryManager.class);
bind(InMemoryRepositoryManager.class).in(SINGLETON);
diff --git a/java/gerrit/PRED__load_commit_labels_1.java b/java/gerrit/PRED__load_commit_labels_1.java
index 5ee292ff..9a656b8 100644
--- a/java/gerrit/PRED__load_commit_labels_1.java
+++ b/java/gerrit/PRED__load_commit_labels_1.java
@@ -16,6 +16,7 @@
import com.googlecode.prolog_cafe.lang.StructureTerm;
import com.googlecode.prolog_cafe.lang.SymbolTerm;
import com.googlecode.prolog_cafe.lang.Term;
+import java.util.Optional;
/** Exports list of {@code commit_label( label('Code-Review', 2), user(12345789) )}. */
class PRED__load_commit_labels_1 extends Predicate.P1 {
@@ -38,13 +39,14 @@
LabelTypes types = cd.getLabelTypes();
for (PatchSetApproval a : cd.currentApprovals()) {
- LabelType t = types.byLabel(a.labelId());
- if (t == null) {
+ Optional<LabelType> t = types.byLabel(a.labelId());
+ if (!t.isPresent()) {
continue;
}
StructureTerm labelTerm =
- new StructureTerm(sym_label, SymbolTerm.intern(t.getName()), new IntegerTerm(a.value()));
+ new StructureTerm(
+ sym_label, SymbolTerm.intern(t.get().getName()), new IntegerTerm(a.value()));
StructureTerm userTerm = new StructureTerm(sym_user, new IntegerTerm(a.accountId().get()));
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 9e51054..d54574a 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -2975,7 +2975,7 @@
}
@Test
- public void externalIdBatchUpdates_commitMsg() throws Exception {
+ public void externalIdBatchUpdates_commitMsg_multipleAccounts() throws Exception {
ExternalId extId1 =
ExternalId.createWithEmail(ExternalId.Key.parse("foo:bar"), admin.id(), "1@foo.com");
ExternalId extId2 =
@@ -2998,6 +2998,22 @@
}
}
+ @Test
+ public void externalIdBatchUpdates_commitMsg_singleAccount() throws Exception {
+ ExternalId extId =
+ ExternalId.createWithEmail(ExternalId.Key.parse("foo:bar"), admin.id(), "1@foo.com");
+
+ accountsUpdateProvider.get().update("foobar", admin.id(), (a, u) -> u.addExternalId(extId));
+
+ try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+ RevWalk rw = new RevWalk(allUsersRepo)) {
+ RevCommit commit =
+ rw.parseCommit(allUsersRepo.exactRef(RefNames.REFS_EXTERNAL_IDS).getObjectId());
+
+ assertThat(commit.getFullMessage()).isEqualTo("foobar\n");
+ }
+ }
+
private void createDraft(PushOneCommit.Result r, String path, String message) throws Exception {
DraftInput in = new DraftInput();
in.path = path;
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 976e828..c08aa7f 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -69,6 +69,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
+import com.google.common.collect.MoreCollectors;
import com.google.common.truth.ThrowableSubject;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.ChangeIndexedCounter;
@@ -104,6 +105,8 @@
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.entities.SubmitRequirement;
import com.google.gerrit.entities.SubmitRequirementExpression;
+import com.google.gerrit.entities.SubmitRequirementExpressionResult;
+import com.google.gerrit.entities.SubmitRequirementResult;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.annotations.Exports;
import com.google.gerrit.extensions.api.accounts.DeleteDraftCommentsInput;
@@ -172,6 +175,7 @@
import com.google.gerrit.server.index.change.ChangeIndex;
import com.google.gerrit.server.index.change.ChangeIndexCollection;
import com.google.gerrit.server.index.change.IndexedChangeQuery;
+import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.patch.DiffSummary;
import com.google.gerrit.server.patch.DiffSummaryKey;
import com.google.gerrit.server.patch.IntraLineDiff;
@@ -4287,6 +4291,40 @@
}
@Test
+ public void submitRequirement_storedForClosedChanges() throws Exception {
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("code-review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:code-review=+2"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ PushOneCommit.Result r = createChange("Add a file", "foo", "content");
+ String changeId = r.getChangeId();
+
+ voteLabel(changeId, "code-review", 2);
+
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(change.submitRequirements, "code-review", Status.SATISFIED);
+
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+ revision.review(ReviewInput.approve());
+ revision.submit();
+
+ ChangeNotes notes = notesFactory.create(project, r.getChange().getId());
+
+ SubmitRequirementResult result =
+ notes.getSubmitRequirementsResult().stream().collect(MoreCollectors.onlyElement());
+ assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.SATISFIED);
+ assertThat(result.submittabilityExpressionResult().status())
+ .isEqualTo(SubmitRequirementExpressionResult.Status.PASS);
+ assertThat(result.submittabilityExpressionResult().expression().expressionString())
+ .isEqualTo("label:code-review=+2");
+ }
+
+ @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/api/revision/RevisionDiffIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
index d8dab33..5f3b702 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
@@ -50,6 +50,7 @@
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.webui.EditWebLink;
+import com.google.gerrit.server.change.FileInfoJsonExperimentImpl;
import com.google.gerrit.server.patch.DiffOperations;
import com.google.gerrit.server.patch.filediff.FileDiffOutput;
import com.google.inject.Inject;
@@ -109,7 +110,8 @@
intraline = baseConfig.getBoolean(TEST_PARAMETER_MARKER, "intraline", false);
useNewDiffCacheListFiles =
- baseConfig.getBoolean("cache", "diff_cache", "runNewDiffCache_ListFiles", false);
+ Arrays.asList(baseConfig.getStringList("experiments", null, "enabled"))
+ .contains(FileInfoJsonExperimentImpl.NEW_DIFF_CACHE_FEATURE);
useNewDiffCacheGetDiff =
baseConfig.getBoolean("cache", "diff_cache", "runNewDiffCache_GetDiff", false);
@@ -153,7 +155,8 @@
PushOneCommit.Result result = push.to("refs/heads/master");
Map<String, FileDiffOutput> modifiedFiles =
- diffOperations.listModifiedFilesAgainstParent(project, result.getCommit(), null);
+ diffOperations.listModifiedFilesAgainstParent(
+ project, result.getCommit(), /* parentNum= */ 0);
assertThat(modifiedFiles.keySet()).containsExactly("/COMMIT_MSG", "f.txt");
assertThat(
@@ -2835,11 +2838,7 @@
}
@Test
- public void symlinkConvertedToRegularFileIsIdentifiedAsAdded() throws Exception {
- // TODO(ghareeb): fix this test for the new diff cache implementation
- assume().that(useNewDiffCacheListFiles).isFalse();
- assume().that(useNewDiffCacheGetDiff).isFalse();
-
+ public void symlinkConvertedToRegularFileIsIdentifiedAsRewritten() throws Exception {
String target = "file.txt";
String symlink = "link.lnk";
@@ -2867,23 +2866,39 @@
gApi.changes().id(result.getChangeId()).current().files(initialRev);
assertThat(changedFiles.keySet()).containsExactly("/COMMIT_MSG", symlink);
- assertThat(changedFiles.get(symlink).status).isEqualTo('W'); // Rewrite
+
+ // Both old and new diff caches agree that the state is rewritten
+ assertThat(changedFiles.get(symlink).status).isEqualTo('W'); // Rewritten
DiffInfo diffInfo =
gApi.changes().id(result.getChangeId()).current().file(symlink).diff(initialRev);
- // The diff logic identifies two entries for the file:
- // 1. One entry as 'DELETED' for the symlink.
- // 2. Another entry as 'ADDED' for the new regular file.
- // Since the diff logic returns a single entry, we prioritize returning the 'ADDED' entry in
- // this case so that the client is able to see the new content that was added to the file.
- assertThat(diffInfo.changeType).isEqualTo(ChangeType.ADDED);
- assertThat(diffInfo.content).hasSize(1);
- assertThat(diffInfo)
- .content()
- .element(0)
- .linesOfB()
- .containsExactly("Content of the new file named 'symlink'");
+ // TODO(ghareeb): Remove the else branch when the new diff cache is rolled out as default.
+ if (useNewDiffCacheGetDiff) {
+ // File diff in New diff cache: change type is correctly identified as REWRITTEN
+ assertThat(diffInfo.changeType).isEqualTo(ChangeType.REWRITE);
+ assertThat(diffInfo.content).hasSize(2);
+ assertThat(diffInfo)
+ .content()
+ .element(0)
+ .linesOfB()
+ .containsExactly("Content of the new file named 'symlink'");
+ assertThat(diffInfo).content().element(1).linesOfA().containsExactly("file.txt");
+ } else {
+ // File diff in old diff cache: The diff logic identifies two entries for the file:
+ // 1. One entry as 'DELETED' for the symlink.
+ // 2. Another entry as 'ADDED' for the new regular file.
+ // Since the diff logic returns a single entry, the implementation prioritizes the 'ADDED'
+ // entry in this case so that the user is able to see the new content that was added to the
+ // file.
+ assertThat(diffInfo.changeType).isEqualTo(ChangeType.ADDED);
+ assertThat(diffInfo.content).hasSize(1);
+ assertThat(diffInfo)
+ .content()
+ .element(0)
+ .linesOfB()
+ .containsExactly("Content of the new file named 'symlink'");
+ }
}
@Test
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionNewDiffCacheIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionNewDiffCacheIT.java
index ec0bcc6..714bd78 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionNewDiffCacheIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionNewDiffCacheIT.java
@@ -14,6 +14,7 @@
package com.google.gerrit.acceptance.api.revision;
+import com.google.gerrit.server.change.FileInfoJsonExperimentImpl;
import com.google.gerrit.testing.ConfigSuite;
import org.eclipse.jgit.lib.Config;
@@ -26,7 +27,8 @@
@ConfigSuite.Default
public static Config newDiffCacheConfig() {
Config config = new Config();
- config.setBoolean("cache", "diff_cache", "runNewDiffCache_ListFiles", true);
+ config.setString(
+ "experiments", null, "enabled", FileInfoJsonExperimentImpl.NEW_DIFF_CACHE_FEATURE);
return config;
}
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java b/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java
new file mode 100644
index 0000000..29d54cc
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java
@@ -0,0 +1,206 @@
+// 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.rest;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.httpd.restapi.RestApiServlet.SC_CLIENT_CLOSED_REQUEST;
+import static org.apache.http.HttpStatus.SC_REQUEST_TIMEOUT;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.server.cancellation.RequestCancelledException;
+import com.google.gerrit.server.cancellation.RequestStateProvider;
+import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.validators.CommitValidationListener;
+import com.google.gerrit.server.git.validators.CommitValidationMessage;
+import com.google.gerrit.server.project.CreateProjectArgs;
+import com.google.gerrit.server.validators.ProjectCreationValidationListener;
+import com.google.gerrit.server.validators.ValidationException;
+import com.google.inject.Inject;
+import java.util.List;
+import org.junit.Test;
+
+public class CancellationIT extends AbstractDaemonTest {
+ @Inject private ExtensionRegistry extensionRegistry;
+
+ @Test
+ public void handleClientDisconnected() throws Exception {
+ ProjectCreationValidationListener projectCreationListener =
+ new ProjectCreationValidationListener() {
+ @Override
+ public void validateNewProject(CreateProjectArgs args) throws ValidationException {
+ // Simulate a request cancellation by throwing RequestCancelledException. In contrast to
+ // an actual request cancellation this allows us to verify the HTTP status code that is
+ // set when a request is cancelled.
+ throw new RequestCancelledException(
+ RequestStateProvider.Reason.CLIENT_CLOSED_REQUEST, /* cancellationMessage= */ null);
+ }
+ };
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(projectCreationListener)) {
+ RestResponse response = adminRestSession.put("/projects/" + name("new"));
+ assertThat(response.getStatusCode()).isEqualTo(SC_CLIENT_CLOSED_REQUEST);
+ assertThat(response.getEntityContent()).isEqualTo("Client Closed Request");
+ }
+ }
+
+ @Test
+ public void handleClientDeadlineExceeded() throws Exception {
+ ProjectCreationValidationListener projectCreationListener =
+ new ProjectCreationValidationListener() {
+ @Override
+ public void validateNewProject(CreateProjectArgs args) throws ValidationException {
+ // Simulate an exceeded deadline by throwing RequestCancelledException.
+ throw new RequestCancelledException(
+ RequestStateProvider.Reason.CLIENT_PROVIDED_DEADLINE_EXCEEDED,
+ /* cancellationMessage= */ null);
+ }
+ };
+ 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("Client Provided Deadline Exceeded");
+ }
+ }
+
+ @Test
+ public void handleServerDeadlineExceeded() throws Exception {
+ ProjectCreationValidationListener projectCreationListener =
+ new ProjectCreationValidationListener() {
+ @Override
+ public void validateNewProject(CreateProjectArgs args) throws ValidationException {
+ // Simulate an exceeded deadline by throwing RequestCancelledException.
+ throw new RequestCancelledException(
+ RequestStateProvider.Reason.SERVER_DEADLINE_EXCEEDED,
+ /* cancellationMessage= */ null);
+ }
+ };
+ 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");
+ }
+ }
+
+ @Test
+ public void handleRequestCancellationWithMessage() throws Exception {
+ ProjectCreationValidationListener projectCreationListener =
+ new ProjectCreationValidationListener() {
+ @Override
+ public void validateNewProject(CreateProjectArgs args) throws ValidationException {
+ // Simulate an exceeded deadline by throwing RequestCancelledException.
+ throw 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() {
+ @Override
+ public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
+ throws CommitValidationException {
+ // Simulate a request cancellation by throwing RequestCancelledException. In contrast to
+ // an actual request cancellation this allows us verify the error message that is sent
+ // to the client.
+ throw new RequestCancelledException(
+ RequestStateProvider.Reason.CLIENT_CLOSED_REQUEST, /* cancellationMessage= */ null);
+ }
+ };
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(commitValidationListener)) {
+ PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+ PushOneCommit.Result r = push.to("refs/heads/master");
+ r.assertErrorStatus("Client Closed Request");
+ }
+ }
+
+ @Test
+ public void handleClientDeadlineExceededForPush() throws Exception {
+ CommitValidationListener commitValidationListener =
+ new CommitValidationListener() {
+ @Override
+ public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
+ throws CommitValidationException {
+ // Simulate an exceeded deadline by throwing RequestCancelledException.
+ throw new RequestCancelledException(
+ RequestStateProvider.Reason.CLIENT_PROVIDED_DEADLINE_EXCEEDED,
+ /* cancellationMessage= */ null);
+ }
+ };
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(commitValidationListener)) {
+ PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+ PushOneCommit.Result r = push.to("refs/heads/master");
+ r.assertErrorStatus("Client Provided Deadline Exceeded");
+ }
+ }
+
+ @Test
+ public void handleServerDeadlineExceededForPush() throws Exception {
+ CommitValidationListener commitValidationListener =
+ new CommitValidationListener() {
+ @Override
+ public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
+ throws CommitValidationException {
+ // Simulate an exceeded deadline by throwing RequestCancelledException.
+ throw new RequestCancelledException(
+ RequestStateProvider.Reason.SERVER_DEADLINE_EXCEEDED,
+ /* cancellationMessage= */ null);
+ }
+ };
+ 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");
+ }
+ }
+
+ @Test
+ public void handleRequestCancellationWithMessageForPush() throws Exception {
+ CommitValidationListener commitValidationListener =
+ new CommitValidationListener() {
+ @Override
+ public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
+ throws CommitValidationException {
+ // Simulate an exceeded deadline by throwing RequestCancelledException.
+ throw 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)");
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java b/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
index 23047a4..e848cef 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
@@ -29,7 +29,6 @@
import com.google.gerrit.entities.SubmitRequirement;
import com.google.gerrit.entities.SubmitRequirementExpression;
import com.google.gerrit.entities.SubmitRequirementExpressionResult;
-import com.google.gerrit.entities.SubmitRequirementExpressionResult.PredicateResult;
import com.google.gerrit.entities.SubmitRequirementExpressionResult.Status;
import com.google.gerrit.entities.SubmitRequirementResult;
import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -102,25 +101,14 @@
assertThat(result.status()).isEqualTo(Status.PASS);
- assertThat(result.getPassingAtoms())
- .containsExactly(
- PredicateResult.builder()
- .predicateString(String.format("project:%s", project.get()))
- .status(true)
- .build(),
- PredicateResult.builder()
- .predicateString("message:\"Fix a bug\"")
- .status(true)
- .build());
+ assertThat(result.passingAtoms())
+ .containsExactly(String.format("project:%s", project.get()), "message:\"Fix a bug\"");
- assertThat(result.getFailingAtoms())
+ assertThat(result.failingAtoms())
.containsExactly(
- PredicateResult.builder()
- // TODO(ghareeb): querying "branch:" creates a RefPredicate. Fix names so that they
- // match
- .predicateString(String.format("ref:refs/heads/foo"))
- .status(false)
- .build());
+ // TODO(ghareeb): querying "branch:" creates a RefPredicate. Fix names so that they
+ // match
+ String.format("ref:refs/heads/foo"));
}
@Test
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SshCancellationIT.java b/javatests/com/google/gerrit/acceptance/ssh/SshCancellationIT.java
new file mode 100644
index 0000000..2cb9637
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/ssh/SshCancellationIT.java
@@ -0,0 +1,102 @@
+// 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.ssh;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
+import com.google.gerrit.acceptance.UseSsh;
+import com.google.gerrit.server.cancellation.RequestCancelledException;
+import com.google.gerrit.server.cancellation.RequestStateProvider;
+import com.google.gerrit.server.project.CreateProjectArgs;
+import com.google.gerrit.server.validators.ProjectCreationValidationListener;
+import com.google.gerrit.server.validators.ValidationException;
+import com.google.inject.Inject;
+import org.junit.Test;
+
+@UseSsh
+public class SshCancellationIT extends AbstractDaemonTest {
+ @Inject private ExtensionRegistry extensionRegistry;
+
+ @Test
+ public void handleClientDisconnected() throws Exception {
+ ProjectCreationValidationListener projectCreationListener =
+ new ProjectCreationValidationListener() {
+ @Override
+ public void validateNewProject(CreateProjectArgs args) throws ValidationException {
+ throw new RequestCancelledException(
+ RequestStateProvider.Reason.CLIENT_CLOSED_REQUEST, /* cancellationMessage= */ null);
+ }
+ };
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(projectCreationListener)) {
+ adminSshSession.exec("gerrit create-project " + name("new"));
+ adminSshSession.assertFailure("Client Closed Request");
+ }
+ }
+
+ @Test
+ public void handleClientDeadlineExceeded() throws Exception {
+ ProjectCreationValidationListener projectCreationListener =
+ new ProjectCreationValidationListener() {
+ @Override
+ public void validateNewProject(CreateProjectArgs args) throws ValidationException {
+ throw new RequestCancelledException(
+ RequestStateProvider.Reason.CLIENT_PROVIDED_DEADLINE_EXCEEDED,
+ /* cancellationMessage= */ null);
+ }
+ };
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(projectCreationListener)) {
+ adminSshSession.exec("gerrit create-project " + name("new"));
+ adminSshSession.assertFailure("Client Provided Deadline Exceeded");
+ }
+ }
+
+ @Test
+ public void handleServerDeadlineExceeded() throws Exception {
+ ProjectCreationValidationListener projectCreationListener =
+ new ProjectCreationValidationListener() {
+ @Override
+ public void validateNewProject(CreateProjectArgs args) throws ValidationException {
+ throw new RequestCancelledException(
+ RequestStateProvider.Reason.SERVER_DEADLINE_EXCEEDED,
+ /* cancellationMessage= */ null);
+ }
+ };
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(projectCreationListener)) {
+ adminSshSession.exec("gerrit create-project " + name("new"));
+ adminSshSession.assertFailure("Server Deadline Exceeded");
+ }
+ }
+
+ @Test
+ public void handleRequestCancellationWithMessage() throws Exception {
+ ProjectCreationValidationListener projectCreationListener =
+ new ProjectCreationValidationListener() {
+ @Override
+ public void validateNewProject(CreateProjectArgs args) throws ValidationException {
+ throw 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/notedb/ChangeNotesStateTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
index feff89c..ecdb03d 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
@@ -36,6 +36,10 @@
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.SubmitRecord;
+import com.google.gerrit.entities.SubmitRequirement;
+import com.google.gerrit.entities.SubmitRequirementExpression;
+import com.google.gerrit.entities.SubmitRequirementExpressionResult;
+import com.google.gerrit.entities.SubmitRequirementResult;
import com.google.gerrit.entities.converter.ChangeMessageProtoConverter;
import com.google.gerrit.entities.converter.PatchSetApprovalProtoConverter;
import com.google.gerrit.entities.converter.PatchSetProtoConverter;
@@ -52,6 +56,9 @@
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.SubmitRequirementExpressionResultProto;
+import com.google.gerrit.server.cache.proto.Cache.SubmitRequirementProto;
+import com.google.gerrit.server.cache.proto.Cache.SubmitRequirementResultProto;
import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
import com.google.gerrit.server.notedb.ChangeNotesState.ChangeColumns;
import com.google.gerrit.server.notedb.ChangeNotesState.Serializer;
@@ -671,6 +678,69 @@
}
@Test
+ public void serializeSubmitRequirementsResult() throws Exception {
+ assertRoundTrip(
+ newBuilder()
+ .submitRequirementsResult(
+ ImmutableList.of(
+ SubmitRequirementResult.builder()
+ .patchSetCommitId(
+ ObjectId.fromString("26e50c7d315a33a13e5cc00902781fa876bc36cd"))
+ .submitRequirement(
+ SubmitRequirement.builder()
+ .setName("Code-Review")
+ .setApplicabilityExpression(
+ SubmitRequirementExpression.of("project:foo"))
+ .setSubmittabilityExpression(
+ SubmitRequirementExpression.create("label:code-review=+2"))
+ .setAllowOverrideInChildProjects(false)
+ .build())
+ .applicabilityExpressionResult(
+ Optional.of(
+ SubmitRequirementExpressionResult.create(
+ SubmitRequirementExpression.create("project:foo"),
+ SubmitRequirementExpressionResult.Status.PASS,
+ ImmutableList.of("project:foo"),
+ ImmutableList.of())))
+ .submittabilityExpressionResult(
+ SubmitRequirementExpressionResult.create(
+ SubmitRequirementExpression.create("label:code-review=+2"),
+ SubmitRequirementExpressionResult.Status.FAIL,
+ ImmutableList.of(),
+ ImmutableList.of("label:code-review=+2")))
+ .build()))
+ .build(),
+ newProtoBuilder()
+ .addSubmitRequirementResult(
+ SubmitRequirementResultProto.newBuilder()
+ .setCommit(
+ ObjectIdConverter.create()
+ .toByteString(
+ ObjectId.fromString("26e50c7d315a33a13e5cc00902781fa876bc36cd")))
+ .setSubmitRequirement(
+ SubmitRequirementProto.newBuilder()
+ .setName("Code-Review")
+ .setApplicabilityExpression("project:foo")
+ .setSubmittabilityExpression("label:code-review=+2")
+ .setAllowOverrideInChildProjects(false)
+ .build())
+ .setApplicabilityExpressionResult(
+ SubmitRequirementExpressionResultProto.newBuilder()
+ .setExpression("project:foo")
+ .setStatus("PASS")
+ .addPassingAtoms("project:foo")
+ .build())
+ .setSubmittabilityExpressionResult(
+ SubmitRequirementExpressionResultProto.newBuilder()
+ .setExpression("label:code-review=+2")
+ .setStatus("FAIL")
+ .addFailingAtoms("label:code-review=+2")
+ .build())
+ .build())
+ .build());
+ }
+
+ @Test
public void serializeAssigneeUpdates() throws Exception {
assertRoundTrip(
newBuilder()
@@ -842,6 +912,9 @@
.put(
"publishedComments",
new TypeLiteral<ImmutableListMultimap<ObjectId, HumanComment>>() {}.getType())
+ .put(
+ "submitRequirementsResult",
+ new TypeLiteral<ImmutableList<SubmitRequirementResult>>() {}.getType())
.put("updateCount", int.class)
.put("mergedOn", Timestamp.class)
.build());
diff --git a/javatests/com/google/gerrit/server/patch/DiffOperationsTest.java b/javatests/com/google/gerrit/server/patch/DiffOperationsTest.java
index 5bf5154..aa313e3 100644
--- a/javatests/com/google/gerrit/server/patch/DiffOperationsTest.java
+++ b/javatests/com/google/gerrit/server/patch/DiffOperationsTest.java
@@ -72,7 +72,7 @@
FileDiffOutput diffOutput =
diffOperations.getModifiedFileAgainstParent(
- testProjectName, newCommitId, /* parentNum=*/ null, fileName2, /* whitespace=*/ null);
+ testProjectName, newCommitId, /* parentNum=*/ 0, fileName2, /* whitespace=*/ null);
assertThat(diffOutput.oldCommitId()).isEqualTo(oldCommitId);
assertThat(diffOutput.newCommitId()).isEqualTo(newCommitId);
diff --git a/javatests/com/google/gerrit/server/schema/SchemaCreatorImplTest.java b/javatests/com/google/gerrit/server/schema/SchemaCreatorImplTest.java
index fc6b412..9cba362 100644
--- a/javatests/com/google/gerrit/server/schema/SchemaCreatorImplTest.java
+++ b/javatests/com/google/gerrit/server/schema/SchemaCreatorImplTest.java
@@ -72,7 +72,7 @@
@Test
public void createSchema_Label_CodeReview() throws Exception {
- LabelType codeReview = getLabelTypes().byLabel("Code-Review");
+ LabelType codeReview = getLabelTypes().byLabel("Code-Review").get();
assertThat(codeReview).isNotNull();
assertThat(codeReview.getName()).isEqualTo("Code-Review");
assertThat(codeReview.getDefaultValue()).isEqualTo(0);
diff --git a/plugins/replication b/plugins/replication
index dc9bb2e..46cfb7d 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit dc9bb2e946e4c6c31e8a4665f30eca6d00017523
+Subproject commit 46cfb7dd5b6891f991cfe66e72c08953487c1c81
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index 35e6449..a28ae59 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit 35e6449a517691a880c94e7467bc07360f8e6666
+Subproject commit a28ae590486934690e4e0a95d7eb75f8b60644a6
diff --git a/plugins/webhooks b/plugins/webhooks
index 9fc9c2d..73f9dc7 160000
--- a/plugins/webhooks
+++ b/plugins/webhooks
@@ -1 +1 @@
-Subproject commit 9fc9c2d4e69f7e2701cbcd873977d3312a231a81
+Subproject commit 73f9dc72bd52f5d64853db31e711717a995f0a46
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index fcf1cf4..5f9c3c5 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -106,10 +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/core/gr-smart-search/gr-smart-search_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",
@@ -123,17 +119,14 @@
"elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_html.ts",
"elements/shared/gr-account-list/gr-account-list_html.ts",
"elements/shared/gr-autocomplete/gr-autocomplete_html.ts",
- "elements/shared/gr-change-status/gr-change-status_html.ts",
"elements/shared/gr-comment-thread/gr-comment-thread_html.ts",
"elements/shared/gr-comment/gr-comment_html.ts",
"elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog_html.ts",
- "elements/shared/gr-copy-clipboard/gr-copy-clipboard_html.ts",
"elements/shared/gr-dialog/gr-dialog_html.ts",
"elements/shared/gr-diff-preferences/gr-diff-preferences_html.ts",
"elements/shared/gr-download-commands/gr-download-commands_html.ts",
"elements/shared/gr-dropdown-list/gr-dropdown-list_html.ts",
"elements/shared/gr-dropdown/gr-dropdown_html.ts",
- "elements/shared/gr-editable-content/gr-editable-content_html.ts",
"elements/shared/gr-hovercard-account/gr-hovercard-account_html.ts",
"elements/shared/gr-label-info/gr-label-info_html.ts",
"elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_html.ts",
diff --git a/polygerrit-ui/app/api/diff.ts b/polygerrit-ui/app/api/diff.ts
index 7400295..4b20072 100644
--- a/polygerrit-ui/app/api/diff.ts
+++ b/polygerrit-ui/app/api/diff.ts
@@ -213,6 +213,10 @@
automatic_blink?: boolean;
}
+export declare type DiffResponsiveMode =
+ | 'FULL_RESPONSIVE'
+ | 'SHRINK_ONLY'
+ | 'NONE';
export declare interface RenderPreferences {
hide_left_side?: boolean;
disable_context_control_buttons?: boolean;
@@ -220,6 +224,7 @@
hide_line_length_indicator?: boolean;
use_block_expansion?: boolean;
image_diff_prefs?: ImageDiffPreferences;
+ responsive_mode?: DiffResponsiveMode;
}
/**
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
index a0b1794..fce7db9 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
@@ -1486,6 +1486,7 @@
}
_handleDeleteConfirm() {
+ this._hideAllDialogs();
this._fireAction(
'/',
assertUIActionInfo(this.actions[ChangeActions.DELETE]),
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
index 9dcb67e..5206fdb 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
@@ -952,7 +952,7 @@
return;
}
e.preventDefault();
- this.fileCursor.next();
+ this.fileCursor.next({circular: true});
this.selectedIndex = this.fileCursor.index;
}
}
@@ -972,7 +972,7 @@
return;
}
e.preventDefault();
- this.fileCursor.previous();
+ this.fileCursor.previous({circular: true});
this.selectedIndex = this.fileCursor.index;
}
}
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
index 0655721..79bc9f6 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
@@ -533,7 +533,7 @@
assert.equal(element.fileCursor.index, 2);
// up should not move the cursor.
- MockInteractions.pressAndReleaseKeyOn(element, 38, null, 'down');
+ MockInteractions.pressAndReleaseKeyOn(element, 38, null, 'up');
assert.equal(element.fileCursor.index, 2);
MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
@@ -548,8 +548,8 @@
MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
- assert.equal(element.fileCursor.index, 0);
- assert.equal(element.selectedIndex, 0);
+ assert.equal(element.fileCursor.index, 1);
+ assert.equal(element.selectedIndex, 1);
const createCommentInPlaceStub = sinon.stub(element.diffCursor,
'createCommentInPlace');
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
index bd4cb76..8201dbc 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
@@ -487,8 +487,8 @@
account="[[account]]"
force-attention="[[_computeHasNewAttention(account, _newAttentionSet)]]"
selected="[[_computeHasNewAttention(account, _newAttentionSet)]]"
- hide-hovercard=""
- selection-chip-style
+ hideHovercard
+ selectionChipStyle
on-click="_handleAttentionClick"
></gr-account-label>
</template>
@@ -558,8 +558,8 @@
account="[[_owner]]"
force-attention="[[_computeHasNewAttention(_owner, _newAttentionSet)]]"
selected="[[_computeHasNewAttention(_owner, _newAttentionSet)]]"
- hide-hovercard=""
- selection-chip-style
+ hideHovercard
+ selectionChipStyle
on-click="_handleAttentionClick"
>
</gr-account-label>
@@ -573,8 +573,8 @@
account="[[_uploader]]"
force-attention="[[_computeHasNewAttention(_uploader, _newAttentionSet)]]"
selected="[[_computeHasNewAttention(_uploader, _newAttentionSet)]]"
- hide-hovercard=""
- selection-chip-style
+ hideHovercard
+ selectionChipStyle
on-click="_handleAttentionClick"
>
</gr-account-label>
@@ -593,8 +593,8 @@
account="[[account]]"
force-attention="[[_computeHasNewAttention(account, _newAttentionSet)]]"
selected="[[_computeHasNewAttention(account, _newAttentionSet)]]"
- hide-hovercard=""
- selection-chip-style
+ hideHovercard
+ selectionChipStyle
on-click="_handleAttentionClick"
>
</gr-account-label>
@@ -614,8 +614,8 @@
account="[[account]]"
force-attention="[[_computeHasNewAttention(account, _newAttentionSet)]]"
selected="[[_computeHasNewAttention(account, _newAttentionSet)]]"
- hide-hovercard=""
- selection-chip-style
+ hideHovercard
+ selectionChipStyle
on-click="_handleAttentionClick"
>
</gr-account-label>
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts
index 73f3dd3..93a432b 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts
@@ -109,7 +109,7 @@
<gr-account-label
account="[[item]]"
on-click="handleAccountClicked"
- selection-chip-style
+ selectionChipStyle
selected="[[isSelected(item, selectedAuthors)]]"
> </gr-account-label>
</template>
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results.ts b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
index d9c94fb..f27a383 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
@@ -590,8 +590,8 @@
<gr-formatted-text
noTrailingMargin
class="message"
- content="${this.result.message}"
- config="${this.repoConfig}"
+ .content="${this.result.message}"
+ .config="${this.repoConfig?.commentlinks}"
></gr-formatted-text>
</gr-endpoint-decorator>
`;
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/elements/core/gr-smart-search/gr-smart-search.ts b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.ts
index aa7e2e0..7419713 100644
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.ts
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.ts
@@ -32,6 +32,12 @@
const SELF_EXPRESSION = 'self';
const ME_EXPRESSION = 'me';
+declare global {
+ interface HTMLElementEventMap {
+ 'handle-search': CustomEvent<SearchBarHandleSearchDetail>;
+ }
+}
+
@customElement('gr-smart-search')
export class GrSmartSearch extends PolymerElement {
static get template() {
@@ -39,7 +45,7 @@
}
@property({type: String})
- searchQuery?: string;
+ searchQuery = '';
@property({type: Object})
_config?: ServerInfo;
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.js b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.js
deleted file mode 100644
index f3a9965..0000000
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.js
+++ /dev/null
@@ -1,137 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 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 './gr-smart-search.js';
-import {stubRestApi} from '../../../test/test-utils.js';
-
-const basicFixture = fixtureFromElement('gr-smart-search');
-
-suite('gr-smart-search tests', () => {
- let element;
-
- setup(() => {
- element = basicFixture.instantiate();
- });
-
- test('Autocompletes accounts', () => {
- stubRestApi('getSuggestedAccounts').callsFake(() =>
- Promise.resolve([
- {
- name: 'fred',
- email: 'fred@goog.co',
- },
- ])
- );
- return element._fetchAccounts('owner', 'fr').then(s => {
- assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'});
- });
- });
-
- test('Inserts self as option when valid', () => {
- stubRestApi('getSuggestedAccounts').callsFake( () =>
- Promise.resolve([
- {
- name: 'fred',
- email: 'fred@goog.co',
- },
- ])
- );
- element._fetchAccounts('owner', 's')
- .then(s => {
- assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'});
- assert.deepEqual(s[1], {text: 'owner:self'});
- })
- .then(() => element._fetchAccounts('owner', 'selfs'))
- .then(s => {
- assert.notEqual(s[0], {text: 'owner:self'});
- });
- });
-
- test('Inserts me as option when valid', () => {
- stubRestApi('getSuggestedAccounts').callsFake( () =>
- Promise.resolve([
- {
- name: 'fred',
- email: 'fred@goog.co',
- },
- ])
- );
- return element._fetchAccounts('owner', 'm')
- .then(s => {
- assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'});
- assert.deepEqual(s[1], {text: 'owner:me'});
- })
- .then(() => element._fetchAccounts('owner', 'meme'))
- .then(s => {
- assert.notEqual(s[0], {text: 'owner:me'});
- });
- });
-
- test('Autocompletes groups', () => {
- stubRestApi('getSuggestedGroups').callsFake( () =>
- Promise.resolve({
- Polygerrit: 0,
- gerrit: 0,
- gerrittest: 0,
- })
- );
- return element._fetchGroups('ownerin', 'pol').then(s => {
- assert.deepEqual(s[0], {text: 'ownerin:Polygerrit'});
- });
- });
-
- test('Autocompletes projects', () => {
- stubRestApi('getSuggestedProjects').callsFake( () =>
- Promise.resolve({Polygerrit: 0}));
- return element._fetchProjects('project', 'pol').then(s => {
- assert.deepEqual(s[0], {text: 'project:Polygerrit'});
- });
- });
-
- test('Autocomplete doesnt override exact matches to input', () => {
- stubRestApi('getSuggestedGroups').callsFake( () =>
- Promise.resolve({
- Polygerrit: 0,
- gerrit: 0,
- gerrittest: 0,
- })
- );
- return element._fetchGroups('ownerin', 'gerrit').then(s => {
- assert.deepEqual(s[0], {text: 'ownerin:Polygerrit'});
- assert.deepEqual(s[1], {text: 'ownerin:gerrit'});
- assert.deepEqual(s[2], {text: 'ownerin:gerrittest'});
- });
- });
-
- test('Autocompletes accounts with no email', () => {
- stubRestApi('getSuggestedAccounts').callsFake( () =>
- Promise.resolve([{name: 'fred'}]));
- return element._fetchAccounts('owner', 'fr').then(s => {
- assert.deepEqual(s[0], {text: 'owner:"fred"', label: 'fred'});
- });
- });
-
- test('Autocompletes accounts with email', () => {
- stubRestApi('getSuggestedAccounts').callsFake( () =>
- Promise.resolve([{email: 'fred@goog.co'}]));
- return element._fetchAccounts('owner', 'fr').then(s => {
- assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: ''});
- });
- });
-});
-
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.ts b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.ts
new file mode 100644
index 0000000..0218a8f
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.ts
@@ -0,0 +1,143 @@
+/**
+ * @license
+ * Copyright (C) 2016 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';
+import './gr-smart-search';
+import {GrSmartSearch} from './gr-smart-search';
+import {stubRestApi} from '../../../test/test-utils';
+import {EmailAddress, GroupId, UrlEncodedRepoName} from '../../../types/common';
+
+const basicFixture = fixtureFromElement('gr-smart-search');
+
+suite('gr-smart-search tests', () => {
+ let element: GrSmartSearch;
+
+ setup(() => {
+ element = basicFixture.instantiate();
+ });
+
+ test('Autocompletes accounts', () => {
+ stubRestApi('getSuggestedAccounts').callsFake(() =>
+ Promise.resolve([
+ {
+ name: 'fred',
+ email: 'fred@goog.co' as EmailAddress,
+ },
+ ])
+ );
+ return element._fetchAccounts('owner', 'fr').then(s => {
+ assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'});
+ });
+ });
+
+ test('Inserts self as option when valid', () => {
+ stubRestApi('getSuggestedAccounts').callsFake(() =>
+ Promise.resolve([
+ {
+ name: 'fred',
+ email: 'fred@goog.co' as EmailAddress,
+ },
+ ])
+ );
+ element
+ ._fetchAccounts('owner', 's')
+ .then(s => {
+ assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'});
+ assert.deepEqual(s[1], {text: 'owner:self'});
+ })
+ .then(() => element._fetchAccounts('owner', 'selfs'))
+ .then(s => {
+ assert.notEqual(s[0], {text: 'owner:self'});
+ });
+ });
+
+ test('Inserts me as option when valid', () => {
+ stubRestApi('getSuggestedAccounts').callsFake(() =>
+ Promise.resolve([
+ {
+ name: 'fred',
+ email: 'fred@goog.co' as EmailAddress,
+ },
+ ])
+ );
+ return element
+ ._fetchAccounts('owner', 'm')
+ .then(s => {
+ assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'});
+ assert.deepEqual(s[1], {text: 'owner:me'});
+ })
+ .then(() => element._fetchAccounts('owner', 'meme'))
+ .then(s => {
+ assert.notEqual(s[0], {text: 'owner:me'});
+ });
+ });
+
+ test('Autocompletes groups', () => {
+ stubRestApi('getSuggestedGroups').callsFake(() =>
+ Promise.resolve({
+ Polygerrit: {id: '4c97682e6ce61b7247f3381b6f1789356666de7f' as GroupId},
+ gerrit: {id: '4c97682e6ce61b7247f3381b6f1789356666de7f' as GroupId},
+ gerrittest: {id: '4c97682e6ce61b7247f3381b6f1789356666de7f' as GroupId},
+ })
+ );
+ return element._fetchGroups('ownerin', 'pol').then(s => {
+ assert.deepEqual(s[0], {text: 'ownerin:Polygerrit'});
+ });
+ });
+
+ test('Autocompletes projects', () => {
+ stubRestApi('getSuggestedProjects').callsFake(() =>
+ Promise.resolve({Polygerrit: {id: 'test' as UrlEncodedRepoName}})
+ );
+ return element._fetchProjects('project', 'pol').then(s => {
+ assert.deepEqual(s[0], {text: 'project:Polygerrit'});
+ });
+ });
+
+ test('Autocomplete doesnt override exact matches to input', () => {
+ stubRestApi('getSuggestedGroups').callsFake(() =>
+ Promise.resolve({
+ Polygerrit: {id: '4c97682e6ce61b7247f3381b6f1789356666de7f' as GroupId},
+ gerrit: {id: '4c97682e6ce61b7247f3381b6f1789356666de7f' as GroupId},
+ gerrittest: {id: '4c97682e6ce61b7247f3381b6f1789356666de7f' as GroupId},
+ })
+ );
+ return element._fetchGroups('ownerin', 'gerrit').then(s => {
+ assert.deepEqual(s[0], {text: 'ownerin:Polygerrit'});
+ assert.deepEqual(s[1], {text: 'ownerin:gerrit'});
+ assert.deepEqual(s[2], {text: 'ownerin:gerrittest'});
+ });
+ });
+
+ test('Autocompletes accounts with no email', () => {
+ stubRestApi('getSuggestedAccounts').callsFake(() =>
+ Promise.resolve([{name: 'fred'}])
+ );
+ return element._fetchAccounts('owner', 'fr').then(s => {
+ assert.deepEqual(s[0], {text: 'owner:"fred"', label: 'fred'});
+ });
+ });
+
+ test('Autocompletes accounts with email', () => {
+ stubRestApi('getSuggestedAccounts').callsFake(() =>
+ Promise.resolve([{email: 'fred@goog.co' as EmailAddress}])
+ );
+ return element._fetchAccounts('owner', 'fr').then(s => {
+ assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: ''});
+ });
+ });
+});
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.ts
index 733c940..462c334 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.ts
@@ -64,6 +64,10 @@
};
}
+export function getLineNumberCellWidth(prefs: DiffPreferencesInfo) {
+ return prefs.font_size * 4;
+}
+
@customElement('gr-diff-builder')
export class GrDiffBuilderElement extends PolymerElement {
static get template() {
@@ -212,7 +216,7 @@
this.$.processor.keyLocations = keyLocations;
this._clearDiffContent();
- this._builder.addColumns(this.diffElement, prefs.font_size);
+ this._builder.addColumns(this.diffElement, getLineNumberCellWidth(prefs));
const isBinary = !!(this.isImageDiff || this.diff.binary);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
index da1d971..51360b9 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
@@ -74,8 +74,7 @@
return sectionEl;
}
- addColumns(outputEl: HTMLElement, fontSize: number): void {
- const width = fontSize * 4;
+ addColumns(outputEl: HTMLElement, lineNumberWidth: number): void {
const colgroup = document.createElement('colgroup');
// Add the blame column.
@@ -84,7 +83,7 @@
// Add left-side line number.
col = this._createElement('col', 'left');
- col.setAttribute('width', width.toString());
+ col.setAttribute('width', lineNumberWidth.toString());
colgroup.appendChild(col);
// Add left-side content.
@@ -92,7 +91,7 @@
// Add right-side line number.
col = document.createElement('col');
- col.setAttribute('width', width.toString());
+ col.setAttribute('width', lineNumberWidth.toString());
colgroup.appendChild(col);
// Add right-side content.
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.ts
index 4ecfcbf..a16aa07 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.ts
@@ -78,8 +78,7 @@
return sectionEl;
}
- addColumns(outputEl: HTMLElement, fontSize: number): void {
- const width = fontSize * 4;
+ addColumns(outputEl: HTMLElement, lineNumberWidth: number): void {
const colgroup = document.createElement('colgroup');
// Add the blame column.
@@ -88,12 +87,12 @@
// Add left-side line number.
col = document.createElement('col');
- col.setAttribute('width', width.toString());
+ col.setAttribute('width', lineNumberWidth.toString());
colgroup.appendChild(col);
// Add right-side line number.
col = document.createElement('col');
- col.setAttribute('width', width.toString());
+ col.setAttribute('width', lineNumberWidth.toString());
colgroup.appendChild(col);
// Add the content.
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
index 5634238..864fd79 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
@@ -31,7 +31,11 @@
GrContextControlsShowConfig,
} from '../gr-context-controls/gr-context-controls';
import {BlameInfo} from '../../../types/common';
-import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
+import {
+ DiffInfo,
+ DiffPreferencesInfo,
+ DiffResponsiveMode,
+} from '../../../types/diff';
import {DiffViewMode, Side} from '../../../constants/constants';
import {DiffLayer} from '../../../types/types';
@@ -71,6 +75,20 @@
}
}
+export function getResponsiveMode(
+ prefs: DiffPreferencesInfo,
+ renderPrefs?: RenderPreferences
+): DiffResponsiveMode {
+ if (renderPrefs?.responsive_mode) {
+ return renderPrefs.responsive_mode;
+ }
+ // Backwards compatibility to the line_wrapping param.
+ if (prefs.line_wrapping) {
+ return 'FULL_RESPONSIVE';
+ }
+ return 'NONE';
+}
+
export abstract class GrDiffBuilder {
private readonly _diff: DiffInfo;
@@ -494,9 +512,9 @@
const {beforeNumber, afterNumber} = line;
if (beforeNumber !== 'FILE' && beforeNumber !== 'LOST') {
- const lineLimit = !this._prefs.line_wrapping
- ? this._prefs.line_length
- : Infinity;
+ const responsiveMode = getResponsiveMode(this._prefs, this._renderPrefs);
+ const lineLimit =
+ responsiveMode === 'NONE' ? this._prefs.line_length : Infinity;
const contentText = this._formatText(
line.text,
this._prefs.tab_size,
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts
index c8b3901..f418cfa 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts
@@ -23,7 +23,7 @@
lineNumberToNumber,
} from '../gr-diff/gr-diff-utils';
-const tokenMatcher = new RegExp(/[a-zA-Z0-9_-]+/g);
+const tokenMatcher = new RegExp(/[\w]+/g);
/** CSS class for all tokens. */
const CSS_TOKEN = 'token';
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
index b576896..0f8752d 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
@@ -43,6 +43,9 @@
@property({type: Boolean})
saveOnChange = false;
+ @property({type: Boolean})
+ showTooltipBelow = false;
+
private readonly restApiService = appContext.restApiService;
/** @override */
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.ts b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.ts
index 9943b58..3ebb58f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.ts
@@ -34,6 +34,7 @@
id="sideBySideBtn"
link=""
has-tooltip=""
+ position-below="[[showTooltipBelow]]"
class$="[[_computeSideBySideSelected(mode)]]"
title="Side-by-side diff"
aria-pressed="[[isSideBySideSelected(mode)]]"
@@ -45,6 +46,7 @@
id="unifiedBtn"
link=""
has-tooltip=""
+ position-below="[[showTooltipBelow]]"
title="Unified diff"
class$="[[_computeUnifiedSelected(mode)]]"
aria-pressed="[[isUnifiedSelected(mode)]]"
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts
index 8d69007d..743f905 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts
@@ -343,6 +343,7 @@
id="modeSelect"
save-on-change="[[!_diffPrefsDisabled]]"
mode="{{changeViewState.diffMode}}"
+ show-tooltip-below=""
></gr-diff-mode-selector>
</div>
<span
@@ -355,6 +356,7 @@
link=""
class="prefsButton"
has-tooltip=""
+ position-below=""
title="Diff preferences"
on-click="_handlePrefsTap"
><iron-icon icon="gr-icons:settings"></iron-icon
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
index 3d9bba7..fdde355 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
@@ -47,7 +47,10 @@
DiffPreferencesInfoKey,
} from '../../../types/diff';
import {GrDiffHighlight} from '../gr-diff-highlight/gr-diff-highlight';
-import {GrDiffBuilderElement} from '../gr-diff-builder/gr-diff-builder-element';
+import {
+ GrDiffBuilderElement,
+ getLineNumberCellWidth,
+} from '../gr-diff-builder/gr-diff-builder-element';
import {
CoverageRange,
DiffLayer,
@@ -74,7 +77,10 @@
import {isSafari, toggleClass} from '../../../utils/dom-util';
import {assertIsDefined} from '../../../utils/common-util';
import {debounce, DelayedTask} from '../../../utils/async-util';
-import {DiffContextExpandedEventDetail} from '../gr-diff-builder/gr-diff-builder';
+import {
+ DiffContextExpandedEventDetail,
+ getResponsiveMode,
+} from '../gr-diff-builder/gr-diff-builder';
const NO_NEWLINE_BASE = 'No newline at end of base file.';
const NO_NEWLINE_REVISION = 'No newline at end of revision file.';
@@ -726,33 +732,46 @@
if (!prefs) return;
this.blame = null;
+ this._updatePreferenceStyles(prefs, this.renderPrefs);
+ if (this.diff && !this.noRenderOnPrefsChange) {
+ this._debounceRenderDiffTable();
+ }
+ }
+
+ _updatePreferenceStyles(
+ prefs: DiffPreferencesInfo,
+ renderPrefs?: RenderPreferences
+ ) {
const lineLength =
this.path === COMMIT_MSG_PATH
? COMMIT_MSG_LINE_LENGTH
: prefs.line_length;
+ const sideBySide = this.viewMode === 'SIDE_BY_SIDE';
const stylesToUpdate: {[key: string]: string} = {};
- if (prefs.line_wrapping) {
- this._diffTableClass = 'full-width';
- if (this.viewMode === 'SIDE_BY_SIDE') {
- stylesToUpdate['--content-width'] = 'none';
- stylesToUpdate['--line-limit'] = `${lineLength}ch`;
- }
+ const responsiveMode = getResponsiveMode(prefs, renderPrefs);
+ const responsive =
+ responsiveMode === 'FULL_RESPONSIVE' || responsiveMode === 'SHRINK_ONLY';
+ this._diffTableClass = responsive ? 'responsive' : '';
+ const lineLimit = `${lineLength}ch`;
+ stylesToUpdate['--line-limit'] = lineLimit;
+ stylesToUpdate['--content-width'] = responsive ? 'none' : lineLimit;
+ if (responsiveMode === 'SHRINK_ONLY') {
+ // Calculating ideal (initial) width for the whole table.
+ const contentWidth = `${sideBySide ? 2 : 1} * ${lineLimit}`;
+ const lineNumberWidth = `2 * ${getLineNumberCellWidth(prefs)}px`;
+ stylesToUpdate[
+ '--diff-max-width'
+ ] = `calc(${contentWidth} + ${lineNumberWidth})`;
} else {
- this._diffTableClass = '';
- stylesToUpdate['--content-width'] = `${lineLength}ch`;
+ stylesToUpdate['--diff-max-width'] = 'none';
}
-
if (prefs.font_size) {
stylesToUpdate['--font-size'] = `${prefs.font_size}px`;
}
this.updateStyles(stylesToUpdate);
-
- if (this.diff && !this.noRenderOnPrefsChange) {
- this._debounceRenderDiffTable();
- }
}
_renderPrefsChanged(renderPrefs?: RenderPreferences) {
@@ -766,6 +785,9 @@
if (renderPrefs.hide_line_length_indicator) {
this.classList.add('hide-line-length-indicator');
}
+ if (this.prefs) {
+ this._updatePreferenceStyles(this.prefs, renderPrefs);
+ }
this.$.diffBuilder.updateRenderPrefs(renderPrefs);
}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.ts b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.ts
index d01943c..5b248da 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.ts
@@ -50,6 +50,9 @@
background-color: var(--diff-blank-background-color);
}
.diffContainer {
+ max-width: var(--diff-max-width, none);
+ }
+ .diffContainer {
display: flex;
font-family: var(--monospace-font-family);
@apply --diff-container-styles;
@@ -169,10 +172,10 @@
.image-diff .content {
background-color: var(--diff-blank-background-color);
}
- .full-width {
+ .responsive {
width: 100%;
}
- .full-width .contentText {
+ .responsive .contentText {
white-space: break-spaces;
word-wrap: break-word;
}
@@ -423,12 +426,12 @@
color: var(--link-color);
text-decoration: none;
}
- .full-width td.blame {
+ .responsive td.blame {
overflow: hidden;
width: 200px;
}
/** Support the line length indicator **/
- .full-width td.content .contentText {
+ .responsive td.content .contentText {
/*
Same strategy as in https://stackoverflow.com/questions/1179928/how-can-i-put-a-vertical-line-down-the-center-of-a-div
*/
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
index 53a2915..73b587b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
@@ -36,7 +36,7 @@
suite('gr-diff tests', () => {
let element;
- const MINIMAL_PREFS = {tab_size: 2, line_length: 80};
+ const MINIMAL_PREFS = {tab_size: 2, line_length: 80, font_size: 12};
setup(() => {
@@ -85,7 +85,66 @@
element = basicFixture.instantiate();
element.prefs = {...MINIMAL_PREFS, line_wrapping: false};
flush();
- assert.isNotOk(getComputedStyleValue('--line-limit', element));
+ assert.equal(getComputedStyleValue('--line-limit', element), '80ch');
+ });
+ suite('FULL_RESPONSIVE mode', () => {
+ setup(() => {
+ element = basicFixture.instantiate();
+ element.prefs = {...MINIMAL_PREFS};
+ element.renderPrefs = {responsive_mode: 'FULL_RESPONSIVE'};
+ });
+
+ test('line limit is based on line_length', () => {
+ element.prefs = {...element.prefs, line_length: 100};
+ flush();
+ assert.equal(getComputedStyleValue('--line-limit', element), '100ch');
+ });
+
+ test('content-width should not be defined', () => {
+ flush();
+ assert.equal(getComputedStyleValue('--content-width', element), 'none');
+ });
+ });
+
+ suite('SHRINK_ONLY mode', () => {
+ setup(() => {
+ element = basicFixture.instantiate();
+ element.prefs = {...MINIMAL_PREFS};
+ element.renderPrefs = {responsive_mode: 'SHRINK_ONLY'};
+ });
+
+ test('line limit is based on line_length', () => {
+ element.prefs = {...element.prefs, line_length: 100};
+ flush();
+ assert.equal(getComputedStyleValue('--line-limit', element), '100ch');
+ });
+
+ test('content-width should not be defined', () => {
+ flush();
+ assert.equal(getComputedStyleValue('--content-width', element), 'none');
+ });
+
+ test('max-width considers two content columns in side-by-side', () => {
+ element.viewMode = 'SIDE_BY_SIDE';
+ flush();
+ assert.equal(getComputedStyleValue('--diff-max-width', element),
+ 'calc(2 * 80ch + 2 * 48px)');
+ });
+
+ test('max-width considers one content column in unified', () => {
+ element.viewMode = 'UNIFIED_DIFF';
+ flush();
+ assert.equal(getComputedStyleValue('--diff-max-width', element),
+ 'calc(1 * 80ch + 2 * 48px)');
+ });
+
+ test('max-width considers font-size', () => {
+ element.prefs = {...element.prefs, font_size: 13};
+ flush();
+ // Each line number column: 4 * 13 = 52px
+ assert.equal(getComputedStyleValue('--diff-max-width', element),
+ 'calc(2 * 80ch + 2 * 52px)');
+ });
});
suite('not logged in', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
index cc66734..1a340b9 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
@@ -15,30 +15,25 @@
* limitations under the License.
*/
import '@polymer/iron-icon/iron-icon';
-import '../../../styles/shared-styles';
import '../gr-avatar/gr-avatar';
import '../gr-hovercard-account/gr-hovercard-account';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-account-label_html';
import {appContext} from '../../../services/app-context';
import {getDisplayName} from '../../../utils/display-name-util';
import {isSelf, isServiceUser} from '../../../utils/account-util';
-import {customElement, property} from '@polymer/decorators';
import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
import {ChangeInfo, AccountInfo, ServerInfo} from '../../../types/common';
import {hasOwnProperty} from '../../../utils/common-util';
import {fireEvent} from '../../../utils/event-util';
import {isInvolved} from '../../../utils/change-util';
import {ShowAlertEventDetail} from '../../../types/events';
+import {GrLitElement} from '../../lit/gr-lit-element';
+import {css, customElement, html, property, state} from 'lit-element';
+import {classMap} from 'lit-html/directives/class-map';
@customElement('gr-account-label')
-export class GrAccountLabel extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrAccountLabel extends GrLitElement {
@property({type: Object})
- account!: AccountInfo;
+ account?: AccountInfo;
@property({type: Object})
_selfAccount?: AccountInfo;
@@ -49,7 +44,7 @@
* related features like adding the user as a reviewer.
*/
@property({type: Object})
- change!: ChangeInfo;
+ change?: ChangeInfo;
@property({type: String})
voteableText?: string;
@@ -83,44 +78,190 @@
@property({
type: Boolean,
- reflectToAttribute: true,
- computed:
- '_computeCancelLeftPadding(hideAvatar, ' +
- 'highlightAttention, account, change, forceAttention)',
+ reflect: true,
})
cancelLeftPadding = false;
@property({type: Boolean})
hideStatus = false;
- @property({type: Object})
+ @state()
_config?: ServerInfo;
- @property({type: Boolean, reflectToAttribute: true})
+ @property({type: Boolean, reflect: true})
selectionChipStyle = false;
@property({
type: Boolean,
- reflectToAttribute: true,
- observer: 'selectedChanged',
+ reflect: true,
})
selected = false;
- @property({type: Boolean, reflectToAttribute: true})
+ @property({type: Boolean, reflect: true})
deselected = false;
reporting: ReportingService;
private readonly restApiService = appContext.restApiService;
+ static get styles() {
+ return [
+ css`
+ :host {
+ display: inline-block;
+ vertical-align: top;
+ position: relative;
+ border-radius: var(--label-border-radius);
+ box-sizing: border-box;
+ white-space: nowrap;
+ padding: 0 var(--account-label-padding-horizontal, 0);
+ }
+ /* If the first element is the avatar, then we cancel the left padding,
+ so we can fit nicely into the gr-account-chip rounding. The obvious
+ alternative of 'chip has padding' and 'avatar gets negative margin'
+ does not work, because we need 'overflow:hidden' on the label. */
+ :host([cancelLeftPadding]) {
+ padding-left: 0;
+ }
+ :host::after {
+ content: var(--account-label-suffix);
+ }
+ :host([deselected][selectionChipStyle]) {
+ background-color: var(--background-color-primary);
+ border: 1px solid var(--comment-separator-color);
+ border-radius: 8px;
+ color: var(--deemphasized-text-color);
+ }
+ :host([selected][selectionChipStyle]) {
+ background-color: var(--chip-selected-background-color);
+ border: 1px solid var(--chip-selected-background-color);
+ border-radius: 8px;
+ color: var(--chip-selected-text-color);
+ }
+ :host([selected]) iron-icon.attention {
+ color: var(--chip-selected-text-color);
+ }
+ gr-avatar {
+ height: calc(var(--line-height-normal) - 2px);
+ width: calc(var(--line-height-normal) - 2px);
+ vertical-align: top;
+ position: relative;
+ top: 1px;
+ }
+ #attentionButton {
+ /* This negates the 4px horizontal padding, which we appreciate as a
+ larger click target, but which we don't want to consume space. :-) */
+ margin: 0 -4px 0 -4px;
+ vertical-align: top;
+ }
+ iron-icon.attention {
+ color: var(--deemphasized-text-color);
+ width: 12px;
+ height: 12px;
+ vertical-align: top;
+ }
+ iron-icon.status {
+ color: var(--deemphasized-text-color);
+ width: 14px;
+ height: 14px;
+ vertical-align: top;
+ position: relative;
+ top: 2px;
+ }
+ .name {
+ display: inline-block;
+ text-decoration: inherit;
+ vertical-align: top;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: var(--account-max-length, 180px);
+ }
+ .hasAttention .name {
+ font-weight: var(--font-weight-bold);
+ }
+ `,
+ ];
+ }
+
+ render() {
+ const {account, change, highlightAttention, forceAttention} = this;
+ if (!account) return;
+ const hasAttention =
+ forceAttention ||
+ this._hasUnforcedAttention(highlightAttention, account, change);
+ this.deselected = !this.selected;
+ this.cancelLeftPadding = !this.hideAvatar && !hasAttention;
+ return html`<span>
+ ${!this.hideHovercard
+ ? html`<gr-hovercard-account
+ for="hovercardTarget"
+ .account="${account}"
+ .change="${change}"
+ ?highlight-attention=${highlightAttention}
+ .voteableText=${this.voteableText}
+ ></gr-hovercard-account>`
+ : ''}
+ ${hasAttention
+ ? html`<gr-button
+ id="attentionButton"
+ link=""
+ aria-label="Remove user from attention set"
+ @click=${this._handleRemoveAttentionClick}
+ ?disabled=${!this._computeAttentionButtonEnabled(
+ highlightAttention,
+ account,
+ change,
+ this.selected,
+ this._selfAccount
+ )}
+ ?has-tooltip=${this._computeAttentionButtonEnabled(
+ highlightAttention,
+ account,
+ change,
+ false,
+ this._selfAccount
+ )}
+ title="${this._computeAttentionIconTitle(
+ highlightAttention,
+ account,
+ change,
+ forceAttention,
+ this.selected,
+ this._selfAccount
+ )}"
+ ><iron-icon
+ class="attention"
+ icon="gr-icons:attention"
+ ></iron-icon>
+ </gr-button>`
+ : ''}
+ </span>
+ <span
+ id="hovercardTarget"
+ class="${classMap({
+ hasAttention: !!hasAttention,
+ })}"
+ >
+ ${!this.hideAvatar
+ ? html`<gr-avatar .account="${account}" imageSize="32"></gr-avatar>`
+ : ''}
+ <span class="text" part="gr-account-label-text">
+ <span class="name"
+ >${this._computeName(account, this.firstName, this._config)}</span
+ >
+ ${!this.hideStatus && account.status
+ ? html`<iron-icon
+ class="status"
+ icon="gr-icons:calendar"
+ ></iron-icon>`
+ : ''}
+ </span>
+ </span>`;
+ }
+
constructor() {
super();
this.reporting = appContext.reportingService;
- }
-
- /** @override */
- ready() {
- super.ready();
this.restApiService.getConfig().then(config => {
this._config = config;
});
@@ -129,72 +270,42 @@
});
this.addEventListener('attention-set-updated', () => {
// For re-evaluation of everything that depends on 'change'.
- this.change = {...this.change};
+ if (this.change) this.change = {...this.change};
});
}
- selectedChanged(selected?: boolean) {
- this.deselected = !selected;
- }
-
_isAttentionSetEnabled(
highlight: boolean,
account: AccountInfo,
- change: ChangeInfo
+ change?: ChangeInfo
) {
return highlight && !!change && !!account && !isServiceUser(account);
}
- _computeCancelLeftPadding(
- hideAvatar: boolean,
- highlight: boolean,
- account: AccountInfo,
- change: ChangeInfo,
- force: boolean
- ) {
- return (
- !hideAvatar && !this._hasAttention(highlight, account, change, force)
- );
- }
-
- _hasAttention(
- highlight: boolean,
- account: AccountInfo,
- change: ChangeInfo,
- force: boolean
- ) {
- return force || this._hasUnforcedAttention(highlight, account, change);
- }
-
_hasUnforcedAttention(
highlight: boolean,
account: AccountInfo,
- change: ChangeInfo
+ change?: ChangeInfo
) {
return (
this._isAttentionSetEnabled(highlight, account, change) &&
+ change &&
change.attention_set &&
!!account._account_id &&
hasOwnProperty(change.attention_set, account._account_id)
);
}
- _computeHasAttentionClass(
- highlight: boolean,
- account: AccountInfo,
- change: ChangeInfo,
- force: boolean
+ _computeName(
+ account?: AccountInfo,
+ firstName?: boolean,
+ config?: ServerInfo
) {
- return this._hasAttention(highlight, account, change, force)
- ? 'hasAttention'
- : '';
- }
-
- _computeName(account: AccountInfo, firstName: boolean, config?: ServerInfo) {
return getDisplayName(config, account, firstName);
}
_handleRemoveAttentionClick(e: MouseEvent) {
+ if (!this.account || !this.change) return;
if (this.selected) return;
e.preventDefault();
e.stopPropagation();
@@ -236,6 +347,7 @@
}
_reportingDetails() {
+ if (!this.account) return;
const targetId = this.account._account_id;
const ownerId =
(this.change && this.change.owner && this.change.owner._account_id) || -1;
@@ -259,7 +371,7 @@
_computeAttentionButtonEnabled(
highlight: boolean,
account: AccountInfo,
- change: ChangeInfo,
+ change: ChangeInfo | undefined,
selected: boolean,
selfAccount?: AccountInfo
) {
@@ -273,7 +385,7 @@
_computeAttentionIconTitle(
highlight: boolean,
account: AccountInfo,
- change: ChangeInfo,
+ change: ChangeInfo | undefined,
force: boolean,
selected: boolean,
selfAccount?: AccountInfo
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.ts b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.ts
deleted file mode 100644
index 352763b..0000000
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.ts
+++ /dev/null
@@ -1,137 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 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 {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style>
- :host {
- display: inline-block;
- vertical-align: top;
- position: relative;
- border-radius: var(--label-border-radius);
- box-sizing: border-box;
- white-space: nowrap;
- padding: 0 var(--account-label-padding-horizontal, 0);
- }
- /* If the first element is the avatar, then we cancel the left padding, so
- we can fit nicely into the gr-account-chip rounding.
- The obvious alternative of 'chip has padding' and 'avatar gets negative
- margin' does not work, because we need 'overflow:hidden' on the label. */
- :host([cancel-left-padding]) {
- padding-left: 0;
- }
- :host::after {
- content: var(--account-label-suffix);
- }
- :host([deselected][selection-chip-style]) {
- background-color: var(--background-color-primary);
- border: 1px solid var(--comment-separator-color);
- border-radius: 8px;
- color: var(--deemphasized-text-color);
- }
- :host([selected][selection-chip-style]) {
- background-color: var(--chip-selected-background-color);
- border: 1px solid var(--chip-selected-background-color);
- border-radius: 8px;
- color: var(--chip-selected-text-color);
- }
- :host([selected]) iron-icon.attention {
- color: var(--chip-selected-text-color);
- }
- gr-avatar {
- height: calc(var(--line-height-normal) - 2px);
- width: calc(var(--line-height-normal) - 2px);
- vertical-align: top;
- position: relative;
- top: 1px;
- }
- #attentionButton {
- /* This negates the 4px horizontal padding, which we appreciate as a
- larger click target, but which we don't want to consume space. :-) */
- margin: 0 -4px 0 -4px;
- vertical-align: top;
- }
- iron-icon.attention {
- color: var(--deemphasized-text-color);
- width: 12px;
- height: 12px;
- vertical-align: top;
- }
- iron-icon.status {
- color: var(--deemphasized-text-color);
- width: 14px;
- height: 14px;
- vertical-align: top;
- position: relative;
- top: 2px;
- }
- .name {
- display: inline-block;
- text-decoration: inherit;
- vertical-align: top;
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: var(--account-max-length, 180px);
- }
- .hasAttention .name {
- font-weight: var(--font-weight-bold);
- }
- </style>
- <span>
- <template is="dom-if" if="[[!hideHovercard]]">
- <gr-hovercard-account
- for="hovercardTarget"
- account="[[account]]"
- change="[[change]]"
- highlight-attention="[[highlightAttention]]"
- voteable-text="[[voteableText]]"
- >
- </gr-hovercard-account>
- </template>
- <template
- is="dom-if"
- if="[[_hasAttention(highlightAttention, account, change, forceAttention)]]"
- >
- <gr-button
- id="attentionButton"
- link=""
- aria-label="Remove user from attention set"
- on-click="_handleRemoveAttentionClick"
- disabled="[[!_computeAttentionButtonEnabled(highlightAttention, account, change, selected, _selfAccount)]]"
- has-tooltip="[[_computeAttentionButtonEnabled(highlightAttention, account, change, false, _selfAccount)]]"
- title="[[_computeAttentionIconTitle(highlightAttention, account, change, forceAttention, selected, _selfAccount)]]"
- ><iron-icon class="attention" icon="gr-icons:attention"></iron-icon>
- </gr-button>
- </template>
- </span>
- <span
- id="hovercardTarget"
- class$="[[_computeHasAttentionClass(highlightAttention, account, change, forceAttention)]]"
- >
- <template is="dom-if" if="[[!hideAvatar]]">
- <gr-avatar account="[[account]]" imageSize="32"></gr-avatar>
- </template>
- <span class="text" part="gr-account-label-text">
- <span class="name">[[_computeName(account, firstName, _config)]]</span>
- <template is="dom-if" if="[[!hideStatus]]">
- <template is="dom-if" if="[[account.status]]">
- <iron-icon class="status" icon="gr-icons:calendar"></iron-icon>
- </template>
- </template>
- </span>
- </span>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.ts b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.ts
index 317806c..a610ffa 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.ts
@@ -89,12 +89,12 @@
<gr-account-label
.account="${this.account}"
.change="${this.change}"
- ?force-attention=${this.forceAttention}
- ?highlight-attention=${this.highlightAttention}
- ?hide-avatar=${this.hideAvatar}
- ?hide-status=${this.hideStatus}
- ?first-name=${this.firstName}
- .voteable-text=${this.voteableText}
+ ?forceAttention=${this.forceAttention}
+ ?highlightAttention=${this.highlightAttention}
+ ?hideAvatar=${this.hideAvatar}
+ ?hideStatus=${this.hideStatus}
+ ?firstName=${this.firstName}
+ .voteableText=${this.voteableText}
part="gr-account-link-text => gr-account-label-text"
>
</gr-account-label>
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.js b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.ts
similarity index 60%
rename from polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.js
rename to polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.ts
index df8632f..bb70855 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.ts
@@ -15,19 +15,26 @@
* limitations under the License.
*/
-import '../../../test/common-test-setup-karma.js';
-import './gr-avatar.js';
-import {getPluginLoader} from '../gr-js-api-interface/gr-plugin-loader.js';
-import {appContext} from '../../../services/app-context.js';
+import '../../../test/common-test-setup-karma';
+import './gr-avatar';
+import {GrAvatar} from './gr-avatar';
+import {getPluginLoader} from '../gr-js-api-interface/gr-plugin-loader';
+import {appContext} from '../../../services/app-context';
+import {AvatarInfo} from '../../../types/common';
+import {
+ createAccountWithEmail,
+ createAccountWithId,
+} from '../../../test/test-data-generators';
const basicFixture = fixtureFromElement('gr-avatar');
suite('gr-avatar tests', () => {
- let element;
- const defaultAvatars = [
+ let element: GrAvatar;
+ const defaultAvatars: AvatarInfo[] = [
{
url: 'https://cdn.example.com/s12-p/photo.jpg',
height: 12,
+ width: 0,
},
];
@@ -36,68 +43,74 @@
});
test('account without avatar', () => {
- assert.equal(
- element._buildAvatarURL({
- _account_id: 123,
- }),
- '');
+ assert.equal(element._buildAvatarURL(createAccountWithId(123)), '');
});
test('methods', () => {
assert.equal(
- element._buildAvatarURL({
- _account_id: 123,
- avatars: defaultAvatars,
- }),
- '/accounts/123/avatar?s=16');
+ element._buildAvatarURL({
+ ...createAccountWithId(123),
+ avatars: defaultAvatars,
+ }),
+ '/accounts/123/avatar?s=16'
+ );
assert.equal(
- element._buildAvatarURL({
- email: 'test@example.com',
- avatars: defaultAvatars,
- }),
- '/accounts/test%40example.com/avatar?s=16');
+ element._buildAvatarURL({
+ ...createAccountWithEmail('test@example.com'),
+ avatars: defaultAvatars,
+ }),
+ '/accounts/test%40example.com/avatar?s=16'
+ );
assert.equal(
- element._buildAvatarURL({
- name: 'John Doe',
- avatars: defaultAvatars,
- }),
- '/accounts/John%20Doe/avatar?s=16');
+ element._buildAvatarURL({
+ name: 'John Doe',
+ avatars: defaultAvatars,
+ }),
+ '/accounts/John%20Doe/avatar?s=16'
+ );
assert.equal(
- element._buildAvatarURL({
- username: 'John_Doe',
- avatars: defaultAvatars,
- }),
- '/accounts/John_Doe/avatar?s=16');
+ element._buildAvatarURL({
+ username: 'John_Doe',
+ avatars: defaultAvatars,
+ }),
+ '/accounts/John_Doe/avatar?s=16'
+ );
assert.equal(
- element._buildAvatarURL({
- _account_id: 123,
- avatars: [
- {
- url: 'https://cdn.example.com/s12-p/photo.jpg',
- height: 12,
- },
- {
- url: 'https://cdn.example.com/s16-p/photo.jpg',
- height: 16,
- },
- {
- url: 'https://cdn.example.com/s100-p/photo.jpg',
- height: 100,
- },
- ],
- }),
- 'https://cdn.example.com/s16-p/photo.jpg');
+ element._buildAvatarURL({
+ ...createAccountWithId(123),
+ avatars: [
+ {
+ url: 'https://cdn.example.com/s12-p/photo.jpg',
+ height: 12,
+ width: 0,
+ },
+ {
+ url: 'https://cdn.example.com/s16-p/photo.jpg',
+ height: 16,
+ width: 0,
+ },
+ {
+ url: 'https://cdn.example.com/s100-p/photo.jpg',
+ height: 100,
+ width: 0,
+ },
+ ] as AvatarInfo[],
+ }),
+ 'https://cdn.example.com/s16-p/photo.jpg'
+ );
assert.equal(
- element._buildAvatarURL({
- _account_id: 123,
- avatars: [
- {
- url: 'https://cdn.example.com/s95-p/photo.jpg',
- height: 95,
- },
- ],
- }),
- '/accounts/123/avatar?s=16');
+ element._buildAvatarURL({
+ ...createAccountWithId(123),
+ avatars: [
+ {
+ url: 'https://cdn.example.com/s95-p/photo.jpg',
+ height: 95,
+ width: 0,
+ },
+ ] as AvatarInfo[],
+ }),
+ '/accounts/123/avatar?s=16'
+ );
assert.equal(element._buildAvatarURL(undefined), '');
});
@@ -114,7 +127,7 @@
element.imageSize = 64;
element.account = {
- _account_id: 123,
+ ...createAccountWithId(123),
avatars: defaultAvatars,
};
flush();
@@ -131,14 +144,14 @@
assert.isFalse(element.hasAttribute('hidden'));
assert.isTrue(
- element.style.backgroundImage.includes(
- '/accounts/123/avatar?s=64'));
+ element.style.backgroundImage.includes('/accounts/123/avatar?s=64')
+ );
});
});
});
suite('plugin has avatars', () => {
- let element;
+ let element: GrAvatar;
setup(() => {
stub('gr-avatar', '_getConfig').callsFake(() =>
@@ -166,7 +179,7 @@
});
suite('config not set', () => {
- let element;
+ let element: GrAvatar;
setup(() => {
stub('gr-avatar', '_getConfig').callsFake(() => Promise.resolve({}));
@@ -180,7 +193,7 @@
element.imageSize = 64;
element.account = {
- _account_id: 123,
+ ...createAccountWithId(123),
avatars: defaultAvatars,
};
// Emulate plugins loaded.
@@ -195,4 +208,3 @@
});
});
});
-
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts
index 65e8e9f..0bd02d5 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts
@@ -77,11 +77,11 @@
@property({type: Object})
resolveWeblinks?: GeneratedWebLink[] = [];
- _computeStatusString(status: ChangeStates) {
+ _computeStatusString(status?: ChangeStates) {
if (status === ChangeStates.WIP && !this.flat) {
return 'Work in Progress';
}
- return status;
+ return status ?? '';
}
_toClassName(str?: ChangeStates) {
@@ -107,14 +107,14 @@
revertedChange?: ChangeInfo,
resolveWeblinks?: GeneratedWebLink[],
status?: ChangeStates
- ): string | undefined {
+ ): string {
if (revertedChange) {
return GerritNav.getUrlForSearchQuery(`${revertedChange._number}`);
}
if (status === ChangeStates.MERGE_CONFLICT && resolveWeblinks?.length) {
- return resolveWeblinks[0].url;
+ return resolveWeblinks[0].url ?? '';
}
- return undefined;
+ return '';
}
showResolveIcon(
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts
index a848b2f..3e0b9a4 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts
@@ -262,7 +262,7 @@
<gr-account-label
account="[[_getAuthor(comment, _selfAccount)]]"
class$="[[_computeAccountLabelClass(draft)]]"
- hide-status=""
+ hideStatus
>
</gr-account-label>
</template>
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
index bbf3442..a218959 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
@@ -367,23 +367,20 @@
getTimerStub = stubReporting('getTimer').returns(mockTimer);
});
- test('create', () => {
+ test('create', async () => {
element.patchNum = 1 as PatchSetNum;
element.comment = {};
- return element._handleSave(mockEvent)!.then(() => {
- assert.equal(
- (queryAndAssert(
- element,
- 'gr-account-label'
- ).shadowRoot?.querySelector(
- 'span.name'
- ) as HTMLSpanElement).innerText.trim(),
- 'Dhruv Srivastava'
- );
- assert.isTrue(endStub.calledOnce);
- assert.isTrue(getTimerStub.calledOnce);
- assert.equal(getTimerStub.lastCall.args[0], 'CreateDraftComment');
- });
+ await element._handleSave(mockEvent);
+ await flush();
+ const grAccountLabel = queryAndAssert(element, 'gr-account-label');
+ const spanName = queryAndAssert<HTMLSpanElement>(
+ grAccountLabel,
+ 'span.name'
+ );
+ assert.equal(spanName.innerText.trim(), 'Dhruv Srivastava');
+ assert.isTrue(endStub.calledOnce);
+ assert.isTrue(getTimerStub.calledOnce);
+ assert.equal(getTimerStub.lastCall.args[0], 'CreateDraftComment');
});
test('update', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
index 4a2bcee..36cffb7 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
@@ -20,6 +20,7 @@
import {IronIconElement} from '@polymer/iron-icon';
import {assertIsDefined, queryAndAssert} from '../../../utils/common-util';
import {classMap} from 'lit-html/directives/class-map';
+import {ifDefined} from 'lit-html/directives/if-defined';
import {css, customElement, html, property} from 'lit-element';
import {GrLitElement} from '../../lit/gr-lit-element';
import {GrButton} from '../gr-button/gr-button';
@@ -126,7 +127,7 @@
link=""
?has-tooltip=${this.hasTooltip}
class="copyToClipboard"
- title="${this.buttonTitle}"
+ title="${ifDefined(this.buttonTitle)}"
@click="${this._copyToClipboard}"
aria-label="Click to copy to clipboard"
>
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.js b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.ts
similarity index 73%
rename from polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.js
rename to polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.ts
index 45847d7..ef62fe9 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.ts
@@ -15,14 +15,16 @@
* limitations under the License.
*/
-import '../../../test/common-test-setup-karma.js';
-import './gr-copy-clipboard.js';
-import {queryAndAssert} from '../../../test/test-utils.js';
+import '../../../test/common-test-setup-karma';
+import './gr-copy-clipboard';
+import {GrCopyClipboard} from './gr-copy-clipboard';
+import {queryAndAssert} from '../../../test/test-utils';
+import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
const basicFixture = fixtureFromElement('gr-copy-clipboard');
suite('gr-copy-clipboard tests', () => {
- let element;
+ let element: GrCopyClipboard;
setup(async () => {
element = basicFixture.instantiate();
@@ -33,35 +35,34 @@
test('copy to clipboard', () => {
const clipboardSpy = sinon.spy(navigator.clipboard, 'writeText');
- const copyBtn = element.shadowRoot
- .querySelector('.copyToClipboard');
+ const copyBtn = queryAndAssert(element, '.copyToClipboard');
MockInteractions.click(copyBtn);
assert.isTrue(clipboardSpy.called);
});
test('focusOnCopy', () => {
element.focusOnCopy();
- const activeElement = element.shadowRoot.activeElement;
- const button = element.shadowRoot.querySelector('.copyToClipboard');
+ const activeElement = element.shadowRoot!.activeElement;
+ const button = queryAndAssert(element, '.copyToClipboard');
assert.deepEqual(activeElement, button);
});
test('_handleInputClick', () => {
// iron-input as parent should never be hidden as copy won't work
// on nested hidden elements
- const ironInputElement = element.shadowRoot.querySelector('iron-input');
+ const ironInputElement = queryAndAssert(element, 'iron-input');
assert.notEqual(getComputedStyle(ironInputElement).display, 'none');
- const inputElement = element.shadowRoot.querySelector('input');
+ const inputElement = queryAndAssert(element, 'input') as HTMLInputElement;
MockInteractions.tap(inputElement);
assert.equal(inputElement.selectionStart, 0);
- assert.equal(inputElement.selectionEnd, element.text.length - 1);
+ assert.equal(inputElement.selectionEnd, element.text!.length! - 1);
});
test('hideInput', async () => {
// iron-input as parent should never be hidden as copy won't work
// on nested hidden elements
- const ironInputElement = element.shadowRoot.querySelector('iron-input');
+ const ironInputElement = queryAndAssert(element, 'iron-input');
assert.notEqual(getComputedStyle(ironInputElement).display, 'none');
const input = queryAndAssert(element, 'input');
@@ -76,10 +77,8 @@
divParent.appendChild(element);
const clickStub = sinon.stub();
divParent.addEventListener('click', clickStub);
- element.stopPropagation = true;
- const copyBtn = element.shadowRoot.querySelector('.copyToClipboard');
+ const copyBtn = queryAndAssert(element, '.copyToClipboard');
MockInteractions.tap(copyBtn);
assert.isFalse(clickStub.called);
});
});
-
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
index 017ba50..9dce127 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
@@ -87,7 +87,7 @@
}
/**
- * Move the cursor forward. Clipped to the ends of the stop list.
+ * Move the cursor forward. Clipped to the end of the stop list.
*
* @param options.filter Skips any stops for which filter returns false.
* @param options.getTargetHeight Optional function to calculate the
@@ -95,22 +95,36 @@
* sometimes different, used by the diff cursor.
* @param options.clipToTop When none of the next indices match, move
* back to first instead of to last.
+ * @param options.circular When on last element, you get to first element.
* @return If a move was performed or why not.
- * @private
*/
next(
options: {
filter?: (stop: HTMLElement) => boolean;
getTargetHeight?: (target: HTMLElement) => number;
clipToTop?: boolean;
+ circular?: boolean;
} = {}
): CursorMoveResult {
return this._moveCursor(1, options);
}
+ /**
+ * Move the cursor backward. Clipped to the beginning of stop list.
+ *
+ * @param options.filter Skips any stops for which filter returns false.
+ * @param options.getTargetHeight Optional function to calculate the
+ * height of the target's 'section'. The height of the target itself is
+ * sometimes different, used by the diff cursor.
+ * @param options.clipToTop When none of the next indices match, move
+ * back to first instead of to last.
+ * @param options.circular When on first element, you get to last element.
+ * @return If a move was performed or why not.
+ */
previous(
options: {
filter?: (stop: HTMLElement) => boolean;
+ circular?: boolean;
} = {}
): CursorMoveResult {
return this._moveCursor(-1, options);
@@ -276,34 +290,18 @@
}
}
- /**
- * Move the cursor forward or backward by delta. Clipped to the beginning or
- * end of stop list.
- *
- * @param delta either -1 or 1.
- * @param options.abort Will abort moving the cursor when encountering a
- * stop for which this condition is met. Will abort even if the stop
- * would have been filtered
- * @param options.filter Will keep going and skip any stops for which this
- * condition is not met.
- * @param options.getTargetHeight Optional function to calculate the
- * height of the target's 'section'. The height of the target itself is
- * sometimes different, used by the diff cursor.
- * @param options.clipToTop When none of the next indices match, move
- * back to first instead of to last.
- * @return If a move was performed or why not.
- * @private
- */
_moveCursor(
delta: number,
{
filter,
getTargetHeight,
clipToTop,
+ circular,
}: {
filter?: (stop: HTMLElement) => boolean;
getTargetHeight?: (target: HTMLElement) => number;
clipToTop?: boolean;
+ circular?: boolean;
} = {}
): CursorMoveResult {
if (!this.stops.length) {
@@ -326,7 +324,10 @@
(delta > 0 && newIndex >= this.stops.length) ||
(delta < 0 && newIndex < 0)
) {
- newIndex = delta < 0 || clipToTop ? 0 : this.stops.length - 1;
+ newIndex =
+ (delta < 0 && !circular) || (delta > 0 && circular) || clipToTop
+ ? 0
+ : this.stops.length - 1;
newStop = this.stops[newIndex];
clipped = true;
break;
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.js b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.js
index ba7e4f8..d0bd420 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.js
@@ -255,6 +255,25 @@
assert.isTrue(cursor.target.focus.called);
});
+ suite('circular options', () => {
+ const options = {circular: true};
+ setup(() => {
+ cursor.stops = [...list.querySelectorAll('li')];
+ });
+
+ test('previous() on first element goes to last element', () => {
+ cursor.setCursor(list.children[0]);
+ cursor.previous(options);
+ assert.equal(cursor.index, list.children.length - 1);
+ });
+
+ test('next() on last element goes to first element', () => {
+ cursor.setCursor(list.children[list.children.length - 1]);
+ cursor.next(options);
+ assert.equal(cursor.index, 0);
+ });
+ });
+
suite('_scrollToTarget', () => {
let scrollStub;
setup(() => {
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
index 83cd380..592efba 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
@@ -106,13 +106,14 @@
_saveDisabled!: boolean;
@property({type: String, observer: '_newContentChanged'})
- _newContent?: string;
+ _newContent = '';
private readonly storage = appContext.storageService;
private readonly reporting = appContext.reportingService;
- private storeTask?: DelayedTask;
+ // Tests use this so needs to be non private
+ storeTask?: DelayedTask;
/** @override */
ready() {
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.ts b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.ts
index c6ff903..7877a1f 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.ts
@@ -69,7 +69,7 @@
box-shadow: var(--elevation-level-1);
/* slightly up to cover rounded corner of the commit msg */
margin-top: calc(-1 * var(--spacing-xs));
- /* To make this bar pop over editor, since editor has relative position.
+ /* To make this bar pop over editor, since editor has relative position.
*/
position: relative;
}
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.js b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.ts
similarity index 73%
rename from polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.js
rename to polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.ts
index 94a7b96..074678e 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.ts
@@ -15,13 +15,17 @@
* limitations under the License.
*/
-import '../../../test/common-test-setup-karma.js';
-import './gr-editable-content.js';
+import '../../../test/common-test-setup-karma';
+import './gr-editable-content';
+import {GrEditableContent} from './gr-editable-content';
+import {queryAndAssert, stubStorage} from '../../../test/test-utils';
+import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
+import {GrButton} from '../gr-button/gr-button';
const basicFixture = fixtureFromElement('gr-editable-content');
suite('gr-editable-content tests', () => {
- let element;
+ let element: GrEditableContent;
setup(() => {
element = basicFixture.instantiate();
@@ -33,8 +37,7 @@
const handler = sinon.spy();
element.addEventListener('editable-content-save', handler);
- MockInteractions.tap(element.shadowRoot
- .querySelector('gr-button[primary]'));
+ MockInteractions.tap(queryAndAssert(element, 'gr-button[primary]'));
assert.isTrue(handler.called);
assert.equal(handler.lastCall.args[0].detail.content, 'foo');
@@ -44,8 +47,7 @@
const handler = sinon.spy();
element.addEventListener('editable-content-cancel', handler);
- MockInteractions.tap(element.shadowRoot
- .querySelector('gr-button.cancel-button'));
+ MockInteractions.tap(queryAndAssert(element, 'gr-button.cancel-button'));
assert.isTrue(handler.called);
});
@@ -79,19 +81,22 @@
});
test('save button is disabled initially', () => {
- assert.isTrue(element.shadowRoot
- .querySelector('gr-button[primary]').disabled);
+ assert.isTrue(
+ queryAndAssert<GrButton>(element, 'gr-button[primary]').disabled
+ );
});
test('save button is enabled when content changes', () => {
element._newContent = 'new content';
- assert.isFalse(element.shadowRoot
- .querySelector('gr-button[primary]').disabled);
+ assert.isFalse(
+ queryAndAssert<GrButton>(element, 'gr-button[primary]').disabled
+ );
});
});
suite('storageKey and related behavior', () => {
- let dispatchSpy;
+ let dispatchSpy: sinon.SinonSpy;
+
setup(() => {
element.content = 'current content';
element.storageKey = 'test';
@@ -99,8 +104,10 @@
});
test('editing toggled to true, has stored data', () => {
- sinon.stub(element.storage, 'getEditableContentItem')
- .returns({message: 'stored content'});
+ stubStorage('getEditableContentItem').returns({
+ message: 'stored content',
+ updated: 0,
+ });
element.editing = true;
assert.equal(element._newContent, 'stored content');
@@ -109,8 +116,7 @@
});
test('editing toggled to true, has no stored data', () => {
- sinon.stub(element.storage, 'getEditableContentItem')
- .returns({});
+ stubStorage('getEditableContentItem').returns(null);
element.editing = true;
assert.equal(element._newContent, 'current content');
@@ -118,28 +124,26 @@
});
test('edits are cached', () => {
- const storeStub =
- sinon.stub(element.storage, 'setEditableContentItem');
- const eraseStub =
- sinon.stub(element.storage, 'eraseEditableContentItem');
+ const storeStub = stubStorage('setEditableContentItem');
+ const eraseStub = stubStorage('eraseEditableContentItem');
element.editing = true;
element._newContent = 'new content';
flush();
- element.storeTask.flush();
+ element.storeTask?.flush();
assert.isTrue(storeStub.called);
assert.deepEqual(
- [element.storageKey, element._newContent],
- storeStub.lastCall.args);
+ [element.storageKey, element._newContent],
+ storeStub.lastCall.args
+ );
element._newContent = '';
flush();
- element.storeTask.flush();
+ element.storeTask?.flush();
assert.isTrue(eraseStub.called);
assert.deepEqual([element.storageKey], eraseStub.lastCall.args);
});
});
});
-
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
index 0193197..ddca5c5 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
@@ -183,7 +183,9 @@
// include pre or all regular lines but stop at next new line
while (
this._isPreFormat(lines[nextI]) ||
- (this._isRegularLine(lines[nextI]) && lines[nextI].length)
+ (this._isRegularLine(lines[nextI]) &&
+ !this._isWhitespaceLine(lines[nextI]) &&
+ lines[nextI].length)
) {
nextI++;
}
@@ -255,13 +257,17 @@
}
_isPreFormat(line: string) {
- return line && /^[ \t]/.test(line);
+ return line && /^[ \t]/.test(line) && !this._isWhitespaceLine(line);
}
_isList(line: string) {
return line && /^[-*] /.test(line);
}
+ _isWhitespaceLine(line: string) {
+ return line && /^\s+$/.test(line);
+ }
+
_makeLinkedText(content = '', isPre?: boolean) {
const text = document.createElement('gr-linked-text');
text.config = this.config;
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.js b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.js
index 3e05f11..8464af7 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.js
@@ -297,6 +297,14 @@
assertBlock(result, 1, 'paragraph', 'B');
});
+ test('pre format 5', () => {
+ const comment = ' Q\n <R>\n S\n \nB';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 2);
+ assertBlock(result, 0, 'pre', ' Q\n <R>\n S');
+ assertBlock(result, 1, 'paragraph', ' \nB');
+ });
+
test('quote 1', () => {
const comment = '> I\'m happy\n > with quotes!\n\nSee above.';
const result = element._computeBlocks(comment);
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.ts b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.ts
index 2812b47..287ed1b 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.ts
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.ts
@@ -14,12 +14,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import '../../../styles/shared-styles';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-linked-text_html';
import {GrLinkTextParser, LinkTextParserConfig} from './link-text-parser';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
-import {customElement, property, observe} from '@polymer/decorators';
+import {GrLitElement} from '../../lit/gr-lit-element';
+import {css, customElement, html, property, query} from 'lit-element';
declare global {
interface HTMLElementTagNameMap {
@@ -27,17 +25,10 @@
}
}
-export interface GrLinkedText {
- $: {
- output: HTMLSpanElement;
- };
-}
-
@customElement('gr-linked-text')
-export class GrLinkedText extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
+export class GrLinkedText extends GrLitElement {
+ @query('#output')
+ outputElement?: HTMLSpanElement;
@property({type: Boolean})
removeZeroWidthSpace?: boolean;
@@ -46,61 +37,63 @@
@property({type: String})
content: string | null = null;
- @property({type: Boolean, reflectToAttribute: true})
+ @property({type: Boolean, reflect: true})
pre = false;
- @property({type: Boolean, reflectToAttribute: true})
+ @property({type: Boolean, reflect: true})
disabled = false;
@property({type: Object})
config?: LinkTextParserConfig;
- @observe('content')
- _contentChanged(content: string | null) {
- // In the case where the config may not be set (perhaps due to the
- // request for it still being in flight), set the content anyway to
- // prevent waiting on the config to display the text.
+ static get styles() {
+ return [
+ css`
+ :host {
+ display: block;
+ }
+ :host([pre]) span {
+ white-space: var(--linked-text-white-space, pre-wrap);
+ word-wrap: var(--linked-text-word-wrap, break-word);
+ }
+ :host([disabled]) a {
+ color: inherit;
+ text-decoration: none;
+ pointer-events: none;
+ }
+ a {
+ color: var(--link-color);
+ }
+ `,
+ ];
+ }
+
+ render() {
if (!this.config) {
return;
}
- this.$.output.textContent = content;
+ return html`<span id="output">${this.content}</span>`;
}
- /**
- * Because either the source text or the linkification config has changed,
- * the content should be re-parsed.
- *
- * @param content The raw, un-linkified source string to parse.
- * @param config The server config specifying commentLink patterns
- */
- @observe('content', 'config')
- _contentOrConfigChanged(
- content: string | null,
- config?: LinkTextParserConfig
- ) {
- if (!config) {
- return;
- }
-
+ updated() {
+ if (!this.outputElement || !this.config) return;
+ this.outputElement.textContent = '';
// TODO(TS): mapCommentlinks always has value, remove
if (!GerritNav.mapCommentlinks) return;
- config = GerritNav.mapCommentlinks(config);
- const output = this.$.output;
- output.textContent = '';
+ const config = GerritNav.mapCommentlinks(this.config);
const parser = new GrLinkTextParser(
config,
(text: string | null, href: string | null, fragment?: DocumentFragment) =>
this._handleParseResult(text, href, fragment),
this.removeZeroWidthSpace
);
- parser.parse(content);
-
+ parser.parse(this.content);
// Ensure that external links originating from HTML commentlink configs
// open in a new tab. @see Issue 5567
// Ensure links to the same host originating from commentlink configs
// open in the same tab. When target is not set - default is _self
// @see Issue 4616
- output.querySelectorAll('a').forEach(anchor => {
+ this.outputElement.querySelectorAll('a').forEach(anchor => {
if (anchor.hostname === window.location.hostname) {
anchor.removeAttribute('target');
} else {
@@ -124,7 +117,8 @@
href: string | null,
fragment?: DocumentFragment
) {
- const output = this.$.output;
+ const output = this.outputElement;
+ if (!output) return;
if (href) {
const a = document.createElement('a');
a.setAttribute('href', href);
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_html.ts b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_html.ts
deleted file mode 100644
index 0d44bc8..0000000
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_html.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 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 {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style>
- :host {
- display: block;
- }
- :host([pre]) span {
- white-space: var(--linked-text-white-space, pre-wrap);
- word-wrap: var(--linked-text-word-wrap, break-word);
- }
- :host([disabled]) a {
- color: inherit;
- text-decoration: none;
- pointer-events: none;
- }
- a {
- color: var(--link-color);
- }
- </style>
- <span id="output"></span>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.ts b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.ts
index b2cdba1..c97c168 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.ts
@@ -85,10 +85,11 @@
window.CANONICAL_PATH = originalCanonicalPath;
});
- test('URL pattern was parsed and linked.', () => {
+ test('URL pattern was parsed and linked.', async () => {
// Regular inline link.
const url = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650';
element.content = url;
+ await flush();
const linkEl = queryAndAssert(element, '#output')
.childNodes[0] as HTMLAnchorElement;
assert.equal(linkEl.target, '_blank');
@@ -97,9 +98,10 @@
assert.equal(linkEl.textContent, url);
});
- test('Bug pattern was parsed and linked', () => {
+ test('Bug pattern was parsed and linked', async () => {
// "Issue/Bug" pattern.
element.content = 'Issue 3650';
+ await flush();
let linkEl = queryAndAssert(element, '#output')
.childNodes[0] as HTMLAnchorElement;
@@ -109,6 +111,7 @@
assert.equal(linkEl.textContent, 'Issue 3650');
element.content = 'Bug 3650';
+ await flush();
linkEl = queryAndAssert(element, '#output')
.childNodes[0] as HTMLAnchorElement;
assert.equal(linkEl.target, '_blank');
@@ -117,10 +120,10 @@
assert.equal(linkEl.textContent, 'Bug 3650');
});
- test('Pattern with same prefix as link was correctly parsed', () => {
+ test('Pattern with same prefix as link was correctly parsed', async () => {
// Pattern starts with the same prefix (`http`) as the url.
element.content = 'httpexample 3650';
-
+ await flush();
assert.equal(queryAndAssert(element, '#output').childNodes.length, 1);
const linkEl = queryAndAssert(element, '#output')
.childNodes[0] as HTMLAnchorElement;
@@ -130,12 +133,12 @@
assert.equal(linkEl.textContent, 'httpexample 3650');
});
- test('Change-Id pattern was parsed and linked', () => {
+ test('Change-Id pattern was parsed and linked', async () => {
// "Change-Id:" pattern.
const changeID = 'I11d6a37f5e9b5df0486f6c922d8836dfa780e03e';
const prefix = 'Change-Id: ';
element.content = prefix + changeID;
-
+ await flush();
const textNode = queryAndAssert(element, '#output').childNodes[0];
const linkEl = queryAndAssert(element, '#output')
.childNodes[1] as HTMLAnchorElement;
@@ -147,14 +150,14 @@
assert.equal(linkEl.textContent, changeID);
});
- test('Change-Id pattern was parsed and linked with base url', () => {
+ test('Change-Id pattern was parsed and linked with base url', async () => {
window.CANONICAL_PATH = '/r';
// "Change-Id:" pattern.
const changeID = 'I11d6a37f5e9b5df0486f6c922d8836dfa780e03e';
const prefix = 'Change-Id: ';
element.content = prefix + changeID;
-
+ await flush();
const textNode = queryAndAssert(element, '#output').childNodes[0];
const linkEl = queryAndAssert(element, '#output')
.childNodes[1] as HTMLAnchorElement;
@@ -166,8 +169,9 @@
assert.equal(linkEl.textContent, changeID);
});
- test('Multiple matches', () => {
+ test('Multiple matches', async () => {
element.content = 'Issue 3650\nIssue 3450';
+ await flush();
const linkEl1 = queryAndAssert(element, '#output')
.childNodes[0] as HTMLAnchorElement;
const linkEl2 = queryAndAssert(element, '#output')
@@ -188,7 +192,7 @@
assert.equal(linkEl2.textContent, 'Issue 3450');
});
- test('Change-Id pattern parsed before bug pattern', () => {
+ test('Change-Id pattern parsed before bug pattern', async () => {
// "Change-Id:" pattern.
const changeID = 'I11d6a37f5e9b5df0486f6c922d8836dfa780e03e';
const prefix = 'Change-Id: ';
@@ -200,7 +204,7 @@
const bugUrl = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650';
element.content = prefix + changeID + bug;
-
+ await flush();
const textNode = queryAndAssert(element, '#output').childNodes[0];
const changeLinkEl = queryAndAssert(element, '#output')
.childNodes[1] as HTMLAnchorElement;
@@ -218,8 +222,9 @@
assert.equal(bugLinkEl.textContent, 'Issue 3650');
});
- test('html field in link config', () => {
+ test('html field in link config', async () => {
element.content = 'google:do a barrel roll';
+ await flush();
const linkEl = queryAndAssert(element, '#output')
.childNodes[0] as HTMLAnchorElement;
assert.equal(
@@ -229,52 +234,58 @@
assert.equal(linkEl.textContent, 'do a barrel roll');
});
- test('removing hash from links', () => {
+ test('removing hash from links', async () => {
element.content = 'hash:foo';
+ await flush();
const linkEl = queryAndAssert(element, '#output')
.childNodes[0] as HTMLAnchorElement;
assert.isTrue(linkEl.href.endsWith('/awesomesauce'));
assert.equal(linkEl.textContent, 'foo');
});
- test('html with base url', () => {
+ test('html with base url', async () => {
window.CANONICAL_PATH = '/r';
element.content = 'test foo';
+ await flush();
const linkEl = queryAndAssert(element, '#output')
.childNodes[0] as HTMLAnchorElement;
assert.isTrue(linkEl.href.endsWith('/r/awesomesauce'));
assert.equal(linkEl.textContent, 'foo');
});
- test('a is not at start', () => {
+ test('a is not at start', async () => {
window.CANONICAL_PATH = '/r';
element.content = 'a test foo';
+ await flush();
const linkEl = queryAndAssert(element, '#output')
.childNodes[1] as HTMLAnchorElement;
assert.isTrue(linkEl.href.endsWith('/r/awesomesauce'));
assert.equal(linkEl.textContent, 'foo');
});
- test('hash html with base url', () => {
+ test('hash html with base url', async () => {
window.CANONICAL_PATH = '/r';
element.content = 'hash:foo';
+ await flush();
const linkEl = queryAndAssert(element, '#output')
.childNodes[0] as HTMLAnchorElement;
assert.isTrue(linkEl.href.endsWith('/r/awesomesauce'));
assert.equal(linkEl.textContent, 'foo');
});
- test('disabled config', () => {
+ test('disabled config', async () => {
element.content = 'foo:baz';
+ await flush();
assert.equal(queryAndAssert(element, '#output').innerHTML, 'foo:baz');
});
- test('R=email labels link correctly', () => {
+ test('R=email labels link correctly', async () => {
element.removeZeroWidthSpace = true;
element.content = 'R=\u200Btest@google.com';
+ await flush();
assert.equal(
queryAndAssert(element, '#output').textContent,
'R=test@google.com'
@@ -285,9 +296,10 @@
);
});
- test('CC=email labels link correctly', () => {
+ test('CC=email labels link correctly', async () => {
element.removeZeroWidthSpace = true;
element.content = 'CC=\u200Btest@google.com';
+ await flush();
assert.equal(
queryAndAssert(element, '#output').textContent,
'CC=test@google.com'
@@ -298,36 +310,42 @@
);
});
- test('only {http,https,mailto} protocols are linkified', () => {
+ test('only {http,https,mailto} protocols are linkified', async () => {
element.content = 'xx mailto:test@google.com yy';
+ await flush();
let links = queryAndAssert(element, '#output').querySelectorAll('a');
assert.equal(links.length, 1);
assert.equal(links[0].getAttribute('href'), 'mailto:test@google.com');
assert.equal(links[0].innerHTML, 'mailto:test@google.com');
element.content = 'xx http://google.com yy';
+ await flush();
links = queryAndAssert(element, '#output').querySelectorAll('a');
assert.equal(links.length, 1);
assert.equal(links[0].getAttribute('href'), 'http://google.com');
assert.equal(links[0].innerHTML, 'http://google.com');
element.content = 'xx https://google.com yy';
+ await flush();
links = queryAndAssert(element, '#output').querySelectorAll('a');
assert.equal(links.length, 1);
assert.equal(links[0].getAttribute('href'), 'https://google.com');
assert.equal(links[0].innerHTML, 'https://google.com');
element.content = 'xx ssh://google.com yy';
+ await flush();
links = queryAndAssert(element, '#output').querySelectorAll('a');
assert.equal(links.length, 0);
element.content = 'xx ftp://google.com yy';
+ await flush();
links = queryAndAssert(element, '#output').querySelectorAll('a');
assert.equal(links.length, 0);
});
- test('links without leading whitespace are linkified', () => {
+ test('links without leading whitespace are linkified', async () => {
element.content = 'xx abcmailto:test@google.com yy';
+ await flush();
assert.equal(
queryAndAssert(element, '#output').innerHTML.substr(0, 6),
'xx abc'
@@ -338,6 +356,7 @@
assert.equal(links[0].innerHTML, 'mailto:test@google.com');
element.content = 'xx defhttp://google.com yy';
+ await flush();
assert.equal(
queryAndAssert(element, '#output').innerHTML.substr(0, 6),
'xx def'
@@ -348,6 +367,7 @@
assert.equal(links[0].innerHTML, 'http://google.com');
element.content = 'xx qwehttps://google.com yy';
+ await flush();
assert.equal(
queryAndAssert(element, '#output').innerHTML.substr(0, 6),
'xx qwe'
@@ -359,6 +379,7 @@
// Non-latin character
element.content = 'xx абвhttps://google.com yy';
+ await flush();
assert.equal(
queryAndAssert(element, '#output').innerHTML.substr(0, 6),
'xx абв'
@@ -369,15 +390,17 @@
assert.equal(links[0].innerHTML, 'https://google.com');
element.content = 'xx ssh://google.com yy';
+ await flush();
links = queryAndAssert(element, '#output').querySelectorAll('a');
assert.equal(links.length, 0);
element.content = 'xx ftp://google.com yy';
+ await flush();
links = queryAndAssert(element, '#output').querySelectorAll('a');
assert.equal(links.length, 0);
});
- test('overlapping links', () => {
+ test('overlapping links', async () => {
element.config = {
b1: {
match: '(B:\\s*)(\\d+)',
@@ -389,7 +412,8 @@
},
};
element.content = '- B: 123, 45';
- const links = element.root!.querySelectorAll('a');
+ await flush();
+ const links = element.shadowRoot!.querySelectorAll('a');
assert.equal(links.length, 2);
assert.equal(
@@ -403,12 +427,4 @@
assert.equal(links[1].href, 'ftp://foo/45');
assert.equal(links[1].textContent, '45');
});
-
- test('_contentOrConfigChanged called with config', () => {
- const contentStub = sinon.stub(element, '_contentChanged');
- const contentConfigStub = sinon.stub(element, '_contentOrConfigChanged');
- element.content = 'some text';
- assert.isTrue(contentStub.called);
- assert.isTrue(contentConfigStub.called);
- });
});
diff --git a/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts
index 3d5a208..5f53819 100644
--- a/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts
+++ b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts
@@ -815,8 +815,11 @@
(tagName === 'INPUT' && type !== 'checkbox') ||
tagName === 'TEXTAREA' ||
// Suppress shortcuts if the key is 'enter'
- // and target is an anchor or button.
- (e.keyCode === 13 && (tagName === 'A' || tagName === 'BUTTON'))
+ // and target is an anchor or button or paper-tab.
+ (e.keyCode === 13 &&
+ (tagName === 'A' ||
+ tagName === 'BUTTON' ||
+ tagName === 'PAPER-TAB'))
) {
return true;
}
diff --git a/polygerrit-ui/app/styles/shared-styles.ts b/polygerrit-ui/app/styles/shared-styles.ts
index 287cf68..a8274cc 100644
--- a/polygerrit-ui/app/styles/shared-styles.ts
+++ b/polygerrit-ui/app/styles/shared-styles.ts
@@ -239,9 +239,13 @@
font-family: var(--header-font-family);
-webkit-font-smoothing: initial;
}
+ --paper-tab-content: {
+ margin-bottom: var(--spacing-s);
+ }
--paper-tab-content-focused: {
/* paper-tabs uses 700 here, which can look awkward */
font-weight: var(--font-weight-h3);
+ background: var(--gray-background-focus);
}
--paper-tab-content-unselected: {
/* paper-tabs uses 0.8 here, but we want to control the color directly */
@@ -249,6 +253,10 @@
color: var(--deemphasized-text-color);
}
}
+ paper-tab:focus {
+ padding-left: 0px;
+ padding-right: 0px;
+ }
iron-autogrow-textarea {
/** This is needed for firefox */
--iron-autogrow-textarea_-_white-space: pre-wrap;
diff --git a/polygerrit-ui/app/styles/themes/app-theme.ts b/polygerrit-ui/app/styles/themes/app-theme.ts
index 1996800..134003b 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 926b02d..69256b2 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/types/common.ts b/polygerrit-ui/app/types/common.ts
index 0f2608e..96c05ee 100644
--- a/polygerrit-ui/app/types/common.ts
+++ b/polygerrit-ui/app/types/common.ts
@@ -43,6 +43,7 @@
ActionNameToActionInfoMap,
ApprovalInfo,
AuthInfo,
+ AvatarInfo,
BasePatchSetNum,
BranchName,
BrandType,
@@ -124,6 +125,7 @@
ActionNameToActionInfoMap,
ApprovalInfo,
AuthInfo,
+ AvatarInfo,
BasePatchSetNum,
BranchName,
BrandType,
diff --git a/polygerrit-ui/app/types/diff.ts b/polygerrit-ui/app/types/diff.ts
index 16338335..223f290 100644
--- a/polygerrit-ui/app/types/diff.ts
+++ b/polygerrit-ui/app/types/diff.ts
@@ -27,6 +27,7 @@
DiffFileMetaInfo as DiffFileMetaInfoApi,
DiffInfo as DiffInfoApi,
DiffIntralineInfo,
+ DiffResponsiveMode,
DiffPreferencesInfo as DiffPreferenceInfoApi,
IgnoreWhitespaceType,
MarkLength,
@@ -37,6 +38,7 @@
export {
ChangeType,
DiffIntralineInfo,
+ DiffResponsiveMode,
IgnoreWhitespaceType,
MarkLength,
MoveDetails,
diff --git a/polygerrit-ui/app/utils/async-util_test.js b/polygerrit-ui/app/utils/async-util_test.js
deleted file mode 100644
index df29e97..0000000
--- a/polygerrit-ui/app/utils/async-util_test.js
+++ /dev/null
@@ -1,46 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 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 {asyncForeach} from './async-util.js';
-
-suite('async-util tests', () => {
- test('loops over each item', () => {
- const fn = sinon.stub().returns(Promise.resolve());
- return asyncForeach([1, 2, 3], fn)
- .then(() => {
- assert.isTrue(fn.calledThrice);
- assert.equal(fn.getCall(0).args[0], 1);
- assert.equal(fn.getCall(1).args[0], 2);
- assert.equal(fn.getCall(2).args[0], 3);
- });
- });
-
- test('halts on stop condition', () => {
- const stub = sinon.stub();
- const fn = (e, stop) => {
- stub(e);
- stop();
- return Promise.resolve();
- };
- return asyncForeach([1, 2, 3], fn)
- .then(() => {
- assert.isTrue(stub.calledOnce);
- assert.equal(stub.lastCall.args[0], 1);
- });
- });
-});
diff --git a/polygerrit-ui/app/utils/async-util_test.ts b/polygerrit-ui/app/utils/async-util_test.ts
new file mode 100644
index 0000000..5c8f610
--- /dev/null
+++ b/polygerrit-ui/app/utils/async-util_test.ts
@@ -0,0 +1,46 @@
+/**
+ * @license
+ * Copyright (C) 2017 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';
+import {asyncForeach} from './async-util';
+
+suite('async-util tests', () => {
+ test('loops over each item', async () => {
+ const fn = sinon.stub().resolves();
+
+ await asyncForeach([1, 2, 3], fn);
+
+ assert.isTrue(fn.calledThrice);
+ assert.equal(fn.firstCall.firstArg, 1);
+ assert.equal(fn.secondCall.firstArg, 2);
+ assert.equal(fn.thirdCall.firstArg, 3);
+ });
+
+ test('halts on stop condition', async () => {
+ const stub = sinon.stub();
+ const fn = (item: number, stopCallback: () => void) => {
+ stub(item);
+ stopCallback();
+ return Promise.resolve();
+ };
+
+ await asyncForeach([1, 2, 3], fn);
+
+ assert.isTrue(stub.calledOnce);
+ assert.equal(stub.lastCall.firstArg, 1);
+ });
+});
diff --git a/polygerrit-ui/app/utils/date-util_test.js b/polygerrit-ui/app/utils/date-util_test.js
deleted file mode 100644
index 96d5bc1..0000000
--- a/polygerrit-ui/app/utils/date-util_test.js
+++ /dev/null
@@ -1,144 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 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 {isValidDate, parseDate, fromNow, isWithinDay, isWithinHalfYear, formatDate, wasYesterday} from './date-util.js';
-
-suite('date-util tests', () => {
- suite('parseDate', () => {
- test('parseDate server date', () => {
- const parsed = parseDate('2015-09-15 20:34:00.000000000');
- assert.equal('2015-09-15T20:34:00.000Z', parsed.toISOString());
- });
- });
-
- suite('isValidDate', () => {
- test('date is valid', () => {
- assert.isTrue(isValidDate(new Date()));
- });
- test('broken date is invalid', () => {
- assert.isFalse(isValidDate(new Date('xxx')));
- });
- });
-
- suite('fromNow', () => {
- test('test all variants', () => {
- const fakeNow = new Date('May 08 2020 12:00:00');
- sinon.useFakeTimers(fakeNow.getTime());
- assert.equal('just now', fromNow(new Date('May 08 2020 11:59:30')));
- assert.equal('1 minute ago', fromNow(new Date('May 08 2020 11:59:00')));
- assert.equal('5 minutes ago', fromNow(new Date('May 08 2020 11:55:00')));
- assert.equal('1 hour ago', fromNow(new Date('May 08 2020 11:00:00')));
- assert.equal(
- '1 hour 5 min ago', fromNow(new Date('May 08 2020 10:55:00')));
- assert.equal('3 hours ago', fromNow(new Date('May 08 2020 9:00:00')));
- assert.equal('1 day ago', fromNow(new Date('May 07 2020 12:00:00')));
- assert.equal('1 day 2 hr ago', fromNow(new Date('May 07 2020 10:00:00')));
- assert.equal('3 days ago', fromNow(new Date('May 05 2020 12:00:00')));
- assert.equal('1 month ago', fromNow(new Date('Apr 05 2020 12:00:00')));
- assert.equal('2 months ago', fromNow(new Date('Mar 05 2020 12:00:00')));
- assert.equal('1 year ago', fromNow(new Date('May 05 2019 12:00:00')));
- assert.equal('10 years ago', fromNow(new Date('May 05 2010 12:00:00')));
- });
- test('rounding error', () => {
- const fakeNow = new Date('May 08 2020 12:00:00');
- sinon.useFakeTimers(fakeNow.getTime());
- assert.equal('2 hours ago', fromNow(new Date('May 08 2020 9:30:00')));
- });
- });
-
- suite('isWithinDay', () => {
- test('basics works', () => {
- assert.isTrue(isWithinDay(new Date('May 08 2020 12:00:00'),
- new Date('May 08 2020 02:00:00')));
- assert.isFalse(isWithinDay(new Date('May 08 2020 12:00:00'),
- new Date('May 07 2020 12:00:00')));
- });
- });
-
- suite('wasYesterday', () => {
- test('less 24 hours', () => {
- assert.isFalse(wasYesterday(new Date('May 08 2020 12:00:00'),
- new Date('May 08 2020 02:00:00')));
- assert.isTrue(wasYesterday(new Date('May 08 2020 12:00:00'),
- new Date('May 07 2020 12:00:00')));
- });
- test('more 24 hours', () => {
- assert.isTrue(wasYesterday(new Date('May 08 2020 12:00:00'),
- new Date('May 07 2020 2:00:00')));
- assert.isFalse(wasYesterday(new Date('May 08 2020 12:00:00'),
- new Date('May 06 2020 14:00:00')));
- });
- });
-
- suite('isWithinHalfYear', () => {
- test('basics works', () => {
- assert.isTrue(isWithinHalfYear(new Date('May 08 2020 12:00:00'),
- new Date('Feb 08 2020 12:00:00')));
- assert.isFalse(isWithinHalfYear(new Date('May 08 2020 12:00:00'),
- new Date('Nov 07 2019 12:00:00')));
- });
- });
-
- suite('formatDate', () => {
- test('works for standard format', () => {
- const stdFormat = 'MMM DD, YYYY';
- assert.equal('May 08, 2020',
- formatDate(new Date('May 08 2020 12:00:00'), stdFormat));
- assert.equal('Feb 28, 2020',
- formatDate(new Date('Feb 28 2020 12:00:00'), stdFormat));
-
- const time24Format = 'HH:mm:ss';
- assert.equal('Feb 28, 2020 12:01:12',
- formatDate(new Date('Feb 28 2020 12:01:12'), stdFormat + ' '
- + time24Format));
- });
- test('works for euro format', () => {
- const euroFormat = 'DD.MM.YYYY';
- assert.equal('01.12.2019',
- formatDate(new Date('Dec 01 2019 12:00:00'), euroFormat));
- assert.equal('20.01.2002',
- formatDate(new Date('Jan 20 2002 12:00:00'), euroFormat));
-
- const time24Format = 'HH:mm:ss';
- assert.equal('28.02.2020 00:01:12',
- formatDate(new Date('Feb 28 2020 00:01:12'), euroFormat + ' '
- + time24Format));
- });
- test('works for iso format', () => {
- const isoFormat = 'YYYY-MM-DD';
- assert.equal('2015-01-01',
- formatDate(new Date('Jan 01 2015 12:00:00'), isoFormat));
- assert.equal('2013-07-03',
- formatDate(new Date('Jul 03 2013 12:00:00'), isoFormat));
-
- const timeFormat = 'h:mm:ss A';
- assert.equal('2013-07-03 5:00:00 AM',
- formatDate(new Date('Jul 03 2013 05:00:00'), isoFormat + ' '
- + timeFormat));
- assert.equal('2013-07-03 5:00:00 PM',
- formatDate(new Date('Jul 03 2013 17:00:00'), isoFormat + ' '
- + timeFormat));
- });
- test('h:mm:ss A shows correctly midnight and midday', () => {
- const timeFormat = 'h:mm A';
- assert.equal('12:14 PM',
- formatDate(new Date('Jul 03 2013 12:14:00'), timeFormat));
- assert.equal('12:15 AM',
- formatDate(new Date('Jul 03 2013 00:15:00'), timeFormat));
- });
- });
-});
diff --git a/polygerrit-ui/app/utils/date-util_test.ts b/polygerrit-ui/app/utils/date-util_test.ts
new file mode 100644
index 0000000..f17ced3
--- /dev/null
+++ b/polygerrit-ui/app/utils/date-util_test.ts
@@ -0,0 +1,219 @@
+/**
+ * @license
+ * Copyright (C) 2020 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 {Timestamp} from '../types/common';
+import '../test/common-test-setup-karma';
+import {
+ isValidDate,
+ parseDate,
+ fromNow,
+ isWithinDay,
+ isWithinHalfYear,
+ formatDate,
+ wasYesterday,
+} from './date-util';
+
+suite('date-util tests', () => {
+ suite('parseDate', () => {
+ test('parseDate server date', () => {
+ const parsed = parseDate('2015-09-15 20:34:00.000000000' as Timestamp);
+ assert.equal('2015-09-15T20:34:00.000Z', parsed.toISOString());
+ });
+ });
+
+ suite('isValidDate', () => {
+ test('date is valid', () => {
+ assert.isTrue(isValidDate(new Date()));
+ });
+ test('broken date is invalid', () => {
+ assert.isFalse(isValidDate(new Date('xxx')));
+ });
+ });
+
+ suite('fromNow', () => {
+ test('test all variants', () => {
+ const fakeNow = new Date('May 08 2020 12:00:00');
+ sinon.useFakeTimers(fakeNow.getTime());
+ assert.equal('just now', fromNow(new Date('May 08 2020 11:59:30')));
+ assert.equal('1 minute ago', fromNow(new Date('May 08 2020 11:59:00')));
+ assert.equal('5 minutes ago', fromNow(new Date('May 08 2020 11:55:00')));
+ assert.equal('1 hour ago', fromNow(new Date('May 08 2020 11:00:00')));
+ assert.equal(
+ '1 hour 5 min ago',
+ fromNow(new Date('May 08 2020 10:55:00'))
+ );
+ assert.equal('3 hours ago', fromNow(new Date('May 08 2020 9:00:00')));
+ assert.equal('1 day ago', fromNow(new Date('May 07 2020 12:00:00')));
+ assert.equal('1 day 2 hr ago', fromNow(new Date('May 07 2020 10:00:00')));
+ assert.equal('3 days ago', fromNow(new Date('May 05 2020 12:00:00')));
+ assert.equal('1 month ago', fromNow(new Date('Apr 05 2020 12:00:00')));
+ assert.equal('2 months ago', fromNow(new Date('Mar 05 2020 12:00:00')));
+ assert.equal('1 year ago', fromNow(new Date('May 05 2019 12:00:00')));
+ assert.equal('10 years ago', fromNow(new Date('May 05 2010 12:00:00')));
+ });
+ test('rounding error', () => {
+ const fakeNow = new Date('May 08 2020 12:00:00');
+ sinon.useFakeTimers(fakeNow.getTime());
+ assert.equal('2 hours ago', fromNow(new Date('May 08 2020 9:30:00')));
+ });
+ });
+
+ suite('isWithinDay', () => {
+ test('basics works', () => {
+ assert.isTrue(
+ isWithinDay(
+ new Date('May 08 2020 12:00:00'),
+ new Date('May 08 2020 02:00:00')
+ )
+ );
+ assert.isFalse(
+ isWithinDay(
+ new Date('May 08 2020 12:00:00'),
+ new Date('May 07 2020 12:00:00')
+ )
+ );
+ });
+ });
+
+ suite('wasYesterday', () => {
+ test('less 24 hours', () => {
+ assert.isFalse(
+ wasYesterday(
+ new Date('May 08 2020 12:00:00'),
+ new Date('May 08 2020 02:00:00')
+ )
+ );
+ assert.isTrue(
+ wasYesterday(
+ new Date('May 08 2020 12:00:00'),
+ new Date('May 07 2020 12:00:00')
+ )
+ );
+ });
+ test('more 24 hours', () => {
+ assert.isTrue(
+ wasYesterday(
+ new Date('May 08 2020 12:00:00'),
+ new Date('May 07 2020 2:00:00')
+ )
+ );
+ assert.isFalse(
+ wasYesterday(
+ new Date('May 08 2020 12:00:00'),
+ new Date('May 06 2020 14:00:00')
+ )
+ );
+ });
+ });
+
+ suite('isWithinHalfYear', () => {
+ test('basics works', () => {
+ assert.isTrue(
+ isWithinHalfYear(
+ new Date('May 08 2020 12:00:00'),
+ new Date('Feb 08 2020 12:00:00')
+ )
+ );
+ assert.isFalse(
+ isWithinHalfYear(
+ new Date('May 08 2020 12:00:00'),
+ new Date('Nov 07 2019 12:00:00')
+ )
+ );
+ });
+ });
+
+ suite('formatDate', () => {
+ test('works for standard format', () => {
+ const stdFormat = 'MMM DD, YYYY';
+ assert.equal(
+ 'May 08, 2020',
+ formatDate(new Date('May 08 2020 12:00:00'), stdFormat)
+ );
+ assert.equal(
+ 'Feb 28, 2020',
+ formatDate(new Date('Feb 28 2020 12:00:00'), stdFormat)
+ );
+
+ const time24Format = 'HH:mm:ss';
+ assert.equal(
+ 'Feb 28, 2020 12:01:12',
+ formatDate(
+ new Date('Feb 28 2020 12:01:12'),
+ stdFormat + ' ' + time24Format
+ )
+ );
+ });
+ test('works for euro format', () => {
+ const euroFormat = 'DD.MM.YYYY';
+ assert.equal(
+ '01.12.2019',
+ formatDate(new Date('Dec 01 2019 12:00:00'), euroFormat)
+ );
+ assert.equal(
+ '20.01.2002',
+ formatDate(new Date('Jan 20 2002 12:00:00'), euroFormat)
+ );
+
+ const time24Format = 'HH:mm:ss';
+ assert.equal(
+ '28.02.2020 00:01:12',
+ formatDate(
+ new Date('Feb 28 2020 00:01:12'),
+ euroFormat + ' ' + time24Format
+ )
+ );
+ });
+ test('works for iso format', () => {
+ const isoFormat = 'YYYY-MM-DD';
+ assert.equal(
+ '2015-01-01',
+ formatDate(new Date('Jan 01 2015 12:00:00'), isoFormat)
+ );
+ assert.equal(
+ '2013-07-03',
+ formatDate(new Date('Jul 03 2013 12:00:00'), isoFormat)
+ );
+
+ const timeFormat = 'h:mm:ss A';
+ assert.equal(
+ '2013-07-03 5:00:00 AM',
+ formatDate(
+ new Date('Jul 03 2013 05:00:00'),
+ isoFormat + ' ' + timeFormat
+ )
+ );
+ assert.equal(
+ '2013-07-03 5:00:00 PM',
+ formatDate(
+ new Date('Jul 03 2013 17:00:00'),
+ isoFormat + ' ' + timeFormat
+ )
+ );
+ });
+ test('h:mm:ss A shows correctly midnight and midday', () => {
+ const timeFormat = 'h:mm A';
+ assert.equal(
+ '12:14 PM',
+ formatDate(new Date('Jul 03 2013 12:14:00'), timeFormat)
+ );
+ assert.equal(
+ '12:15 AM',
+ formatDate(new Date('Jul 03 2013 00:15:00'), timeFormat)
+ );
+ });
+ });
+});
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), '');
+ });
+});
diff --git a/polygerrit-ui/app/utils/path-list-util_test.js b/polygerrit-ui/app/utils/path-list-util_test.js
deleted file mode 100644
index 4d06344..0000000
--- a/polygerrit-ui/app/utils/path-list-util_test.js
+++ /dev/null
@@ -1,161 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 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 {SpecialFilePath} from '../constants/constants.js';
-import {
- addUnmodifiedFiles,
- computeDisplayPath,
- isMagicPath,
- specialFilePathCompare, truncatePath,
-} from './path-list-util.js';
-
-suite('path-list-utl tests', () => {
- test('special sort', () => {
- const testFiles = [
- '/a.h',
- '/MERGE_LIST',
- '/a.cpp',
- '/COMMIT_MSG',
- '/asdasd',
- '/mrPeanutbutter.py',
- ];
- assert.deepEqual(
- testFiles.sort(specialFilePathCompare),
- [
- '/COMMIT_MSG',
- '/MERGE_LIST',
- '/a.h',
- '/a.cpp',
- '/asdasd',
- '/mrPeanutbutter.py',
- ]);
- });
-
- test('special file path sorting', () => {
- assert.deepEqual(
- ['.b', '/COMMIT_MSG', '.a', 'file'].sort(
- specialFilePathCompare),
- ['/COMMIT_MSG', '.a', '.b', 'file']);
-
- assert.deepEqual(
- ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.h'].sort(
- specialFilePathCompare),
- ['/COMMIT_MSG', '.b', 'foo/bar/baz.h', 'foo/bar/baz.cc']);
-
- assert.deepEqual(
- ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.hpp'].sort(
- specialFilePathCompare),
- ['/COMMIT_MSG', '.b', 'foo/bar/baz.hpp', 'foo/bar/baz.cc']);
-
- assert.deepEqual(
- ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.hxx'].sort(
- specialFilePathCompare),
- ['/COMMIT_MSG', '.b', 'foo/bar/baz.hxx', 'foo/bar/baz.cc']);
-
- assert.deepEqual(
- ['foo/bar.h', 'foo/bar.hxx', 'foo/bar.hpp'].sort(
- specialFilePathCompare),
- ['foo/bar.h', 'foo/bar.hpp', 'foo/bar.hxx']);
-
- // Regression test for Issue 4448.
- assert.deepEqual(
- [
- 'minidump/minidump_memory_writer.cc',
- 'minidump/minidump_memory_writer.h',
- 'minidump/minidump_thread_writer.cc',
- 'minidump/minidump_thread_writer.h',
- ].sort(specialFilePathCompare),
- [
- 'minidump/minidump_memory_writer.h',
- 'minidump/minidump_memory_writer.cc',
- 'minidump/minidump_thread_writer.h',
- 'minidump/minidump_thread_writer.cc',
- ]);
-
- // Regression test for Issue 4545.
- assert.deepEqual(
- [
- 'task_test.go',
- 'task.go',
- ].sort(specialFilePathCompare),
- [
- 'task.go',
- 'task_test.go',
- ]);
- });
-
- test('file display name', () => {
- assert.equal(computeDisplayPath('/foo/bar/baz'), '/foo/bar/baz');
- assert.equal(computeDisplayPath('/foobarbaz'), '/foobarbaz');
- assert.equal(computeDisplayPath('/COMMIT_MSG'), 'Commit message');
- assert.equal(computeDisplayPath('/MERGE_LIST'), 'Merge list');
- });
-
- test('isMagicPath', () => {
- assert.isFalse(isMagicPath(undefined));
- assert.isFalse(isMagicPath('/foo.cc'));
- assert.isTrue(isMagicPath('/COMMIT_MSG'));
- assert.isTrue(isMagicPath('/MERGE_LIST'));
- });
-
- test('patchset level comments are hidden', () => {
- const commentedPaths = {
- [SpecialFilePath.PATCHSET_LEVEL_COMMENTS]: true,
- 'file1.txt': true,
- };
-
- const files = {'file2.txt': {status: 'M'}};
- addUnmodifiedFiles(files, commentedPaths);
- assert.equal(files['file1.txt'].status, 'U');
- assert.equal(files['file2.txt'].status, 'M');
- assert.isFalse(files.hasOwnProperty(
- SpecialFilePath.PATCHSET_LEVEL_COMMENTS));
- });
-
- test('truncatePath with long path should add ellipsis', () => {
- let path = 'level1/level2/level3/level4/file.js';
- let shortenedPath = truncatePath(path);
- // The expected path is truncated with an ellipsis.
- const expectedPath = '\u2026/file.js';
- assert.equal(shortenedPath, expectedPath);
-
- path = 'level2/file.js';
- shortenedPath = truncatePath(path);
- assert.equal(shortenedPath, expectedPath);
- });
-
- test('truncatePath with opt_threshold', () => {
- let path = 'level1/level2/level3/level4/file.js';
- let shortenedPath = truncatePath(path, 2);
- // The expected path is truncated with an ellipsis.
- const expectedPath = '\u2026/level4/file.js';
- assert.equal(shortenedPath, expectedPath);
-
- path = 'level2/file.js';
- shortenedPath = truncatePath(path, 2);
- assert.equal(shortenedPath, path);
- });
-
- test('truncatePath with short path should not add ellipsis', () => {
- const path = 'file.js';
- const expectedPath = 'file.js';
- const shortenedPath = truncatePath(path);
- assert.equal(shortenedPath, expectedPath);
- });
-});
-
diff --git a/polygerrit-ui/app/utils/path-list-util_test.ts b/polygerrit-ui/app/utils/path-list-util_test.ts
new file mode 100644
index 0000000..79b5f09
--- /dev/null
+++ b/polygerrit-ui/app/utils/path-list-util_test.ts
@@ -0,0 +1,170 @@
+/**
+ * @license
+ * Copyright (C) 2016 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';
+import {FileInfoStatus, SpecialFilePath} from '../constants/constants';
+import {
+ addUnmodifiedFiles,
+ computeDisplayPath,
+ isMagicPath,
+ specialFilePathCompare,
+ truncatePath,
+} from './path-list-util';
+import {FileInfo} from '../api/rest-api';
+import {hasOwnProperty} from './common-util';
+
+suite('path-list-utl tests', () => {
+ test('special sort', () => {
+ const testFiles = [
+ '/a.h',
+ '/MERGE_LIST',
+ '/a.cpp',
+ '/COMMIT_MSG',
+ '/asdasd',
+ '/mrPeanutbutter.py',
+ ];
+ assert.deepEqual(testFiles.sort(specialFilePathCompare), [
+ '/COMMIT_MSG',
+ '/MERGE_LIST',
+ '/a.h',
+ '/a.cpp',
+ '/asdasd',
+ '/mrPeanutbutter.py',
+ ]);
+ });
+
+ test('special file path sorting', () => {
+ assert.deepEqual(
+ ['.b', '/COMMIT_MSG', '.a', 'file'].sort(specialFilePathCompare),
+ ['/COMMIT_MSG', '.a', '.b', 'file']
+ );
+
+ assert.deepEqual(
+ ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.h'].sort(
+ specialFilePathCompare
+ ),
+ ['/COMMIT_MSG', '.b', 'foo/bar/baz.h', 'foo/bar/baz.cc']
+ );
+
+ assert.deepEqual(
+ ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.hpp'].sort(
+ specialFilePathCompare
+ ),
+ ['/COMMIT_MSG', '.b', 'foo/bar/baz.hpp', 'foo/bar/baz.cc']
+ );
+
+ assert.deepEqual(
+ ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.hxx'].sort(
+ specialFilePathCompare
+ ),
+ ['/COMMIT_MSG', '.b', 'foo/bar/baz.hxx', 'foo/bar/baz.cc']
+ );
+
+ assert.deepEqual(
+ ['foo/bar.h', 'foo/bar.hxx', 'foo/bar.hpp'].sort(specialFilePathCompare),
+ ['foo/bar.h', 'foo/bar.hpp', 'foo/bar.hxx']
+ );
+
+ // Regression test for Issue 4448.
+ assert.deepEqual(
+ [
+ 'minidump/minidump_memory_writer.cc',
+ 'minidump/minidump_memory_writer.h',
+ 'minidump/minidump_thread_writer.cc',
+ 'minidump/minidump_thread_writer.h',
+ ].sort(specialFilePathCompare),
+ [
+ 'minidump/minidump_memory_writer.h',
+ 'minidump/minidump_memory_writer.cc',
+ 'minidump/minidump_thread_writer.h',
+ 'minidump/minidump_thread_writer.cc',
+ ]
+ );
+
+ // Regression test for Issue 4545.
+ assert.deepEqual(['task_test.go', 'task.go'].sort(specialFilePathCompare), [
+ 'task.go',
+ 'task_test.go',
+ ]);
+ });
+
+ test('file display name', () => {
+ assert.equal(computeDisplayPath('/foo/bar/baz'), '/foo/bar/baz');
+ assert.equal(computeDisplayPath('/foobarbaz'), '/foobarbaz');
+ assert.equal(computeDisplayPath('/COMMIT_MSG'), 'Commit message');
+ assert.equal(computeDisplayPath('/MERGE_LIST'), 'Merge list');
+ });
+
+ test('isMagicPath', () => {
+ assert.isFalse(isMagicPath(undefined));
+ assert.isFalse(isMagicPath('/foo.cc'));
+ assert.isTrue(isMagicPath('/COMMIT_MSG'));
+ assert.isTrue(isMagicPath('/MERGE_LIST'));
+ });
+
+ test('patchset level comments are hidden', () => {
+ const commentedPaths = {
+ [SpecialFilePath.PATCHSET_LEVEL_COMMENTS]: true,
+ 'file1.txt': true,
+ };
+
+ const files: {[filename: string]: FileInfo} = {
+ 'file2.txt': {
+ status: FileInfoStatus.REWRITTEN,
+ size_delta: 10,
+ size: 10,
+ },
+ };
+ addUnmodifiedFiles(files, commentedPaths);
+ assert.equal(files['file1.txt'].status, FileInfoStatus.UNMODIFIED);
+ assert.equal(files['file2.txt'].status, FileInfoStatus.REWRITTEN);
+ assert.isFalse(
+ hasOwnProperty(files, SpecialFilePath.PATCHSET_LEVEL_COMMENTS)
+ );
+ });
+
+ test('truncatePath with long path should add ellipsis', () => {
+ let path = 'level1/level2/level3/level4/file.js';
+ let shortenedPath = truncatePath(path);
+ // The expected path is truncated with an ellipsis.
+ const expectedPath = '\u2026/file.js';
+ assert.equal(shortenedPath, expectedPath);
+
+ path = 'level2/file.js';
+ shortenedPath = truncatePath(path);
+ assert.equal(shortenedPath, expectedPath);
+ });
+
+ test('truncatePath with opt_threshold', () => {
+ let path = 'level1/level2/level3/level4/file.js';
+ let shortenedPath = truncatePath(path, 2);
+ // The expected path is truncated with an ellipsis.
+ const expectedPath = '\u2026/level4/file.js';
+ assert.equal(shortenedPath, expectedPath);
+
+ path = 'level2/file.js';
+ shortenedPath = truncatePath(path, 2);
+ assert.equal(shortenedPath, path);
+ });
+
+ test('truncatePath with short path should not add ellipsis', () => {
+ const path = 'file.js';
+ const expectedPath = 'file.js';
+ const shortenedPath = truncatePath(path);
+ assert.equal(shortenedPath, expectedPath);
+ });
+});
diff --git a/polygerrit-ui/app/utils/url-util_test.js b/polygerrit-ui/app/utils/url-util_test.ts
similarity index 66%
rename from polygerrit-ui/app/utils/url-util_test.js
rename to polygerrit-ui/app/utils/url-util_test.ts
index 5cd4bb4..63dc81d 100644
--- a/polygerrit-ui/app/utils/url-util_test.js
+++ b/polygerrit-ui/app/utils/url-util_test.ts
@@ -15,7 +15,9 @@
* limitations under the License.
*/
-import '../test/common-test-setup-karma.js';
+import {ServerInfo} from '../api/rest-api';
+import '../test/common-test-setup-karma';
+import {createGerritInfo, createServerInfo} from '../test/test-data-generators';
import {
getBaseUrl,
getDocsBaseUrl,
@@ -25,11 +27,13 @@
toPath,
toPathname,
toSearchParams,
-} from './url-util.js';
+} from './url-util';
+import {appContext} from '../services/app-context';
+import {stubRestApi} from '../test/test-utils';
suite('url-util tests', () => {
suite('getBaseUrl tests', () => {
- let originalCanonicalPath;
+ let originalCanonicalPath: string | undefined;
suiteSetup(() => {
originalCanonicalPath = window.CANONICAL_PATH;
@@ -51,43 +55,50 @@
});
test('null config', async () => {
- const mockRestApi = {
- probePath: sinon.stub().returns(Promise.resolve(true)),
- };
- const docsBaseUrl = await getDocsBaseUrl(null, mockRestApi);
- assert.isTrue(
- mockRestApi.probePath.calledWith('/Documentation/index.html'));
+ const probePathMock = stubRestApi('probePath').resolves(true);
+ const docsBaseUrl = await getDocsBaseUrl(
+ undefined,
+ appContext.restApiService
+ );
+ assert.isTrue(probePathMock.calledWith('/Documentation/index.html'));
assert.equal(docsBaseUrl, '/Documentation');
});
test('no doc config', async () => {
- const mockRestApi = {
- probePath: sinon.stub().returns(Promise.resolve(true)),
+ const probePathMock = stubRestApi('probePath').resolves(true);
+ const config: ServerInfo = {
+ ...createServerInfo(),
+ gerrit: createGerritInfo(),
};
- const config = {gerrit: {}};
- const docsBaseUrl = await getDocsBaseUrl(config, mockRestApi);
- assert.isTrue(
- mockRestApi.probePath.calledWith('/Documentation/index.html'));
+ const docsBaseUrl = await getDocsBaseUrl(
+ config,
+ appContext.restApiService
+ );
+ assert.isTrue(probePathMock.calledWith('/Documentation/index.html'));
assert.equal(docsBaseUrl, '/Documentation');
});
test('has doc config', async () => {
- const mockRestApi = {
- probePath: sinon.stub().returns(Promise.resolve(true)),
+ const probePathMock = stubRestApi('probePath').resolves(true);
+ const config: ServerInfo = {
+ ...createServerInfo(),
+ gerrit: {...createGerritInfo(), doc_url: 'foobar'},
};
- const config = {gerrit: {doc_url: 'foobar'}};
- const docsBaseUrl = await getDocsBaseUrl(config, mockRestApi);
- assert.isFalse(mockRestApi.probePath.called);
+ const docsBaseUrl = await getDocsBaseUrl(
+ config,
+ appContext.restApiService
+ );
+ assert.isFalse(probePathMock.called);
assert.equal(docsBaseUrl, 'foobar');
});
test('no probe', async () => {
- const mockRestApi = {
- probePath: sinon.stub().returns(Promise.resolve(false)),
- };
- const docsBaseUrl = await getDocsBaseUrl(null, mockRestApi);
- assert.isTrue(
- mockRestApi.probePath.calledWith('/Documentation/index.html'));
+ const probePathMock = stubRestApi('probePath').resolves(false);
+ const docsBaseUrl = await getDocsBaseUrl(
+ undefined,
+ appContext.restApiService
+ );
+ assert.isTrue(probePathMock.calledWith('/Documentation/index.html'));
assert.isNotOk(docsBaseUrl);
});
});
@@ -144,7 +155,9 @@
assert.equal(toPath('asdf', params), 'asdf');
params.set('qwer', 'zxcv');
assert.equal(toPath('asdf', params), 'asdf?qwer=zxcv');
- assert.equal(toPath(toPathname('asdf?qwer=zxcv'),
- toSearchParams('asdf?qwer=zxcv')), 'asdf?qwer=zxcv');
+ assert.equal(
+ toPath(toPathname('asdf?qwer=zxcv'), toSearchParams('asdf?qwer=zxcv')),
+ 'asdf?qwer=zxcv'
+ );
});
});
diff --git a/proto/cache.proto b/proto/cache.proto
index 781538a..aa04555 100644
--- a/proto/cache.proto
+++ b/proto/cache.proto
@@ -78,7 +78,7 @@
// Instead, we just take the tedious yet simple approach of having a "has_foo"
// field for each nullable field "foo", indicating whether or not foo is null.
//
-// Next ID: 27
+// Next ID: 28
message ChangeNotesStateProto {
// Effectively required, even though the corresponding ChangeNotesState field
// is optional, since the field is only absent when NoteDb is disabled, in
@@ -226,6 +226,8 @@
// Epoch millis.
int64 merged_on_millis = 25;
bool has_merged_on = 26;
+
+ repeated SubmitRequirementResultProto submit_requirement_result = 27;
}
// Serialized form of com.google.gerrit.server.query.change.ConflictKey
@@ -480,6 +482,28 @@
bool allow_override_in_child_projects = 6;
}
+// Serialized form of com.google.gerrit.entities.SubmitRequirementResult.
+// Next ID: 6
+message SubmitRequirementResultProto {
+ SubmitRequirementProto submit_requirement = 1;
+ SubmitRequirementExpressionResultProto applicability_expression_result = 2;
+ SubmitRequirementExpressionResultProto submittability_expression_result = 3;
+ SubmitRequirementExpressionResultProto override_expression_result = 4;
+
+ // Patchset commit ID at which the submit requirements are evaluated.
+ bytes commit = 5;
+}
+
+// Serialized form of com.google.gerrit.entities.SubmitRequirementExpressionResult.
+// Next ID: 6
+message SubmitRequirementExpressionResultProto {
+ string expression = 1;
+ string status = 2; // enum as string
+ string error_message = 3;
+ repeated string passing_atoms = 4;
+ repeated string failing_atoms = 5;
+}
+
// Serialized form of com.google.gerrit.server.project.ConfiguredMimeTypes.
// Next ID: 4
message ConfiguredMimeTypeProto {