Merge "Migrate gr-change-star to lit element"
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index 0318cd7..d8b6250 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -113,7 +113,7 @@
* `receivecommits/ps_revision_missing`: errors due to patch set revision missing
* `receivecommits/push_count`: number of pushes
** `kind`:
- The push kind (direct vs. magic).
+ The push kind (magic, direct or direct_submit).
** `project`:
The name of the project for which the push is done.
** `type`:
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index cc8d813..6e6b9d7 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -565,6 +565,11 @@
cherry-picked locally using the git cherry-pick command and then
pushed to Gerrit.
+[[pure-revert]]
+is:pure-revert::
++
+True if the change is a pure revert.
+
[[status]]
status:open, status:pending, status:new::
+
@@ -772,6 +777,22 @@
+
Matches changes with label voted with any score.
+`label:Code-Review=+1,count=2`::
++
+Matches changes with exactly two +1 votes to the code-review label. The {MAX,
+MIN, ANY} votes can also be used, for example `label:Code-Review=MAX,count=2` is
+equivalent to `label:Code-Review=2,count=2` (if 2 is the maximum positive vote
+for the code review label). The maximum supported value for `count` is 5.
+`count=0` is not allowed and the query request will fail with `400 Bad Request`.
+
+`label:Code-Review=+1,count>=2`::
++
+Matches changes having two or more +1 votes to the code-review label. Can also
+be used with the {MAX, MIN, ANY} label votes. All operators `>`, `>=`, `<`, `<=`
+are supported.
+Note that a query like `label:Code-Review=+1,count<x` will not match with
+changes having zero +1 votes to this label.
+
`label:Non-Author-Code-Review=need`::
+
Matches changes where the submit rules indicate that a label named
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 66e7d80..f4c7a92 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -747,14 +747,19 @@
return;
}
- if (!magicCommands.isEmpty()) {
- metrics.pushCount.increment("magic", project.getName(), getUpdateType(magicCommands));
- }
- if (!regularCommands.isEmpty()) {
- metrics.pushCount.increment("direct", project.getName(), getUpdateType(regularCommands));
- }
-
try {
+ if (!magicCommands.isEmpty()) {
+ parseMagicBranch(Iterables.getLast(magicCommands));
+ // Using the submit option submits the created change(s) immediately without checking labels
+ // nor submit rules. Hence we shouldn't record such pushes as "magic" which implies that
+ // code review is being done.
+ String pushKind = magicBranch != null && magicBranch.submit ? "direct_submit" : "magic";
+ metrics.pushCount.increment(pushKind, project.getName(), getUpdateType(magicCommands));
+ }
+ if (!regularCommands.isEmpty()) {
+ metrics.pushCount.increment("direct", project.getName(), getUpdateType(regularCommands));
+ }
+
if (!regularCommands.isEmpty()) {
handleRegularCommands(regularCommands, progress);
return;
@@ -763,7 +768,6 @@
boolean first = true;
for (ReceiveCommand cmd : magicCommands) {
if (first) {
- parseMagicBranch(cmd);
first = false;
} else {
reject(cmd, "duplicate request");
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveConstants.java b/java/com/google/gerrit/server/git/receive/ReceiveConstants.java
index df1888b..e50482d 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveConstants.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveConstants.java
@@ -21,7 +21,7 @@
@VisibleForTesting
public static final String ONLY_USERS_WITH_TOGGLE_WIP_STATE_PERM_CAN_MODIFY_WIP =
- "only users with Toogle-Wip-State permission can modify Work-in-Progress";
+ "only users with Toggle-Wip-State permission can modify Work-in-Progress";
static final String COMMAND_REJECTION_MESSAGE_FOOTER =
"Contact an administrator to fix the permissions";
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index 6ebc293..2cdb7c8 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -33,6 +33,7 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
+import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
@@ -412,6 +413,10 @@
integer(ChangeQueryBuilder.FIELD_REVERTOF)
.build(cd -> cd.change().getRevertOf() != null ? cd.change().getRevertOf().get() : null);
+ public static final FieldDef<ChangeData, String> IS_PURE_REVERT =
+ fullText(ChangeQueryBuilder.FIELD_PURE_REVERT)
+ .build(cd -> Boolean.TRUE.equals(cd.isPureRevert()) ? "1" : "0");
+
@VisibleForTesting
static List<String> getReviewerFieldValues(ReviewerSet reviewers) {
List<String> r = new ArrayList<>(reviewers.asTable().size() * 2);
@@ -612,8 +617,10 @@
private static Iterable<String> getLabels(ChangeData cd) {
Set<String> allApprovals = new HashSet<>();
Set<String> distinctApprovals = new HashSet<>();
+ Table<String, Short, Integer> voteCounts = HashBasedTable.create();
for (PatchSetApproval a : cd.currentApprovals()) {
if (a.value() != 0 && !a.isLegacySubmit()) {
+ increment(voteCounts, a.label(), a.value());
Optional<LabelType> labelType = cd.getLabelTypes().byLabel(a.labelId());
allApprovals.add(formatLabel(a.label(), a.value(), a.accountId()));
@@ -626,9 +633,67 @@
}
}
allApprovals.addAll(distinctApprovals);
+ allApprovals.addAll(getCountLabelFormats(voteCounts, cd));
return allApprovals;
}
+ private static void increment(Table<String, Short, Integer> table, String k1, short k2) {
+ if (!table.contains(k1, k2)) {
+ table.put(k1, k2, 1);
+ } else {
+ int val = table.get(k1, k2);
+ table.put(k1, k2, val + 1);
+ }
+ }
+
+ private static List<String> getCountLabelFormats(
+ Table<String, Short, Integer> voteCounts, ChangeData cd) {
+ List<String> allFormats = new ArrayList<>();
+ for (String label : voteCounts.rowMap().keySet()) {
+ Optional<LabelType> labelType = cd.getLabelTypes().byLabel(label);
+ Map<Short, Integer> row = voteCounts.row(label);
+ for (short vote : row.keySet()) {
+ int count = row.get(vote);
+ allFormats.addAll(getCountLabelFormats(labelType, label, vote, count));
+ }
+ }
+ return allFormats;
+ }
+
+ private static List<String> getCountLabelFormats(
+ Optional<LabelType> labelType, String label, short vote, int count) {
+ List<String> formats =
+ getMagicLabelFormats(label, vote, labelType, /* accountId= */ null, /* count= */ count);
+ formats.add(formatLabel(label, vote, count));
+ return formats;
+ }
+
+ /** Get magic label formats corresponding to the {MIN, MAX, ANY} label votes. */
+ private static List<String> getMagicLabelFormats(
+ String label, short labelVal, Optional<LabelType> labelType, @Nullable Account.Id accountId) {
+ return getMagicLabelFormats(label, labelVal, labelType, accountId, /* count= */ null);
+ }
+
+ /** Get magic label formats corresponding to the {MIN, MAX, ANY} label votes. */
+ private static List<String> getMagicLabelFormats(
+ String label,
+ short labelVal,
+ Optional<LabelType> labelType,
+ @Nullable Account.Id accountId,
+ @Nullable Integer count) {
+ List<String> labels = new ArrayList<>();
+ if (labelType.isPresent()) {
+ if (labelVal == labelType.get().getMaxPositive()) {
+ labels.add(formatLabel(label, MagicLabelValue.MAX.name(), accountId, count));
+ }
+ if (labelVal == labelType.get().getMaxNegative()) {
+ labels.add(formatLabel(label, MagicLabelValue.MIN.name(), accountId, count));
+ }
+ }
+ labels.add(formatLabel(label, MagicLabelValue.ANY.name(), accountId, count));
+ return labels;
+ }
+
private static List<String> getLabelOwnerFormats(
PatchSetApproval a, ChangeData cd, Optional<LabelType> labelType) {
List<String> allFormats = new ArrayList<>();
@@ -653,22 +718,6 @@
return allFormats;
}
- /** Get magic label formats corresponding to the {MIN, MAX, ANY} label votes. */
- private static List<String> getMagicLabelFormats(
- String label, short labelVal, Optional<LabelType> labelType, @Nullable Account.Id accountId) {
- List<String> labels = new ArrayList<>();
- 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;
- }
-
public static Set<String> getAuthorParts(ChangeData cd) {
return SchemaUtil.getPersonParts(cd.getAuthor());
}
@@ -743,21 +792,33 @@
decodeProtos(field, PatchSetApprovalProtoConverter.INSTANCE)));
public static String formatLabel(String label, int value) {
- return formatLabel(label, value, null);
+ return formatLabel(label, value, /* accountId= */ null, /* count= */ null);
+ }
+
+ public static String formatLabel(String label, int value, @Nullable Integer count) {
+ return formatLabel(label, value, /* accountId= */ null, count);
}
public static String formatLabel(String label, int value, Account.Id accountId) {
+ return formatLabel(label, value, accountId, /* count= */ null);
+ }
+
+ public static String formatLabel(
+ String label, int value, @Nullable Account.Id accountId, @Nullable Integer count) {
return label.toLowerCase()
+ (value >= 0 ? "+" : "")
+ value
- + (accountId != null ? "," + formatAccount(accountId) : "");
+ + (accountId != null ? "," + formatAccount(accountId) : "")
+ + (count != null ? ",count=" + count : "");
}
- public static String formatLabel(String label, String value, @Nullable Account.Id accountId) {
+ public static String formatLabel(
+ String label, String value, @Nullable Account.Id accountId, @Nullable Integer count) {
return label.toLowerCase()
+ "="
+ value
- + (accountId != null ? "," + formatAccount(accountId) : "");
+ + (accountId != null ? "," + formatAccount(accountId) : "")
+ + (count != null ? ",count=" + count : "");
}
private static String formatAccount(Account.Id accountId) {
diff --git a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index 9339d62..ee93065 100644
--- a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -183,9 +183,18 @@
new Schema.Builder<ChangeData>().add(V69).add(ChangeField.ATTENTION_SET_USERS_COUNT).build();
/** Added new field {@link ChangeField#UPLOADER}. */
+ @Deprecated
static final Schema<ChangeData> V71 =
new Schema.Builder<ChangeData>().add(V70).add(ChangeField.UPLOADER).build();
+ /** Added new field {@link ChangeField#IS_PURE_REVERT}. */
+ @Deprecated
+ static final Schema<ChangeData> V72 =
+ new Schema.Builder<ChangeData>().add(V71).add(ChangeField.IS_PURE_REVERT).build();
+
+ /** Added new "count=$count" argument to the {@link ChangeField#LABEL} operator. */
+ static final Schema<ChangeData> V73 = schema(V72, false);
+
/**
* Name of the change index to be used when contacting index backends or loading configurations.
*/
diff --git a/java/com/google/gerrit/server/query/change/ChangePredicates.java b/java/com/google/gerrit/server/query/change/ChangePredicates.java
index 3afdcdd..7e50c6f 100644
--- a/java/com/google/gerrit/server/query/change/ChangePredicates.java
+++ b/java/com/google/gerrit/server/query/change/ChangePredicates.java
@@ -342,4 +342,12 @@
public static Predicate<ChangeData> submitRuleStatus(String value) {
return new ChangeIndexPredicate(ChangeField.SUBMIT_RULE_RESULT, value);
}
+
+ /**
+ * Returns a predicate that matches with changes that are pure reverts if {@code value} is equal
+ * to "1", or non-pure reverts if {@code value} is "0".
+ */
+ public static Predicate<ChangeData> pureRevert(String value) {
+ return new ChangeIndexPredicate(ChangeField.IS_PURE_REVERT, value);
+ }
}
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 8006c61..d435df1 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -84,6 +84,7 @@
import com.google.gerrit.server.plugincontext.PluginSetContext;
import com.google.gerrit.server.project.ChildProjects;
import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.query.change.PredicateArgs.ValOp;
import com.google.gerrit.server.rules.SubmitRule;
import com.google.gerrit.server.submit.SubmitDryRun;
import com.google.inject.Inject;
@@ -204,6 +205,7 @@
public static final String FIELD_WATCHEDBY = "watchedby";
public static final String FIELD_WIP = "wip";
public static final String FIELD_REVERTOF = "revertof";
+ public static final String FIELD_PURE_REVERT = "ispurerevert";
public static final String FIELD_CHERRYPICK = "cherrypick";
public static final String FIELD_CHERRY_PICK_OF_CHANGE = "cherrypickofchange";
public static final String FIELD_CHERRY_PICK_OF_PATCHSET = "cherrypickofpatchset";
@@ -213,6 +215,7 @@
public static final String ARG_ID_GROUP = "group";
public static final String ARG_ID_OWNER = "owner";
public static final String ARG_ID_NON_UPLOADER = "non_uploader";
+ public static final String ARG_COUNT = "count";
public static final Account.Id OWNER_ACCOUNT_ID = Account.id(0);
public static final Account.Id NON_UPLOADER_ACCOUNT_ID = Account.id(-1);
@@ -704,6 +707,11 @@
return ChangePredicates.assignee(Account.id(ChangeField.NO_ASSIGNEE));
}
+ if ("pure-revert".equalsIgnoreCase(value)) {
+ checkFieldAvailable(ChangeField.IS_PURE_REVERT, "is:pure-revert");
+ return ChangePredicates.pureRevert("1");
+ }
+
if ("submittable".equalsIgnoreCase(value)) {
// SubmittablePredicate will match if *any* of the submit records are OK,
// but we need to check that they're *all* OK, so check that none of the
@@ -940,6 +948,8 @@
throws QueryParseException, IOException, ConfigInvalidException {
Set<Account.Id> accounts = null;
AccountGroup.UUID group = null;
+ Integer count = null;
+ PredicateArgs.Operator countOp = null;
// Parse for:
// label:Code-Review=1,user=jsmith or
@@ -950,6 +960,7 @@
// Special case: votes by owners can be tracked with ",owner":
// label:Code-Review+2,owner
// label:Code-Review+2,user=owner
+ // label:Code-Review+1,count=2
List<String> splitReviewer = Lists.newArrayList(Splitter.on(',').limit(2).split(name));
name = splitReviewer.get(0); // remove all but the vote piece, e.g.'CodeReview=1'
@@ -957,17 +968,40 @@
// process the user/group piece
PredicateArgs lblArgs = new PredicateArgs(splitReviewer.get(1));
- for (Map.Entry<String, String> pair : lblArgs.keyValue.entrySet()) {
- if (pair.getKey().equalsIgnoreCase(ARG_ID_USER)) {
- if (pair.getValue().equals(ARG_ID_OWNER)) {
+ // Disallow using the "count=" arg in conjunction with the "user=" or "group=" args. to avoid
+ // unnecessary complexity.
+ assertDisjunctive(lblArgs, ARG_COUNT, ARG_ID_USER);
+ assertDisjunctive(lblArgs, ARG_COUNT, ARG_ID_GROUP);
+
+ for (Map.Entry<String, ValOp> pair : lblArgs.keyValue.entrySet()) {
+ String key = pair.getKey();
+ String value = pair.getValue().value();
+ PredicateArgs.Operator operator = pair.getValue().operator();
+ if (key.equalsIgnoreCase(ARG_ID_USER)) {
+ if (value.equals(ARG_ID_OWNER)) {
accounts = Collections.singleton(OWNER_ACCOUNT_ID);
- } else if (pair.getValue().equals(ARG_ID_NON_UPLOADER)) {
+ } else if (value.equals(ARG_ID_NON_UPLOADER)) {
accounts = Collections.singleton(NON_UPLOADER_ACCOUNT_ID);
} else {
- accounts = parseAccount(pair.getValue());
+ accounts = parseAccount(value);
}
- } else if (pair.getKey().equalsIgnoreCase(ARG_ID_GROUP)) {
- group = parseGroup(pair.getValue()).getUUID();
+ } else if (key.equalsIgnoreCase(ARG_ID_GROUP)) {
+ group = parseGroup(value).getUUID();
+ } else if (key.equalsIgnoreCase(ARG_COUNT)) {
+ if (!isInt(value)) {
+ throw new QueryParseException("Invalid count argument. Value should be an integer");
+ }
+ count = Integer.parseInt(value);
+ countOp = operator;
+ if (count == 0) {
+ throw new QueryParseException("Argument count=0 is not allowed.");
+ }
+ if (count > LabelPredicate.MAX_COUNT) {
+ throw new QueryParseException(
+ String.format(
+ "count=%d is not allowed. Maximum allowed value for count is %d.",
+ count, LabelPredicate.MAX_COUNT));
+ }
} else {
throw new QueryParseException("Invalid argument identifier '" + pair.getKey() + "'");
}
@@ -1016,7 +1050,18 @@
}
}
- return new LabelPredicate(args, name, accounts, group);
+ return new LabelPredicate(args, name, accounts, group, count, countOp);
+ }
+
+ /** Assert that keys {@code k1} and {@code k2} do not exist in {@code labelArgs} together. */
+ private void assertDisjunctive(PredicateArgs labelArgs, String k1, String k2)
+ throws QueryParseException {
+ Map<String, ValOp> keyValArgs = labelArgs.keyValue;
+ if (keyValArgs.containsKey(k1) && keyValArgs.containsKey(k2)) {
+ throw new QueryParseException(
+ String.format(
+ "Cannot use the '%s' argument in conjunction with the '%s' argument", k1, k2));
+ }
}
private static boolean isInt(String s) {
@@ -1339,7 +1384,7 @@
try (Repository git = args.repoManager.openRepository(args.allUsersName)) {
// [name=]<name>
if (inputArgs.keyValue.containsKey(ARG_ID_NAME)) {
- name = inputArgs.keyValue.get(ARG_ID_NAME);
+ name = inputArgs.keyValue.get(ARG_ID_NAME).value();
} else if (inputArgs.positional.size() == 1) {
name = Iterables.getOnlyElement(inputArgs.positional);
} else if (inputArgs.positional.size() > 1) {
@@ -1348,7 +1393,7 @@
// [,user=<user>]
if (inputArgs.keyValue.containsKey(ARG_ID_USER)) {
- Set<Account.Id> accounts = parseAccount(inputArgs.keyValue.get(ARG_ID_USER));
+ Set<Account.Id> accounts = parseAccount(inputArgs.keyValue.get(ARG_ID_USER).value());
if (accounts != null && accounts.size() > 1) {
throw error(
String.format(
@@ -1390,7 +1435,7 @@
try (Repository git = args.repoManager.openRepository(args.allUsersName)) {
// [name=]<name>
if (inputArgs.keyValue.containsKey(ARG_ID_NAME)) {
- name = inputArgs.keyValue.get(ARG_ID_NAME);
+ name = inputArgs.keyValue.get(ARG_ID_NAME).value();
} else if (inputArgs.positional.size() == 1) {
name = Iterables.getOnlyElement(inputArgs.positional);
} else if (inputArgs.positional.size() > 1) {
@@ -1399,7 +1444,7 @@
// [,user=<user>]
if (inputArgs.keyValue.containsKey(ARG_ID_USER)) {
- Set<Account.Id> accounts = parseAccount(inputArgs.keyValue.get(ARG_ID_USER));
+ Set<Account.Id> accounts = parseAccount(inputArgs.keyValue.get(ARG_ID_USER).value());
if (accounts != null && accounts.size() > 1) {
throw error(
String.format(
diff --git a/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java b/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
index 12efecb..b2bc6aa 100644
--- a/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.query.change;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.Change;
@@ -34,17 +35,34 @@
protected final ProjectCache projectCache;
protected final PermissionBackend permissionBackend;
protected final IdentifiedUser.GenericFactory userFactory;
+ /** label name to be matched. */
protected final String label;
+
+ /** Expected vote value for the label. */
protected final int expVal;
+
+ /**
+ * Number of times the value {@link #expVal} for label {@link #label} should occur. If null, match
+ * with any count greater or equal to 1.
+ */
+ @Nullable protected final Integer count;
+
+ /** Account ID that has voted on the label. */
protected final Account.Id account;
+
protected final AccountGroup.UUID group;
public EqualsLabelPredicate(
- LabelPredicate.Args args, String label, int expVal, Account.Id account) {
- super(ChangeField.LABEL, ChangeField.formatLabel(label, expVal, account));
+ LabelPredicate.Args args,
+ String label,
+ int expVal,
+ Account.Id account,
+ @Nullable Integer count) {
+ super(ChangeField.LABEL, ChangeField.formatLabel(label, expVal, account, count));
this.permissionBackend = args.permissionBackend;
this.projectCache = args.projectCache;
this.userFactory = args.userFactory;
+ this.count = count;
this.group = args.group;
this.label = label;
this.expVal = expVal;
@@ -60,6 +78,14 @@
return false;
}
+ if (Integer.valueOf(0).equals(count)) {
+ // We don't match against count=0 so that the computation is identical to the stored values
+ // in the index. We do that since computing count=0 requires looping on all {label_type,
+ // vote_value} for the change and storing a {count=0} format for it in the change index which
+ // is computationally expensive.
+ return false;
+ }
+
Optional<ProjectState> project = projectCache.get(c.getDest().project());
if (!project.isPresent()) {
// The project has disappeared.
@@ -73,12 +99,13 @@
}
boolean hasVote = false;
+ int matchingVotes = 0;
object.setStorageConstraint(ChangeData.StorageConstraint.INDEX_PRIMARY_NOTEDB_SECONDARY);
for (PatchSetApproval p : object.currentApprovals()) {
if (labelType.matches(p)) {
hasVote = true;
if (match(object, p.value(), p.accountId())) {
- return true;
+ matchingVotes += 1;
}
}
}
@@ -87,7 +114,7 @@
return true;
}
- return false;
+ return count == null ? matchingVotes >= 1 : matchingVotes == count;
}
protected static LabelType type(LabelTypes types, String toFind) {
diff --git a/java/com/google/gerrit/server/query/change/LabelPredicate.java b/java/com/google/gerrit/server/query/change/LabelPredicate.java
index 5f017fb..2e09075 100644
--- a/java/com/google/gerrit/server/query/change/LabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/LabelPredicate.java
@@ -14,8 +14,8 @@
package com.google.gerrit.server.query.change;
-import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.index.query.OrPredicate;
@@ -29,9 +29,11 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
+import java.util.stream.IntStream;
public class LabelPredicate extends OrPredicate<ChangeData> {
protected static final int MAX_LABEL_VALUE = 4;
+ protected static final int MAX_COUNT = 5; // inclusive
protected static class Args {
protected final ProjectCache projectCache;
@@ -40,6 +42,8 @@
protected final String value;
protected final Set<Account.Id> accounts;
protected final AccountGroup.UUID group;
+ protected final Integer count;
+ protected final PredicateArgs.Operator countOp;
protected Args(
ProjectCache projectCache,
@@ -47,13 +51,17 @@
IdentifiedUser.GenericFactory userFactory,
String value,
Set<Account.Id> accounts,
- AccountGroup.UUID group) {
+ AccountGroup.UUID group,
+ @Nullable Integer count,
+ @Nullable PredicateArgs.Operator countOp) {
this.projectCache = projectCache;
this.permissionBackend = permissionBackend;
this.userFactory = userFactory;
this.value = value;
this.accounts = accounts;
this.group = group;
+ this.count = count;
+ this.countOp = countOp;
}
}
@@ -75,19 +83,35 @@
ChangeQueryBuilder.Arguments a,
String value,
Set<Account.Id> accounts,
- AccountGroup.UUID group) {
+ AccountGroup.UUID group,
+ @Nullable Integer count,
+ @Nullable PredicateArgs.Operator countOp) {
super(
predicates(
- new Args(a.projectCache, a.permissionBackend, a.userFactory, value, accounts, group)));
+ new Args(
+ a.projectCache,
+ a.permissionBackend,
+ a.userFactory,
+ value,
+ accounts,
+ group,
+ count,
+ countOp)));
this.value = value;
}
protected static List<Predicate<ChangeData>> predicates(Args args) {
String v = args.value;
-
+ List<Integer> counts = getCounts(args.count, args.countOp);
try {
MagicLabelVote mlv = MagicLabelVote.parseWithEquals(v);
- return ImmutableList.of(magicLabelPredicate(args, mlv));
+ List<Predicate<ChangeData>> result = Lists.newArrayListWithCapacity(counts.size());
+ if (counts.isEmpty()) {
+ result.add(magicLabelPredicate(args, mlv, /* count= */ null));
+ } else {
+ counts.forEach(count -> result.add(magicLabelPredicate(args, mlv, count)));
+ }
+ return result;
} catch (IllegalArgumentException e) {
// Try next format.
}
@@ -123,16 +147,24 @@
int min = range.min;
int max = range.max;
- List<Predicate<ChangeData>> r = Lists.newArrayListWithCapacity(max - min + 1);
+ List<Predicate<ChangeData>> r =
+ Lists.newArrayListWithCapacity((counts.isEmpty() ? 1 : counts.size()) * (max - min + 1));
for (int i = min; i <= max; i++) {
- r.add(onePredicate(args, prefix, i));
+ if (counts.isEmpty()) {
+ r.add(onePredicate(args, prefix, i, /* count= */ null));
+ } else {
+ for (int count : counts) {
+ r.add(onePredicate(args, prefix, i, count));
+ }
+ }
}
return r;
}
- protected static Predicate<ChangeData> onePredicate(Args args, String label, int expVal) {
+ protected static Predicate<ChangeData> onePredicate(
+ Args args, String label, int expVal, @Nullable Integer count) {
if (expVal != 0) {
- return equalsLabelPredicate(args, label, expVal);
+ return equalsLabelPredicate(args, label, expVal, count);
}
return noLabelQuery(args, label);
}
@@ -140,34 +172,66 @@
protected static Predicate<ChangeData> noLabelQuery(Args args, String label) {
List<Predicate<ChangeData>> r = Lists.newArrayListWithCapacity(2 * MAX_LABEL_VALUE);
for (int i = 1; i <= MAX_LABEL_VALUE; i++) {
- r.add(equalsLabelPredicate(args, label, i));
- r.add(equalsLabelPredicate(args, label, -i));
+ r.add(equalsLabelPredicate(args, label, i, /* count= */ null));
+ r.add(equalsLabelPredicate(args, label, -i, /* count= */ null));
}
return not(or(r));
}
- protected static Predicate<ChangeData> equalsLabelPredicate(Args args, String label, int expVal) {
+ protected static Predicate<ChangeData> equalsLabelPredicate(
+ Args args, String label, int expVal, @Nullable Integer count) {
if (args.accounts == null || args.accounts.isEmpty()) {
- return new EqualsLabelPredicate(args, label, expVal, null);
+ return new EqualsLabelPredicate(args, label, expVal, null, count);
}
List<Predicate<ChangeData>> r = new ArrayList<>();
for (Account.Id a : args.accounts) {
- r.add(new EqualsLabelPredicate(args, label, expVal, a));
+ r.add(new EqualsLabelPredicate(args, label, expVal, a, count));
}
return or(r);
}
- protected static Predicate<ChangeData> magicLabelPredicate(Args args, MagicLabelVote mlv) {
+ protected static Predicate<ChangeData> magicLabelPredicate(
+ Args args, MagicLabelVote mlv, @Nullable Integer count) {
if (args.accounts == null || args.accounts.isEmpty()) {
- return new MagicLabelPredicate(args, mlv, /* account= */ null);
+ return new MagicLabelPredicate(args, mlv, /* account= */ null, count);
}
List<Predicate<ChangeData>> r = new ArrayList<>();
for (Account.Id a : args.accounts) {
- r.add(new MagicLabelPredicate(args, mlv, a));
+ r.add(new MagicLabelPredicate(args, mlv, a, count));
}
return or(r);
}
+ private static List<Integer> getCounts(
+ @Nullable Integer count, @Nullable PredicateArgs.Operator countOp) {
+ List<Integer> result = new ArrayList<>();
+ if (count == null) {
+ return result;
+ }
+ switch (countOp) {
+ case EQUAL:
+ case GREATER_EQUAL:
+ case LESS_EQUAL:
+ result.add(count);
+ break;
+ default:
+ break;
+ }
+ switch (countOp) {
+ case GREATER:
+ case GREATER_EQUAL:
+ IntStream.range(count + 1, MAX_COUNT + 1).forEach(result::add);
+ break;
+ case LESS:
+ case LESS_EQUAL:
+ IntStream.range(0, count).forEach(result::add);
+ break;
+ default:
+ break;
+ }
+ return result;
+ }
+
@Override
public String toString() {
return ChangeQueryBuilder.FIELD_LABEL + ":" + value;
diff --git a/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java b/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java
index 3917c79..5a81ca1 100644
--- a/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.query.change;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.LabelType;
@@ -30,13 +31,21 @@
protected final LabelPredicate.Args args;
private final MagicLabelVote magicLabelVote;
private final Account.Id account;
+ @Nullable private final Integer count;
public MagicLabelPredicate(
- LabelPredicate.Args args, MagicLabelVote magicLabelVote, Account.Id account) {
- super(ChangeField.LABEL, magicLabelVote.formatLabel());
+ LabelPredicate.Args args,
+ MagicLabelVote magicLabelVote,
+ Account.Id account,
+ @Nullable Integer count) {
+ super(
+ ChangeField.LABEL,
+ ChangeField.formatLabel(
+ magicLabelVote.label(), magicLabelVote.value().name(), account, count));
this.account = account;
this.args = args;
this.magicLabelVote = magicLabelVote;
+ this.count = count;
}
@Override
@@ -87,7 +96,7 @@
}
private EqualsLabelPredicate numericPredicate(String label, short value) {
- return new EqualsLabelPredicate(args, label, value, account);
+ return new EqualsLabelPredicate(args, label, value, account, count);
}
protected static LabelType type(LabelTypes types, String toFind) {
diff --git a/java/com/google/gerrit/server/query/change/PredicateArgs.java b/java/com/google/gerrit/server/query/change/PredicateArgs.java
index d82b9bc..9f0dffb 100644
--- a/java/com/google/gerrit/server/query/change/PredicateArgs.java
+++ b/java/com/google/gerrit/server/query/change/PredicateArgs.java
@@ -14,12 +14,15 @@
package com.google.gerrit.server.query.change;
+import com.google.auto.value.AutoValue;
import com.google.common.base.Splitter;
import com.google.gerrit.index.query.QueryParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
/**
* This class is used to extract comma separated values in a predicate.
@@ -30,8 +33,35 @@
* appear in the map and others in the positional list (e.g. "vote=approved,jb_2.3).
*/
public class PredicateArgs {
+ private static final Pattern SPLIT_PATTERN = Pattern.compile("(>|>=|=|<|<=)([^=].*)$");
+
public List<String> positional;
- public Map<String, String> keyValue;
+ public Map<String, ValOp> keyValue;
+
+ enum Operator {
+ EQUAL("="),
+ GREATER_EQUAL(">="),
+ GREATER(">"),
+ LESS_EQUAL("<="),
+ LESS("<");
+
+ final String op;
+
+ Operator(String op) {
+ this.op = op;
+ }
+ };
+
+ @AutoValue
+ public abstract static class ValOp {
+ abstract String value();
+
+ abstract Operator operator();
+
+ static ValOp create(String value, Operator operator) {
+ return new AutoValue_PredicateArgs_ValOp(value, operator);
+ }
+ }
/**
* Parses query arguments into {@link #keyValue} and/or {@link #positional}..
@@ -46,19 +76,39 @@
keyValue = new HashMap<>();
for (String arg : Splitter.on(',').split(args)) {
- List<String> splitKeyValue = Splitter.on('=').splitToList(arg);
+ Matcher m = SPLIT_PATTERN.matcher(arg);
- if (splitKeyValue.size() == 1) {
- positional.add(splitKeyValue.get(0));
- } else if (splitKeyValue.size() == 2) {
- if (!keyValue.containsKey(splitKeyValue.get(0))) {
- keyValue.put(splitKeyValue.get(0), splitKeyValue.get(1));
+ if (!m.find()) {
+ positional.add(arg);
+ } else if (m.groupCount() == 2) {
+ String key = arg.substring(0, m.start());
+ String op = m.group(1);
+ String val = m.group(2);
+ if (!keyValue.containsKey(key)) {
+ keyValue.put(key, ValOp.create(val, getOperator(op)));
} else {
- throw new QueryParseException("Duplicate key " + splitKeyValue.get(0));
+ throw new QueryParseException("Duplicate key " + key);
}
} else {
- throw new QueryParseException("invalid arg " + arg);
+ throw new QueryParseException("Invalid arg " + arg);
}
}
}
+
+ private Operator getOperator(String operator) {
+ switch (operator) {
+ case "<":
+ return Operator.LESS;
+ case "<=":
+ return Operator.LESS_EQUAL;
+ case "=":
+ return Operator.EQUAL;
+ case ">=":
+ return Operator.GREATER_EQUAL;
+ case ">":
+ return Operator.GREATER;
+ default:
+ throw new IllegalArgumentException("Invalid Operator " + operator);
+ }
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java b/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
index f511683..b76d5cb 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
@@ -25,6 +25,7 @@
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.LabelFunction;
import com.google.gerrit.entities.SubmitRequirement;
import com.google.gerrit.entities.SubmitRequirementExpression;
@@ -32,6 +33,7 @@
import com.google.gerrit.entities.SubmitRequirementExpressionResult.Status;
import com.google.gerrit.entities.SubmitRequirementResult;
import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.project.SubmitRequirementsEvaluator;
import com.google.gerrit.server.query.change.ChangeData;
@@ -215,6 +217,33 @@
.isEqualTo("Unsupported operator invalid_field:invalid_value");
}
+ @Test
+ public void byPureRevert() throws Exception {
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result pushResult =
+ createChange(testRepo, "refs/heads/master", "Fix a bug", "file.txt", "content", "topic");
+ changeData = pushResult.getChange();
+ changeId = pushResult.getChangeId();
+
+ SubmitRequirement sr =
+ createSubmitRequirement(
+ /* applicabilityExpr= */ "project:" + project.get(),
+ /* submittabilityExpr= */ "is:pure-revert",
+ /* overrideExpr= */ "");
+
+ SubmitRequirementResult result = evaluator.evaluateRequirement(sr, changeData);
+ assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.UNSATISFIED);
+ approve(changeId);
+ gApi.changes().id(changeId).current().submit();
+
+ ChangeInfo changeInfo = gApi.changes().id(changeId).revert().get();
+ String revertId = Integer.toString(changeInfo._number);
+ ChangeData revertChangeData =
+ changeQueryProvider.get().byLegacyChangeId(Change.Id.parse(revertId)).get(0);
+ result = evaluator.evaluateRequirement(sr, revertChangeData);
+ assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.SATISFIED);
+ }
+
private void voteLabel(String changeId, String labelName, int score) throws RestApiException {
gApi.changes().id(changeId).current().review(new ReviewInput().label(labelName, score));
}
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 3b671aa..5253a5b 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -26,6 +26,7 @@
import static com.google.gerrit.server.project.testing.TestLabels.label;
import static com.google.gerrit.server.project.testing.TestLabels.value;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;
@@ -40,6 +41,9 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multimaps;
import com.google.common.collect.Streams;
import com.google.common.truth.ThrowableSubject;
import com.google.gerrit.acceptance.ExtensionRegistry;
@@ -48,6 +52,7 @@
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.RawInputUtil;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.BranchNameKey;
@@ -141,7 +146,6 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
-import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@@ -1038,6 +1042,7 @@
ChangeInserter ins3 = newChange(repo);
ChangeInserter ins4 = newChange(repo);
ChangeInserter ins5 = newChange(repo);
+ ChangeInserter ins6 = newChange(repo);
Change reviewMinus2Change = insert(repo, ins);
gApi.changes().id(reviewMinus2Change.getId().get()).current().review(ReviewInput.reject());
@@ -1050,7 +1055,13 @@
Change reviewPlus1Change = insert(repo, ins4);
gApi.changes().id(reviewPlus1Change.getId().get()).current().review(ReviewInput.recommend());
- Change reviewPlus2Change = insert(repo, ins5);
+ Change reviewTwoPlus1Change = insert(repo, ins5);
+ gApi.changes().id(reviewTwoPlus1Change.getId().get()).current().review(ReviewInput.recommend());
+ requestContext.setContext(newRequestContext(createAccount("user1")));
+ gApi.changes().id(reviewTwoPlus1Change.getId().get()).current().review(ReviewInput.recommend());
+ requestContext.setContext(newRequestContext(userId));
+
+ Change reviewPlus2Change = insert(repo, ins6);
gApi.changes().id(reviewPlus2Change.getId().get()).current().review(ReviewInput.approve());
Map<String, Short> m =
@@ -1061,8 +1072,10 @@
assertThat(m).hasSize(1);
assertThat(m).containsEntry("Code-Review", Short.valueOf((short) 1));
- Map<Integer, Change> changes = new LinkedHashMap<>(5);
+ Multimap<Integer, Change> changes =
+ Multimaps.newListMultimap(Maps.newLinkedHashMap(), () -> Lists.newArrayList());
changes.put(2, reviewPlus2Change);
+ changes.put(1, reviewTwoPlus1Change);
changes.put(1, reviewPlus1Change);
changes.put(0, noLabelChange);
changes.put(-1, reviewMinus1Change);
@@ -1074,9 +1087,9 @@
assertQuery("label:Code-Review=-1", reviewMinus1Change);
assertQuery("label:Code-Review-1", reviewMinus1Change);
assertQuery("label:Code-Review=0", noLabelChange);
- assertQuery("label:Code-Review=+1", reviewPlus1Change);
- assertQuery("label:Code-Review=1", reviewPlus1Change);
- assertQuery("label:Code-Review+1", reviewPlus1Change);
+ assertQuery("label:Code-Review=+1", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery("label:Code-Review=1", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery("label:Code-Review+1", reviewTwoPlus1Change, reviewPlus1Change);
assertQuery("label:Code-Review=+2", reviewPlus2Change);
assertQuery("label:Code-Review=2", reviewPlus2Change);
assertQuery("label:Code-Review+2", reviewPlus2Change);
@@ -1084,6 +1097,7 @@
assertQuery(
"label:Code-Review=ANY",
reviewPlus2Change,
+ reviewTwoPlus1Change,
reviewPlus1Change,
reviewMinus1Change,
reviewMinus2Change);
@@ -1112,14 +1126,70 @@
assertQuery("label:Code-Review<-2");
assertQuery("label:Code-Review=+1,anotheruser");
- assertQuery("label:Code-Review=+1,user", reviewPlus1Change);
- assertQuery("label:Code-Review=+1,user=user", reviewPlus1Change);
- assertQuery("label:Code-Review=+1,Administrators", reviewPlus1Change);
- assertQuery("label:Code-Review=+1,group=Administrators", reviewPlus1Change);
- assertQuery("label:Code-Review=+1,user=owner", reviewPlus1Change);
- assertQuery("label:Code-Review=+1,owner", reviewPlus1Change);
+ assertQuery("label:Code-Review=+1,user", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery("label:Code-Review=+1,user=user", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery("label:Code-Review=+1,Administrators", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery(
+ "label:Code-Review=+1,group=Administrators", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery("label:Code-Review=+1,user=owner", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery("label:Code-Review=+1,owner", reviewTwoPlus1Change, reviewPlus1Change);
assertQuery("label:Code-Review=+2,owner", reviewPlus2Change);
assertQuery("label:Code-Review=-2,owner", reviewMinus2Change);
+
+ // count=0 is not allowed
+ Exception thrown =
+ assertThrows(BadRequestException.class, () -> assertQuery("label:Code-Review=+2,count=0"));
+ assertThat(thrown).hasMessageThat().isEqualTo("Argument count=0 is not allowed.");
+ assertQuery("label:Code-Review=1,count=1", reviewPlus1Change);
+ assertQuery("label:Code-Review=1,count=2", reviewTwoPlus1Change);
+ assertQuery("label:Code-Review=1,count>=2", reviewTwoPlus1Change);
+ assertQuery("label:Code-Review=1,count>1", reviewTwoPlus1Change);
+ assertQuery("label:Code-Review=1,count>=1", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery("label:Code-Review=1,count=3");
+ thrown =
+ assertThrows(BadRequestException.class, () -> assertQuery("label:Code-Review=1,count=7"));
+ assertThat(thrown)
+ .hasMessageThat()
+ .isEqualTo("count=7 is not allowed. Maximum allowed value for count is 5.");
+
+ // Less than operator does not match with changes having count=0 for a specific vote value (i.e.
+ // no votes for that specific value). We do that deliberately since the computation of count=0
+ // for label values is expensive when the change is re-indexed. This is because the operation
+ // requires generating all formats for all {label-type, vote}=0 values that are non-voted for
+ // the change and storing them with the 'count=0' format.
+ assertQuery("label:Code-Review=1,count<5", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery("label:Code-Review=1,count<=5", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery(
+ "label:Code-Review=1,count<=1", // reviewTwoPlus1Change is not matched since its count=2
+ reviewPlus1Change);
+ assertQuery(
+ "label:Code-Review=1,count<5 label:Code-Review=1,count>=1",
+ reviewTwoPlus1Change,
+ reviewPlus1Change);
+ assertQuery(
+ "label:Code-Review=1,count<=5 label:Code-Review=1,count>=1",
+ reviewTwoPlus1Change,
+ reviewPlus1Change);
+ assertQuery("label:Code-Review=1,count<=1 label:Code-Review=1,count>=1", reviewPlus1Change);
+
+ assertQuery("label:Code-Review=MAX,count=1", reviewPlus2Change);
+ assertQuery("label:Code-Review=MAX,count=2");
+ assertQuery("label:Code-Review=MIN,count=1", reviewMinus2Change);
+ assertQuery("label:Code-Review=MIN,count>1");
+ assertQuery("label:Code-Review=MAX,count<2", reviewPlus2Change);
+ assertQuery("label:Code-Review=MIN,count<1");
+ assertQuery("label:Code-Review=MAX,count<2 label:Code-Review=MAX,count>=1", reviewPlus2Change);
+ assertQuery("label:Code-Review=MIN,count<1 label:Code-Review=MIN,count>=1");
+ assertQuery("label:Code-Review>=+1,count=2", reviewTwoPlus1Change);
+
+ // "count" and "user" args cannot be used simultaneously.
+ assertThrows(
+ BadRequestException.class,
+ () -> assertQuery("label:Code-Review=+1,user=non_uploader,count=2"));
+
+ // "count" and "group" args cannot be used simultaneously.
+ assertThrows(
+ BadRequestException.class, () -> assertQuery("label:Code-Review=+1,group=gerrit,count=2"));
}
@Test
@@ -1224,16 +1294,15 @@
assertQuery("label:Code-Review=+1,non_uploader", reviewPlus1Change);
}
- private Change[] codeReviewInRange(Map<Integer, Change> changes, int start, int end) {
- int size = 0;
- Change[] range = new Change[end - start + 1];
- for (int i : changes.keySet()) {
+ private Change[] codeReviewInRange(Multimap<Integer, Change> changes, int start, int end) {
+ List<Change> range = new ArrayList<>();
+ for (Map.Entry<Integer, Change> entry : changes.entries()) {
+ int i = entry.getKey();
if (i >= start && i <= end) {
- range[size] = changes.get(i);
- size++;
+ range.add(entry.getValue());
}
}
- return range;
+ return range.toArray(new Change[0]);
}
private String createGroup(String name, String owner) throws Exception {
@@ -3891,6 +3960,33 @@
}
@Test
+ public void isPureRevert() throws Exception {
+ assume().that(getSchema().hasField(ChangeField.IS_PURE_REVERT)).isTrue();
+ TestRepository<Repo> repo = createProject("repo");
+ // Create two commits and revert second commit (initial commit can't be reverted)
+ Change initial = insert(repo, newChange(repo));
+ gApi.changes().id(initial.getChangeId()).current().review(ReviewInput.approve());
+ gApi.changes().id(initial.getChangeId()).current().submit();
+
+ ChangeInfo changeToRevert =
+ gApi.changes().create(new ChangeInput("repo", "master", "commit to revert")).get();
+ gApi.changes().id(changeToRevert.id).current().review(ReviewInput.approve());
+ gApi.changes().id(changeToRevert.id).current().submit();
+
+ ChangeInfo changeThatReverts = gApi.changes().id(changeToRevert.id).revert().get();
+ Change.Id changeThatRevertsId = Change.id(changeThatReverts._number);
+ assertQueryByIds("is:pure-revert", changeThatRevertsId);
+
+ // Update the change that reverts such that it's not a pure revert
+ gApi.changes()
+ .id(changeThatReverts.id)
+ .edit()
+ .modifyFile("some-file.txt", RawInputUtil.create("newcontent".getBytes(UTF_8)));
+ gApi.changes().id(changeThatReverts.id).edit().publish();
+ assertQueryByIds("is:pure-revert");
+ }
+
+ @Test
public void selfFailsForAnonymousUser() throws Exception {
for (String query : ImmutableList.of("assignee:self", "has:star", "is:starred", "star:star")) {
assertQuery(query);
diff --git a/plugins/package.json b/plugins/package.json
index 4e3c376..e5d245c 100644
--- a/plugins/package.json
+++ b/plugins/package.json
@@ -6,7 +6,7 @@
"@polymer/decorators": "^3.0.0",
"@polymer/polymer": "^3.4.1",
"@gerritcodereview/typescript-api": "3.4.4",
- "lit": "2.0.0-rc.3"
+ "lit": "^2.0.2"
},
"license": "Apache-2.0",
"private": true
diff --git a/plugins/yarn.lock b/plugins/yarn.lock
index 3ff1cc4..4cbe489 100644
--- a/plugins/yarn.lock
+++ b/plugins/yarn.lock
@@ -7,10 +7,10 @@
resolved "https://registry.yarnpkg.com/@gerritcodereview/typescript-api/-/typescript-api-3.4.4.tgz#9f09687038088dd7edd3b4e30d249502eb21bfbc"
integrity sha512-MAiQwntcQ59b92yYDsVIXj3oBbAB4C7HELkLFFbYs4ZjzC43XqqtR9VF0dh5OUC8wzFZttgUiOmGehk9edpPuw==
-"@lit/reactive-element@^1.0.0-rc.2":
- version "1.0.0-rc.2"
- resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.0.0-rc.2.tgz#f24dba16ea571a08dca70f1783bd2ca5ec8de3ee"
- integrity sha512-cujeIl5Ei8FC7UHf4/4Q3bRJOtdTe1vpJV/JEBYCggedmQ+2P8A2oz7eE+Vxi6OJ4nc0X+KZxXnBoH4QrEbmEQ==
+"@lit/reactive-element@^1.0.0":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.0.1.tgz#853cacd4d78d79059f33f66f8e7b0e5c34bee294"
+ integrity sha512-nSD5AA2AZkKuXuvGs8IK7K5ZczLAogfDd26zT9l6S7WzvqALdVWcW5vMUiTnZyj5SPcNwNNANj0koeV1ieqTFQ==
"@polymer/decorators@^3.0.0":
version "3.0.0"
@@ -26,43 +26,36 @@
dependencies:
"@webcomponents/shadycss" "^1.9.1"
-"@types/trusted-types@^1.0.1":
- version "1.0.6"
- resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-1.0.6.tgz#569b8a08121d3203398290d602d84d73c8dcf5da"
- integrity sha512-230RC8sFeHoT6sSUlRO6a8cAnclO06eeiq1QDfiv2FGCLWFvvERWgwIQD4FWqD9A69BN7Lzee4OXwoMVnnsWDw==
+"@types/trusted-types@^2.0.2":
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756"
+ integrity sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==
"@webcomponents/shadycss@^1.9.1":
version "1.11.0"
resolved "https://registry.yarnpkg.com/@webcomponents/shadycss/-/shadycss-1.11.0.tgz#73e289996c002d8be694cd3be0e83c46ad25e7e0"
integrity sha512-L5O/+UPum8erOleNjKq6k58GVl3fNsEQdSOyh0EUhNmi7tHUyRuCJy1uqJiWydWcLARE5IPsMoPYMZmUGrz1JA==
-lit-element@^3.0.0-rc.2:
- version "3.0.0-rc.2"
- resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-3.0.0-rc.2.tgz#883d0b6fd7b846226d360699d1b713da5fc7e1b7"
- integrity sha512-2Z7DabJ3b5K+p5073vFjMODoaWqy5PIaI4y6ADKm+fCGc8OnX9fU9dMoUEBZjFpd/bEFR9PBp050tUtBnT9XTQ==
+lit-element@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-3.0.1.tgz#3c545af17d8a46268bc1dd5623a47486e6ff76f4"
+ integrity sha512-vs9uybH9ORyK49CFjoNGN85HM9h5bmisU4TQ63phe/+GYlwvY/3SIFYKdjV6xNvzz8v2MnVC+9+QOkPqh+Q3Ew==
dependencies:
- "@lit/reactive-element" "^1.0.0-rc.2"
- lit-html "^2.0.0-rc.3"
+ "@lit/reactive-element" "^1.0.0"
+ lit-html "^2.0.0"
-lit-html@^2.0.0-rc.3:
- version "2.0.0-rc.3"
- resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.0.0-rc.3.tgz#1c216e548630e18d3093d97f4e29563abce659af"
- integrity sha512-Y6P8LlAyQuqvzq6l/Nc4z5/P5M/rVLYKQIRxcNwSuGajK0g4kbcBFQqZmgvqKG+ak+dHZjfm2HUw9TF5N/pkCw==
+lit-html@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.0.1.tgz#63241015efa07bc9259b6f96f04abd052d2a1f95"
+ integrity sha512-KF5znvFdXbxTYM/GjpdOOnMsjgRcFGusTnB54ixnCTya5zUR0XqrDRj29ybuLS+jLXv1jji6Y8+g4W7WP8uL4w==
dependencies:
- "@types/trusted-types" "^1.0.1"
+ "@types/trusted-types" "^2.0.2"
-lit-html@^2.0.0-rc.4:
- version "2.0.0-rc.4"
- resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.0.0-rc.4.tgz#1015fa8f1f7c8c5b79999ed0bc11c3b79ff1aab5"
- integrity sha512-WSLGu3vxq7y8q/oOd9I3zxyBELNLLiDk6gAYoKK4PGctI5fbh6lhnO/jVBdy0PV/vTc+cLJCA/occzx3YoNPeg==
+lit@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/lit/-/lit-2.0.2.tgz#5e6f422924e0732258629fb379556b6d23f7179c"
+ integrity sha512-hKA/1YaSB+P+DvKWuR2q1Xzy/iayhNrJ3aveD0OQ9CKn6wUjsdnF/7LavDOJsKP/K5jzW/kXsuduPgRvTFrFJw==
dependencies:
- "@types/trusted-types" "^1.0.1"
-
-lit@2.0.0-rc.3:
- version "2.0.0-rc.3"
- resolved "https://registry.yarnpkg.com/lit/-/lit-2.0.0-rc.3.tgz#8b6a85268aba287c11125dfe57e88e0bc09beaff"
- integrity sha512-UZDLWuspl7saA+WvS0e+TE3NdGGE05hOIwUPTWiibs34c5QupcEzpjB/aElt79V9bELQVNbUUwa0Ow7D1Wuszw==
- dependencies:
- "@lit/reactive-element" "^1.0.0-rc.2"
- lit-element "^3.0.0-rc.2"
- lit-html "^2.0.0-rc.4"
+ "@lit/reactive-element" "^1.0.0"
+ lit-element "^3.0.0"
+ lit-html "^2.0.0"
diff --git a/polygerrit-ui/app/api/rest-api.ts b/polygerrit-ui/app/api/rest-api.ts
index 39b40b6..e2b1502 100644
--- a/polygerrit-ui/app/api/rest-api.ts
+++ b/polygerrit-ui/app/api/rest-api.ts
@@ -1065,7 +1065,7 @@
url: string;
/** URL to the icon of the link. */
image_url?: string;
- /* The links target. */
+ /* Value of the "target" attribute for anchor elements. */
target?: string;
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts
index cf1e8ef..cd55e15 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts
@@ -46,7 +46,8 @@
width: 100%;
}
.comments,
- .reviewers {
+ .reviewers,
+ .requirements {
white-space: nowrap;
}
.reviewers {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
index 12cf314..68566f0 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
@@ -16,9 +16,10 @@
*/
import '../../../styles/gr-change-list-styles';
+import '../../../styles/gr-font-styles';
+import '../../../styles/shared-styles';
import '../../shared/gr-cursor-manager/gr-cursor-manager';
import '../gr-change-list-item/gr-change-list-item';
-import '../../../styles/shared-styles';
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
import {afterNextRender} from '@polymer/polymer/lib/utils/render-status';
import {PolymerElement} from '@polymer/polymer/polymer-element';
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.ts b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.ts
index c31da77..77320b9 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.ts
@@ -20,6 +20,9 @@
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
+ <style include="gr-font-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
<style include="gr-change-list-styles">
#changeList {
border-collapse: collapse;
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
index ad8f72f..e4b466b 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
@@ -232,6 +232,9 @@
height: var(--line-height-small);
vertical-align: top;
}
+ .checksChip a iron-icon.launch {
+ color: var(--link-color);
+ }
.checksChip.error {
color: var(--error-foreground);
border-color: var(--error-foreground);
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index 8aeb082..ca237d1 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -623,6 +623,12 @@
private diffViewMode?: DiffViewMode;
+ /**
+ * If the user comes back to the change page we want to remember the scroll
+ * position when we re-render the page as is.
+ */
+ private scrollPosition?: number;
+
override ready() {
super.ready();
aPluginHasRegistered$.pipe(takeUntil(this.disconnected$)).subscribe(b => {
@@ -711,6 +717,7 @@
this.addEventListener('open-fix-preview', e => this._onOpenFixPreview(e));
this.addEventListener('close-fix-preview', e => this._onCloseFixPreview(e));
document.addEventListener('visibilitychange', this.handleVisibilityChange);
+ document.addEventListener('scroll', this.handleScroll);
this.addEventListener(EventType.SHOW_PRIMARY_TAB, e =>
this._setActivePrimaryTab(e)
@@ -729,6 +736,7 @@
'visibilitychange',
this.handleVisibilityChange
);
+ document.removeEventListener('scroll', this.handleScroll);
this.replyRefitTask?.cancel();
this.scrollTask?.cancel();
@@ -746,6 +754,15 @@
return this.shadowRoot!.querySelector<GrThreadList>('gr-thread-list');
}
+ private readonly handleScroll = () => {
+ if (!this.isViewCurrent) return;
+ this.scrollTask = debounce(
+ this.scrollTask,
+ () => (this.scrollPosition = document.documentElement.scrollTop),
+ 150
+ );
+ };
+
_onOpenFixPreview(e: OpenFixPreviewEvent) {
this.$.applyFixDialog.open(e);
}
@@ -1251,6 +1268,7 @@
// If there is no change in patchset or changeNum, such as when user goes
// to the diff view and then comes back to change page then there is no
// need to reload anything and we render the change view component as is.
+ document.documentElement.scrollTop = this.scrollPosition ?? 0;
return;
}
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
index 760e211..962ccef 100644
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
@@ -37,6 +37,7 @@
import {Execution} from '../../../constants/reporting';
import {ChangeStatus} from '../../../constants/constants';
import {KnownExperimentId} from '../../../services/flags/flags';
+import {fontStyles} from '../../../styles/gr-font-styles';
@customElement('gr-label-scores')
export class GrLabelScores extends LitElement {
@@ -55,6 +56,7 @@
static override get styles() {
return [
+ fontStyles,
css`
.scoresTable {
display: table;
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts
index e4703df..744db3b 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts
@@ -35,6 +35,9 @@
href?: string;
@property()
+ label?: string;
+
+ @property()
showSubmittableCheck = false;
@property()
@@ -110,7 +113,12 @@
const linkClass = this._computeLinkClass(change);
return html`
<div class="changeContainer">
- <a href="${ifDefined(this.href)}" class="${linkClass}"><slot></slot></a>
+ <a
+ href="${ifDefined(this.href)}"
+ aria-label="${ifDefined(this.label)}"
+ class="${linkClass}"
+ ><slot></slot
+ ></a>
${this.showSubmittableCheck
? html`<span
tabindex="-1"
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
index 7493e2f..963c009 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
@@ -33,6 +33,7 @@
import {appContext} from '../../../services/app-context';
import {ParsedChangeInfo} from '../../../types/types';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {truncatePath} from '../../../utils/path-list-util';
import {pluralize} from '../../../utils/string-util';
import {
changeIsOpen,
@@ -146,6 +147,45 @@
this.conflictingChanges.length,
this.cherryPickChanges.length
);
+
+ const sectionRenderers = [
+ this.renderRelationChain,
+ this.renderSubmittedTogether,
+ this.renderSameTopic,
+ this.renderMergeConflicts,
+ this.renderCherryPicks,
+ ];
+
+ let firstNonEmptySectionFound = false;
+ const sections = [];
+ for (const renderer of sectionRenderers) {
+ const section: TemplateResult<1> | undefined = renderer.call(
+ this,
+ !firstNonEmptySectionFound,
+ sectionSize
+ );
+ firstNonEmptySectionFound = firstNonEmptySectionFound || !!section;
+ sections.push(section);
+ }
+
+ return html`<gr-endpoint-decorator name="related-changes-section">
+ <gr-endpoint-param
+ name="change"
+ .value=${this.change}
+ ></gr-endpoint-param>
+ <gr-endpoint-slot name="top"></gr-endpoint-slot>
+ ${sections}
+ <gr-endpoint-slot name="bottom"></gr-endpoint-slot>
+ </gr-endpoint-decorator>`;
+ }
+
+ private renderRelationChain(
+ isFirst: boolean,
+ sectionSize: (section: Section) => number
+ ) {
+ if (this.relatedChanges.length === 0) {
+ return undefined;
+ }
const relatedChangesMarkersPredicate = this.markersPredicateFactory(
this.relatedChanges.length,
this.relatedChanges.findIndex(relatedChange =>
@@ -158,17 +198,11 @@
this.patchNum,
this.relatedChanges
);
- let firstNonEmptySectionFound = false;
- let isFirstNonEmpty =
- !firstNonEmptySectionFound && !!this.relatedChanges.length;
- firstNonEmptySectionFound = firstNonEmptySectionFound || isFirstNonEmpty;
- const relatedChangeSection = html` <section
- id="relatedChanges"
- ?hidden=${!this.relatedChanges.length}
- >
+
+ return html`<section id="relatedChanges">
<gr-related-collapse
title="Relation chain"
- class="${classMap({first: isFirstNonEmpty})}"
+ class="${classMap({first: isFirst})}"
.length=${this.relatedChanges.length}
.numChangesWhenCollapsed=${sectionSize(Section.RELATED_CHANGES)}
>
@@ -200,8 +234,19 @@
)}
</gr-related-collapse>
</section>`;
+ }
+ private renderSubmittedTogether(
+ isFirst: boolean,
+ sectionSize: (section: Section) => number
+ ) {
const submittedTogetherChanges = this.submittedTogether?.changes ?? [];
+ if (
+ !submittedTogetherChanges.length &&
+ !this.submittedTogether?.non_visible_changes
+ ) {
+ return undefined;
+ }
const countNonVisibleChanges =
this.submittedTogether?.non_visible_changes ?? 0;
const submittedTogetherMarkersPredicate = this.markersPredicateFactory(
@@ -211,19 +256,10 @@
),
sectionSize(Section.SUBMITTED_TOGETHER)
);
- isFirstNonEmpty =
- !firstNonEmptySectionFound &&
- (!!submittedTogetherChanges?.length ||
- !!this.submittedTogether?.non_visible_changes);
- firstNonEmptySectionFound = firstNonEmptySectionFound || isFirstNonEmpty;
- const submittedTogetherSection = html`<section
- id="submittedTogether"
- ?hidden=${!submittedTogetherChanges?.length &&
- !this.submittedTogether?.non_visible_changes}
- >
+ return html`<section id="submittedTogether">
<gr-related-collapse
title="Submitted together"
- class="${classMap({first: isFirstNonEmpty})}"
+ class="${classMap({first: isFirst})}"
.length=${submittedTogetherChanges.length}
.numChangesWhenCollapsed=${sectionSize(Section.SUBMITTED_TOGETHER)}
>
@@ -239,14 +275,14 @@
${this.renderMarkers(
submittedTogetherMarkersPredicate(index)
)}<gr-related-change
+ .label="${this.renderChangeTitle(change)}"
.change="${change}"
.href="${GerritNav.getUrlForChangeById(
change._number,
change.project
)}"
.showSubmittableCheck=${true}
- >${change.project}: ${change.branch}:
- ${change.subject}</gr-related-change
+ >${this.renderChangeLine(change)}</gr-related-change
>
</div>`
)}
@@ -255,22 +291,25 @@
(+ ${pluralize(countNonVisibleChanges, 'non-visible change')})
</div>
</section>`;
+ }
+
+ private renderSameTopic(
+ isFirst: boolean,
+ sectionSize: (section: Section) => number
+ ) {
+ if (!this.sameTopicChanges?.length) {
+ return undefined;
+ }
const sameTopicMarkersPredicate = this.markersPredicateFactory(
this.sameTopicChanges.length,
-1,
sectionSize(Section.SAME_TOPIC)
);
- isFirstNonEmpty =
- !firstNonEmptySectionFound && !!this.sameTopicChanges?.length;
- firstNonEmptySectionFound = firstNonEmptySectionFound || isFirstNonEmpty;
- const sameTopicSection = html`<section
- id="sameTopic"
- ?hidden=${!this.sameTopicChanges?.length}
- >
+ return html`<section id="sameTopic">
<gr-related-collapse
title="Same topic"
- class="${classMap({first: isFirstNonEmpty})}"
+ class="${classMap({first: isFirst})}"
.length=${this.sameTopicChanges.length}
.numChangesWhenCollapsed=${sectionSize(Section.SAME_TOPIC)}
>
@@ -287,33 +326,35 @@
sameTopicMarkersPredicate(index)
)}<gr-related-change
.change="${change}"
+ .label="${this.renderChangeTitle(change)}"
.href="${GerritNav.getUrlForChangeById(
change._number,
change.project
)}"
- >${change.project}: ${change.branch}:
- ${change.subject}</gr-related-change
+ >${this.renderChangeLine(change)}</gr-related-change
>
</div>`
)}
</gr-related-collapse>
</section>`;
+ }
+ private renderMergeConflicts(
+ isFirst: boolean,
+ sectionSize: (section: Section) => number
+ ) {
+ if (!this.conflictingChanges?.length) {
+ return undefined;
+ }
const mergeConflictsMarkersPredicate = this.markersPredicateFactory(
this.conflictingChanges.length,
-1,
sectionSize(Section.MERGE_CONFLICTS)
);
- isFirstNonEmpty =
- !firstNonEmptySectionFound && !!this.conflictingChanges?.length;
- firstNonEmptySectionFound = firstNonEmptySectionFound || isFirstNonEmpty;
- const mergeConflictsSection = html`<section
- id="mergeConflicts"
- ?hidden=${!this.conflictingChanges?.length}
- >
+ return html`<section id="mergeConflicts">
<gr-related-collapse
title="Merge conflicts"
- class="${classMap({first: isFirstNonEmpty})}"
+ class="${classMap({first: isFirst})}"
.length=${this.conflictingChanges.length}
.numChangesWhenCollapsed=${sectionSize(Section.MERGE_CONFLICTS)}
>
@@ -340,22 +381,24 @@
)}
</gr-related-collapse>
</section>`;
+ }
+ private renderCherryPicks(
+ isFirst: boolean,
+ sectionSize: (section: Section) => number
+ ) {
+ if (!this.cherryPickChanges.length) {
+ return undefined;
+ }
const cherryPicksMarkersPredicate = this.markersPredicateFactory(
this.cherryPickChanges.length,
-1,
sectionSize(Section.CHERRY_PICKS)
);
- isFirstNonEmpty =
- !firstNonEmptySectionFound && !!this.cherryPickChanges?.length;
- firstNonEmptySectionFound = firstNonEmptySectionFound || isFirstNonEmpty;
- const cherryPicksSection = html`<section
- id="cherryPicks"
- ?hidden=${!this.cherryPickChanges?.length}
- >
+ return html`<section id="cherryPicks">
<gr-related-collapse
title="Cherry picks"
- class="${classMap({first: isFirstNonEmpty})}"
+ class="${classMap({first: isFirst})}"
.length=${this.cherryPickChanges.length}
.numChangesWhenCollapsed=${sectionSize(Section.CHERRY_PICKS)}
>
@@ -382,17 +425,17 @@
)}
</gr-related-collapse>
</section>`;
+ }
- return html`<gr-endpoint-decorator name="related-changes-section">
- <gr-endpoint-param
- name="change"
- .value=${this.change}
- ></gr-endpoint-param>
- <gr-endpoint-slot name="top"></gr-endpoint-slot>
- ${relatedChangeSection} ${submittedTogetherSection} ${sameTopicSection}
- ${mergeConflictsSection} ${cherryPicksSection}
- <gr-endpoint-slot name="bottom"></gr-endpoint-slot>
- </gr-endpoint-decorator>`;
+ private renderChangeTitle(change: ChangeInfo) {
+ return `${change.project}: ${change.branch}: ${change.subject}`;
+ }
+
+ private renderChangeLine(change: ChangeInfo) {
+ const truncatedRepo = truncatePath(change.project, 2);
+ return html`<span class="truncatedRepo" .title="${change.project}"
+ >${truncatedRepo}</span
+ >: ${change.branch}: ${change.subject}`;
}
sectionSizeFactory(
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.ts
index a6dc338f..d0b56fb 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.ts
@@ -30,6 +30,7 @@
createSubmittedTogetherInfo,
} from '../../../test/test-data-generators';
import {
+ query,
queryAndAssert,
resetPlugins,
stubRestApi,
@@ -227,11 +228,8 @@
Promise.resolve(submittedTogether)
);
await element.reload();
- const relatedChanges = queryAndAssert<GrRelatedCollapse>(
- queryAndAssert<HTMLElement>(element, '#relatedChanges'),
- 'gr-related-collapse'
- );
- assert.isFalse(relatedChanges!.classList.contains('first'));
+ const relatedChanges = query<HTMLElement>(element, '#relatedChanges');
+ assert.notExists(relatedChanges);
const submittedTogetherSection = queryAndAssert<GrRelatedCollapse>(
queryAndAssert<HTMLElement>(element, '#submittedTogether'),
'gr-related-collapse'
@@ -255,11 +253,11 @@
'gr-related-collapse'
);
assert.isTrue(relatedChanges!.classList.contains('first'));
- const submittedTogetherSection = queryAndAssert<GrRelatedCollapse>(
- queryAndAssert<HTMLElement>(element, '#submittedTogether'),
- 'gr-related-collapse'
+ const submittedTogetherSection = query<HTMLElement>(
+ element,
+ '#submittedTogether'
);
- assert.isFalse(submittedTogetherSection!.classList.contains('first'));
+ assert.notExists(submittedTogetherSection);
const cherryPicks = queryAndAssert<GrRelatedCollapse>(
queryAndAssert<HTMLElement>(element, '#cherryPicks'),
'gr-related-collapse'
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
index 2992923..6406e3c 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
@@ -159,25 +159,8 @@
</tr>
</thead>
<tbody>
- ${submit_requirements.map(
- requirement => html`<tr
- id="requirement-${charsOnly(requirement.name)}"
- >
- <td>${this.renderStatus(requirement.status)}</td>
- <td class="name">
- <gr-limited-text
- class="name"
- limit="25"
- .text="${requirement.name}"
- ></gr-limited-text>
- </td>
- <td>
- <div class="votes-cell">
- ${this.renderVotes(requirement)}
- ${this.renderChecks(requirement)}
- </div>
- </td>
- </tr>`
+ ${submit_requirements.map(requirement =>
+ this.renderRequirement(requirement)
)}
</tbody>
</table>
@@ -195,6 +178,37 @@
${this.renderTriggerVotes()}`;
}
+ renderRequirement(requirement: SubmitRequirementResultInfo) {
+ return html`
+ <tr id="requirement-${charsOnly(requirement.name)}">
+ <td>${this.renderStatus(requirement.status)}</td>
+ <td class="name">
+ <gr-limited-text
+ class="name"
+ limit="25"
+ .text="${requirement.name}"
+ ></gr-limited-text>
+ </td>
+ <td>
+ <gr-endpoint-decorator
+ class="votes-cell"
+ name="${`submit-requirement-${charsOnly(requirement.name)}`}"
+ >
+ <gr-endpoint-param
+ name="change"
+ .value=${this.change}
+ ></gr-endpoint-param>
+ <gr-endpoint-param
+ name="requirement"
+ .value=${requirement}
+ ></gr-endpoint-param>
+ ${this.renderVotes(requirement)}${this.renderChecks(requirement)}
+ </gr-endpoint-decorator>
+ </td>
+ </tr>
+ `;
+ }
+
renderStatus(status: SubmitRequirementStatus) {
const icon = iconForStatus(status);
return html`<iron-icon
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
index f5072b9..a6176e1 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
@@ -41,7 +41,6 @@
import {DiffLayer, ParsedChangeInfo} from '../../../types/types';
import {GrButton} from '../../shared/gr-button/gr-button';
import {TokenHighlightLayer} from '../gr-diff-builder/token-highlight-layer';
-import {KnownExperimentId} from '../../../services/flags/flags';
export interface GrApplyFixDialog {
$: {
@@ -109,10 +108,7 @@
constructor() {
super();
this.restApiService.getPreferences().then(prefs => {
- if (
- !prefs?.disable_token_highlighting &&
- appContext.flagsService.isEnabled(KnownExperimentId.TOKEN_HIGHLIGHTING)
- ) {
+ if (!prefs?.disable_token_highlighting) {
this.layers = [new TokenHighlightLayer(this)];
}
});
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 de7d007..54b2450f 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
@@ -198,7 +198,6 @@
element,
} = this.findTokenAncestor(e?.target);
if (!newHighlight || newHighlight === this.currentHighlight) return;
- if (this.countOccurrences(newHighlight) <= 1) return;
this.hoveredElement = element;
this.updateTokenTask = debounce(
this.updateTokenTask,
@@ -247,13 +246,6 @@
return this.findTokenAncestor(el.parentElement);
}
- countOccurrences(token: string | undefined) {
- if (!token) return 0;
- const linesLeft = this.tokenToLinesLeft.get(token);
- const linesRight = this.tokenToLinesRight.get(token);
- return (linesLeft?.size ?? 0) + (linesRight?.size ?? 0);
- }
-
private updateTokenHighlight(
newHighlight: string | undefined,
newLineNumber: number,
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer_test.ts
index 2993d35..a0670b8 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer_test.ts
@@ -290,6 +290,34 @@
assert.deepEqual(tokenHighlightingCalls[1].details, undefined);
});
+ test('triggers listener on token with single occurrence', async () => {
+ const clock = sinon.useFakeTimers();
+ const line1 = createLine('a tokenWithSingleOccurence');
+ const line2 = createLine('can be highlighted', 2);
+ annotate(line1);
+ annotate(line2, Side.RIGHT, 2);
+ const tokenNode = queryAndAssert(line1, '.tk-tokenWithSingleOccurence');
+ assert.isTrue(tokenNode.classList.contains('token'));
+ dispatchMouseEvent(
+ 'mouseover',
+ MockInteractions.middleOfNode(tokenNode),
+ tokenNode
+ );
+ assert.equal(tokenHighlightingCalls.length, 0);
+ clock.tick(HOVER_DELAY_MS);
+ assert.equal(tokenHighlightingCalls.length, 1);
+ assert.deepEqual(tokenHighlightingCalls[0].details, {
+ token: 'tokenWithSingleOccurence',
+ side: Side.RIGHT,
+ element: tokenNode,
+ range: {start_line: 1, start_column: 3, end_line: 1, end_column: 26},
+ });
+
+ MockInteractions.click(container);
+ assert.equal(tokenHighlightingCalls.length, 2);
+ assert.deepEqual(tokenHighlightingCalls[1].details, undefined);
+ });
+
test('clicking clears highlight', async () => {
const clock = sinon.useFakeTimers();
const line1 = createLine('two words');
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
index a828b9c..025e477 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
@@ -336,9 +336,7 @@
const preferencesPromise = appContext.restApiService.getPreferences();
await getPluginLoader().awaitPluginsLoaded();
const prefs = await preferencesPromise;
- const enableTokenHighlight =
- appContext.flagsService.isEnabled(KnownExperimentId.TOKEN_HIGHLIGHTING) &&
- !prefs?.disable_token_highlighting;
+ const enableTokenHighlight = !prefs?.disable_token_highlighting;
assertIsDefined(this.path, 'path');
this._layers = this.getLayers(this.path, enableTokenHighlight);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
index 8901636..6facdca 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
@@ -1304,7 +1304,7 @@
test('gr-diff-host provides syntax highlighting layer', async () => {
stubRestApi('getDiff').returns(Promise.resolve({content: []}));
await element.reload();
- assert.equal(element.$.diff.layers[0], element.syntaxLayer);
+ assert.equal(element.$.diff.layers[1], element.syntaxLayer);
});
test('rendering normal-sized diff does not disable syntax', () => {
@@ -1358,7 +1358,7 @@
test('gr-diff-host provides syntax highlighting layer', async () => {
stubRestApi('getDiff').returns(Promise.resolve({content: []}));
await element.reload();
- assert.equal(element.$.diff.layers[0], element.syntaxLayer);
+ assert.equal(element.$.diff.layers[1], element.syntaxLayer);
});
test('syntax layer should be disabled', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
index d2c9de6..a47b685 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
@@ -185,7 +185,7 @@
mobileText: '3',
bottomText: '',
value: 3,
- date: '2021-03-30 01:02:03.000000000' as Timestamp,
+ date: '2020-02-01 01:02:03.000000000' as Timestamp,
} as DropdownItem,
{
disabled: true,
@@ -194,7 +194,7 @@
mobileText: '2',
bottomText: '',
value: 2,
- date: '2021-03-30 01:02:03.000000000' as Timestamp,
+ date: '2020-02-01 01:02:03.000000000' as Timestamp,
} as DropdownItem,
{
disabled: true,
@@ -203,7 +203,7 @@
mobileText: '1',
bottomText: '',
value: 1,
- date: '2021-03-30 01:02:03.000000000' as Timestamp,
+ date: '2020-02-01 01:02:03.000000000' as Timestamp,
} as DropdownItem,
{
text: 'Base',
@@ -332,7 +332,7 @@
mobileText: '3',
bottomText: '',
value: 3,
- date: '2021-03-30 01:02:03.000000000' as Timestamp,
+ date: '2020-02-01 01:02:03.000000000' as Timestamp,
} as DropdownItem,
{
disabled: false,
@@ -341,7 +341,7 @@
mobileText: '2 description',
bottomText: 'description',
value: 2,
- date: '2021-03-30 01:02:03.000000000' as Timestamp,
+ date: '2020-02-01 01:02:03.000000000' as Timestamp,
} as DropdownItem,
{
disabled: true,
@@ -350,7 +350,7 @@
mobileText: '1',
bottomText: '',
value: 1,
- date: '2021-03-30 01:02:03.000000000' as Timestamp,
+ date: '2020-02-01 01:02:03.000000000' as Timestamp,
} as DropdownItem,
];
diff --git a/polygerrit-ui/app/elements/diff/gr-range-header/gr-range-header.ts b/polygerrit-ui/app/elements/diff/gr-range-header/gr-range-header.ts
index dcf7236..8ce8ce2 100644
--- a/polygerrit-ui/app/elements/diff/gr-range-header/gr-range-header.ts
+++ b/polygerrit-ui/app/elements/diff/gr-range-header/gr-range-header.ts
@@ -55,7 +55,7 @@
override render() {
const icon = this.icon ?? '';
return html` <div class="row">
- <iron-icon class="icon" .icon=${icon}></iron-icon>
+ <iron-icon class="icon" .icon=${icon} aria-hidden="true"></iron-icon>
<slot></slot>
</div>`;
}
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 dabf761..0283ca4 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
@@ -261,7 +261,7 @@
${!this.hideStatus && account.status
? html`<iron-icon
class="status"
- icon="gr-icons:calendar"
+ icon="gr-icons:unavailable"
></iron-icon>`
: ''}
</span>
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
index 246dcd9..7ecced0 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
@@ -55,7 +55,6 @@
import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
import {FILE, LineNumber} from '../../diff/gr-diff/gr-diff-line';
import {GrButton} from '../gr-button/gr-button';
-import {KnownExperimentId} from '../../../services/flags/flags';
import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
import {DiffLayer, RenderPreferences} from '../../../api/diff';
import {
@@ -209,8 +208,6 @@
private readonly reporting = appContext.reportingService;
- private readonly flagsService = appContext.flagsService;
-
private readonly commentsService = appContext.commentsService;
readonly storage = appContext.storageService;
@@ -377,10 +374,7 @@
}
_initLayers(disableTokenHighlighting: boolean) {
- if (
- this.flagsService.isEnabled(KnownExperimentId.TOKEN_HIGHLIGHTING) &&
- !disableTokenHighlighting
- ) {
+ if (!disableTokenHighlighting) {
this.layers.push(new TokenHighlightLayer(this));
}
this.layers.push(this.syntaxLayer);
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
index 6a34fbb..e2ebb14 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
@@ -226,7 +226,7 @@
return html`
<div class="status">
<span class="title">
- <iron-icon icon="gr-icons:calendar"></iron-icon>
+ <iron-icon icon="gr-icons:unavailable"></iron-icon>
Status:
</span>
<span class="value">${this.account.status}</span>
diff --git a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
index 1a6239f..2533e00 100644
--- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
+++ b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
@@ -162,6 +162,8 @@
<g id="description"><path xmlns="http://www.w3.org/2000/svg" d="M0 0h24v24H0V0z" fill="none"/><path xmlns="http://www.w3.org/2000/svg" d="M8 16h8v2H8zm0-4h8v2H8zm6-10H6c-1.1 0-2 .9-2 2v16c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"/></g>
<!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=settings_backup_restore and 0.65 scale and 4 translate https://fonts.google.com/icons?selected=Material+Icons&icon.query=done-->
<g id="overridden"><path xmlns="http://www.w3.org/2000/svg" d="M0 0h24v24H0V0z" fill="none"/><path xmlns="http://www.w3.org/2000/svg" d="M12 15 zM2 4v6h6V8H5.09C6.47 5.61 9.04 4 12 4c4.42 0 8 3.58 8 8s-3.58 8-8 8-8-3.58-8-8H2c0 5.52 4.48 10 10.01 10C17.53 22 22 17.52 22 12S17.53 2 12.01 2C8.73 2 5.83 3.58 4 6.01V4H2z"/><path xmlns="http://www.w3.org/2000/svg" d="M9.85 14.53 7.12 11.8l-.91.91L9.85 16.35 17.65 8.55l-.91-.91L9.85 14.53z"/></g>
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons:event_busy -->
+ <g id="unavailable"><path d="M0 0h24v24H0z" fill="none"/><path d="M9.31 17l2.44-2.44L14.19 17l1.06-1.06-2.44-2.44 2.44-2.44L14.19 10l-2.44 2.44L9.31 10l-1.06 1.06 2.44 2.44-2.44 2.44L9.31 17zM19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11z"/></g>
</defs>
</svg>
</iron-iconset-svg>`;
diff --git a/polygerrit-ui/app/services/flags/flags.ts b/polygerrit-ui/app/services/flags/flags.ts
index 2839874..863f95f 100644
--- a/polygerrit-ui/app/services/flags/flags.ts
+++ b/polygerrit-ui/app/services/flags/flags.ts
@@ -25,7 +25,6 @@
*/
export enum KnownExperimentId {
NEW_IMAGE_DIFF_UI = 'UiFeature__new_image_diff_ui',
- TOKEN_HIGHLIGHTING = 'UiFeature__token_highlighting',
CHECKS_DEVELOPER = 'UiFeature__checks_developer',
SUBMIT_REQUIREMENTS_UI = 'UiFeature__submit_requirements_ui',
}
diff --git a/polygerrit-ui/app/test/test-data-generators.ts b/polygerrit-ui/app/test/test-data-generators.ts
index 584592e..dd56ce2 100644
--- a/polygerrit-ui/app/test/test-data-generators.ts
+++ b/polygerrit-ui/app/test/test-data-generators.ts
@@ -274,7 +274,7 @@
[revisionId: string]: RevisionInfo;
} {
const revisions: {[revisionId: string]: RevisionInfo} = {};
- const revisionDate = TEST_CHANGE_CREATED;
+ let revisionDate = TEST_CHANGE_CREATED;
const revisionIdStart = 1; // The same as getCurrentRevision
for (let i = 0; i < count; i++) {
const revisionId = (i + revisionIdStart).toString(16);
@@ -285,6 +285,7 @@
};
revisions[revisionId] = revision;
// advance 1 day
+ revisionDate = new Date(revisionDate);
revisionDate.setDate(revisionDate.getDate() + 1);
}
return revisions;
@@ -298,12 +299,13 @@
export function createChangeMessages(count: number): ChangeMessageInfo[] {
const messageIdStart = 1000;
const messages: ChangeMessageInfo[] = [];
- const messageDate = TEST_CHANGE_CREATED;
+ let messageDate = TEST_CHANGE_CREATED;
for (let i = 0; i < count; i++) {
messages.push({
...createChangeMessageInfo((i + messageIdStart).toString(16)),
date: dateToTimestamp(messageDate),
});
+ messageDate = new Date(messageDate);
messageDate.setDate(messageDate.getDate() + 1);
}
return messages;