Merge "Set "@typescript-eslint/no-floating-promises" to off in plugin eslint config"
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 5d40e2d..fadbad9 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -724,6 +724,22 @@
Valid relations are >=, >, \<=, <, or no relation, which will match if the number of unresolved
comments is exactly equal.
+[[unmet_requirement]]
+unmet_requirement:'SUBMIT_REQUIREMENT_NAME'::
++
+Matches changes where the given submit requirement is evaluated and unmet (its result is UNSATISFIED, ERROR, or TIMEOUT). The requirement name comparison is case-insensitive.
+
+[[unsatisfied_requirement_count]]
+unsatisfied_requirement_count:'RELATION''NUMBER'::
++
+True if the number of unsatisfied submit requirements satisfies the given relation for the given number.
++
+For example, unsatisfied_requirement_count:>0 will be true for any change which has at least one unsatisfied
+submit requirement while unsatisfied_requirement_count:0 will be true for any change which has all submit requirements resolved.
++
+Valid relations are >=, >, \<=, <, or no relation, which will match if the number of unsatisfied
+submit requirements is exactly equal.
+
== Argument Quoting
Operator values that are not bare words (roughly A-Z, a-z, 0-9, @,
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index e26165a..f7f1ce1 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -1674,6 +1674,51 @@
STORED_SUBMIT_REQUIREMENTS_SPEC =
STORED_SUBMIT_REQUIREMENTS_FIELD.storedOnly("full_submit_requirements");
+ /**
+ * Names of submit requirements that are not fulfilled for a change (i.e., whose evaluation status
+ * is {@link com.google.gerrit.entities.SubmitRequirementResult.Status#UNSATISFIED}, {@link
+ * com.google.gerrit.entities.SubmitRequirementResult.Status#ERROR}, or {@link
+ * com.google.gerrit.entities.SubmitRequirementResult.Status#TIMEOUT}).
+ *
+ * <p>Note that if evaluating a submit requirement results in an ERROR or TIMEOUT, it is
+ * considered unfulfilled and its name will be included in this field.
+ */
+ public static final IndexedField<ChangeData, Iterable<String>> UNMET_REQUIREMENT_FIELD =
+ IndexedField.<ChangeData>iterableStringBuilder("UnmetRequirement")
+ .build(
+ cd ->
+ cd.submitRequirementsIncludingLegacy().values().stream()
+ .filter(sr -> !sr.fulfilled())
+ .map(sr -> sr.submitRequirement().name().toLowerCase(Locale.US))
+ .collect(toImmutableSet()));
+
+ public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec UNMET_REQUIREMENT_SPEC =
+ UNMET_REQUIREMENT_FIELD.exact("unmet_requirement");
+
+ /**
+ * The number of submit requirements that are not fulfilled for a change (i.e., whose evaluation
+ * status is {@link com.google.gerrit.entities.SubmitRequirementResult.Status#UNSATISFIED}, {@link
+ * com.google.gerrit.entities.SubmitRequirementResult.Status#ERROR}, or {@link
+ * com.google.gerrit.entities.SubmitRequirementResult.Status#TIMEOUT}).
+ *
+ * <p>Note that if evaluating a submit requirement results in an ERROR or TIMEOUT, it is
+ * considered unfulfilled and is counted here.
+ */
+ public static final IndexedField<ChangeData, Integer> UNSATISFIED_REQUIREMENT_COUNT_FIELD =
+ IndexedField.<ChangeData>integerBuilder("UnsatisfiedRequirementCount")
+ .stored()
+ .build(
+ cd ->
+ (int)
+ cd.submitRequirementsIncludingLegacy().values().stream()
+ .filter(sr -> !sr.fulfilled())
+ .count(),
+ (cd, field) -> cd.setUnsatisfiedRequirementCount(field));
+
+ public static final IndexedField<ChangeData, Integer>.SearchSpec
+ UNSATISFIED_REQUIREMENT_COUNT_SPEC =
+ UNSATISFIED_REQUIREMENT_COUNT_FIELD.integerRange("unsatisfied_requirement_count");
+
private static void parseSubmitRequirements(
Iterable<Cache.SubmitRequirementResultProto> values, ChangeData out) {
out.setSubmitRequirements(
diff --git a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index aaa7535..809dd0a 100644
--- a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -274,6 +274,7 @@
@Deprecated static final Schema<ChangeData> V87 = schema(V86);
/** Add REVIEWERS_COUNT_FIELD */
+ @Deprecated
static final Schema<ChangeData> V88 =
new Schema.Builder<ChangeData>()
.add(V87)
@@ -281,6 +282,16 @@
.addSearchSpecs(ChangeField.REVIEWER_COUNT_SPEC)
.build();
+ /** Add met and unmet requirement tracking fields */
+ static final Schema<ChangeData> V89 =
+ new Schema.Builder<ChangeData>()
+ .add(V88)
+ .addIndexedFields(
+ ChangeField.UNMET_REQUIREMENT_FIELD, ChangeField.UNSATISFIED_REQUIREMENT_COUNT_FIELD)
+ .addSearchSpecs(
+ ChangeField.UNMET_REQUIREMENT_SPEC, ChangeField.UNSATISFIED_REQUIREMENT_COUNT_SPEC)
+ .build();
+
/**
* 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/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index a3b1e3e..6dee3eb 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -443,6 +443,7 @@
Maps.newLinkedHashMapWithExpectedSize(1);
private Map<SubmitRequirement, SubmitRequirementResult> submitRequirements;
+ private Integer unsatisfiedRequirementCount;
private StorageConstraint storageConstraint = StorageConstraint.NOTEDB_ONLY;
private Change change;
@@ -1307,6 +1308,14 @@
projectConfigReqs, legacyReqs, this);
}
+ public Integer unsatisfiedRequirementCount() {
+ return unsatisfiedRequirementCount;
+ }
+
+ public void setUnsatisfiedRequirementCount(Integer count) {
+ this.unsatisfiedRequirementCount = count;
+ }
+
/**
* Get all evaluated submit requirements for this change, including those from parent projects.
* For closed changes, submit requirements are read from the change notes. For active changes,
diff --git a/java/com/google/gerrit/server/query/change/ChangePredicates.java b/java/com/google/gerrit/server/query/change/ChangePredicates.java
index fdaa061..6331323 100644
--- a/java/com/google/gerrit/server/query/change/ChangePredicates.java
+++ b/java/com/google/gerrit/server/query/change/ChangePredicates.java
@@ -242,6 +242,12 @@
ChangeField.HASHTAG_SPEC, HashtagsUtil.cleanupHashtag(hashtag).toLowerCase(Locale.US));
}
+ /** Returns a predicate that matches changes that have an unmet submit requirement. */
+ public static Predicate<ChangeData> unmetRequirement(String requirementName) {
+ return new ChangeIndexPredicate(
+ ChangeField.UNMET_REQUIREMENT_SPEC, requirementName.toLowerCase(Locale.US));
+ }
+
/** Returns a predicate that matches changes tagged with the provided {@code hashtag}. */
public static Predicate<ChangeData> fuzzyHashtag(String hashtag) {
// Use toLowerCase without locale to match behavior in ChangeField.
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 6311c65..68530ef 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -1460,6 +1460,21 @@
}
@Operator
+ public Predicate<ChangeData> unmet_requirement(String requirementName)
+ throws QueryParseException {
+ checkFieldAvailable(ChangeField.UNMET_REQUIREMENT_SPEC, "unmet_requirement");
+ return ChangePredicates.unmetRequirement(requirementName);
+ }
+
+ @Operator
+ public Predicate<ChangeData> unsatisfied_requirement_count(String value)
+ throws QueryParseException {
+ checkFieldAvailable(
+ ChangeField.UNSATISFIED_REQUIREMENT_COUNT_SPEC, "unsatisfied_requirement_count");
+ return new UnsatisfiedRequirementCountPredicate(value);
+ }
+
+ @Operator
public Predicate<ChangeData> cc(String who)
throws QueryParseException, IOException, ConfigInvalidException {
return reviewerByState(who, ReviewerStateInternal.CC, false);
diff --git a/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java
index 26ccb2c..4ca53c5 100644
--- a/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java
@@ -131,6 +131,21 @@
}
@Override
+ public Predicate<ChangeData> unmet_requirement(String requirementName)
+ throws QueryParseException {
+ throw new QueryParseException(
+ "Operator 'unmet_requirement' cannot be used in submit requirement expressions.");
+ }
+
+ @Override
+ public Predicate<ChangeData> unsatisfied_requirement_count(String value)
+ throws QueryParseException {
+ throw new QueryParseException(
+ "Operator 'unsatisfied_requirement_count' cannot be used in submit requirement"
+ + " expressions.");
+ }
+
+ @Override
public Predicate<ChangeData> has(String value) throws QueryParseException {
if (value.toLowerCase(Locale.US).startsWith(SUBMODULE_UPDATE_HAS_ARG)) {
List<String> args = SUBMODULE_UPDATE_SPLITTER.splitToList(value);
diff --git a/java/com/google/gerrit/server/query/change/UnsatisfiedRequirementCountPredicate.java b/java/com/google/gerrit/server/query/change/UnsatisfiedRequirementCountPredicate.java
new file mode 100644
index 0000000..827558e
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/UnsatisfiedRequirementCountPredicate.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2026 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.query.change;
+
+import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.server.index.change.ChangeField;
+
+public class UnsatisfiedRequirementCountPredicate extends IntegerRangeChangePredicate {
+ public UnsatisfiedRequirementCountPredicate(String value) throws QueryParseException {
+ super(ChangeField.UNSATISFIED_REQUIREMENT_COUNT_SPEC, value);
+ }
+
+ @Override
+ protected Integer getValueInt(ChangeData changeData) {
+ Integer count = changeData.unsatisfiedRequirementCount();
+ if (count != null) {
+ return count;
+ }
+ return ChangeField.UNSATISFIED_REQUIREMENT_COUNT_SPEC.get(changeData);
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementIT.java b/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementIT.java
index 5bf1a40..b8d06ef 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementIT.java
@@ -2783,6 +2783,58 @@
}
@Test
+ public void submitRequirement_disallowsUnsatisfiedRequirementCountInExpression()
+ throws Exception {
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("Wrong-Req")
+ .setSubmittabilityExpression(
+ SubmitRequirementExpression.create("unsatisfied_requirement_count:0"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ SubmitRequirementResultInfo srResult =
+ change.submitRequirements.stream()
+ .filter(sr -> sr.name.equals("Wrong-Req"))
+ .collect(MoreCollectors.onlyElement());
+ assertThat(srResult.status).isEqualTo(Status.ERROR);
+ assertThat(srResult.submittabilityExpressionResult.errorMessage)
+ .isEqualTo(
+ "Operator 'unsatisfied_requirement_count' cannot be used in submit requirement"
+ + " expressions.");
+ }
+
+ @Test
+ public void submitRequirement_disallowsUnmetRequirementInExpression() throws Exception {
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("Wrong-Req")
+ .setSubmittabilityExpression(
+ SubmitRequirementExpression.create("unmet_requirement:Code-Review"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ SubmitRequirementResultInfo srResult =
+ change.submitRequirements.stream()
+ .filter(sr -> sr.name.equals("Wrong-Req"))
+ .collect(MoreCollectors.onlyElement());
+ assertThat(srResult.status).isEqualTo(Status.ERROR);
+ assertThat(srResult.submittabilityExpressionResult.errorMessage)
+ .isEqualTo(
+ "Operator 'unmet_requirement' cannot be used in submit requirement expressions.");
+ }
+
+ @Test
public void submitRequirements_forcedByDirectSubmission() throws Exception {
projectOperations
.project(project)
diff --git a/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java b/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
index 53431d1..6fcbf5d 100644
--- a/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
+++ b/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
@@ -105,6 +105,29 @@
}
@Test
+ public void unmetRequirementField() {
+ SubmitRequirementResult sr1 =
+ submitRequirementResult(
+ "Code-Review", "label:CR=+1", SubmitRequirementExpressionResult.Status.PASS);
+ SubmitRequirementResult sr2 =
+ submitRequirementResult(
+ "Verified", "label:V=+1", SubmitRequirementExpressionResult.Status.FAIL);
+
+ assertThat(sr1.fulfilled()).isTrue();
+ assertThat(sr2.fulfilled()).isFalse();
+
+ ChangeData cd = org.mockito.Mockito.mock(ChangeData.class);
+ org.mockito.Mockito.when(cd.submitRequirementsIncludingLegacy())
+ .thenReturn(
+ com.google.common.collect.ImmutableMap.of(
+ sr1.submitRequirement(), sr1,
+ sr2.submitRequirement(), sr2));
+
+ assertThat(ChangeField.UNMET_REQUIREMENT_FIELD.get(cd)).containsExactly("verified");
+ assertThat(ChangeField.UNSATISFIED_REQUIREMENT_COUNT_FIELD.get(cd)).isEqualTo(1);
+ }
+
+ @Test
public void storedSubmitRecords() {
assertStoredRecordRoundTrip(record(SubmitRecord.Status.CLOSED));
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 8d1e3dd..059be1b 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -4559,6 +4559,35 @@
}
@Test
+ public void bySubmitRequirement_unmet() throws Exception {
+ assume().that(getSchema().hasField(ChangeField.UNMET_REQUIREMENT_SPEC)).isTrue();
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 = insert(project, newChange(repo));
+ assertQuery("unmet_requirement:Code-Review", change1);
+
+ approve(change1);
+ assertQuery("unmet_requirement:Code-Review");
+ }
+
+ @Test
+ public void bySubmitRequirement_unsatisfiedCount() throws Exception {
+ assume().that(getSchema().hasField(ChangeField.UNSATISFIED_REQUIREMENT_COUNT_SPEC)).isTrue();
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 = insert(project, newChange(repo));
+ assertQuery("unsatisfied_requirement_count:>0", change1);
+ assertQuery("unsatisfied_requirement_count:1", change1);
+ assertQuery("unsatisfied_requirement_count:<=1", change1);
+ assertQuery("unmet_requirement:Code-Review", change1);
+
+ approve(change1);
+ assertQuery("unsatisfied_requirement_count:0", change1);
+ assertQuery("unsatisfied_requirement_count:<1", change1);
+ assertQuery("unmet_requirement:Code-Review");
+ }
+
+ @Test
public void byUrlEncodedProject() throws Exception {
Project.NameKey project = Project.nameKey("repo+foo");
repo = createAndOpenProject(project);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
index adfc532..9c7ec81 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
@@ -536,6 +536,7 @@
.headerLeft {
display: flex;
align-items: center;
+ min-width: 0;
}
gr-patch-range-select {
display: block;
@@ -571,7 +572,9 @@
margin-right: var(--spacing-m);
font-weight: var(--font-weight-medium);
white-space: nowrap;
- overflow: auto;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ flex-shrink: 100;
}
.patchRangeLeft {
align-items: center;
@@ -595,6 +598,8 @@
.jumpToFileContainer {
display: inline-block;
word-break: break-all;
+ min-width: 0;
+ overflow: hidden;
}
.mobile {
display: none;
@@ -920,11 +925,9 @@
>${this.changeNum}</a
><span class="changeNumberColon">:</span>
</div>
- <div>
- <span class="headerSubject"
- >${trimWithEllipsis(this.change?.subject, 80)}</span
- >
- </div>
+ <span class="headerSubject"
+ >${trimWithEllipsis(this.change?.subject, 80)}</span
+ >
<div class="checkboxDiv">
<md-checkbox
id="reviewed"
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
index 1d5a9b5..e19a98c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
@@ -246,9 +246,7 @@
<a href="/c/test-project/+/42"> 42 </a>
<span class="changeNumberColon"> : </span>
</div>
- <div>
- <span class="headerSubject"> Test subject </span>
- </div>
+ <span class="headerSubject"> Test subject </span>
<div class="checkboxDiv">
<md-checkbox
class="hideOnEdit reviewed"
@@ -421,6 +419,51 @@
);
});
+ test('nav links stay inside the header when space runs out', async () => {
+ element.change = {
+ ...createParsedChange(),
+ subject:
+ 'A change subject that is long enough to overflow a narrow header',
+ };
+ element.path =
+ 'polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts';
+ element.files = getFilesFromFileList([element.path]);
+ const forceDesktop = document.createElement('style');
+ forceDesktop.textContent = '.navLinks.desktop { display: flex; }';
+ element.shadowRoot!.appendChild(forceDesktop);
+ await element.updateComplete;
+
+ const header = queryAndAssert(element, 'header');
+ const navLinks = queryAndAssert(element, '.navLinks');
+ const subject = queryAndAssert(element, '.headerSubject');
+ assert.notEqual(getComputedStyle(navLinks).display, 'none');
+ const naturalSubjectWidth = subject.getBoundingClientRect().width;
+
+ element.style.width = '600px';
+ await element.updateComplete;
+
+ assert.isAtMost(
+ navLinks.getBoundingClientRect().right,
+ header.getBoundingClientRect().right,
+ 'nav links overflow the header'
+ );
+ assert.isAtMost(
+ header.scrollWidth,
+ header.clientWidth,
+ 'header content overflows the header box'
+ );
+ assert.isBelow(
+ subject.getBoundingClientRect().width,
+ naturalSubjectWidth,
+ 'subject should absorb the width deficit'
+ );
+ assert.isAbove(
+ subject.scrollWidth,
+ subject.clientWidth,
+ 'subject should be ellipsized rather than pushing the nav links out'
+ );
+ });
+
test('keyboard shortcuts', async () => {
clock = sinon.useFakeTimers({
toFake: ['Date'],