Add label score definition to the OWNERS file
That allows to configure a specific score value that is sufficient from
the OWNERS perspective to approve the file changes.
For instance the following OWNERS configuration:
inherited: true
label: Code-Review, 1
owners:
- group/Legal
matchers:
- exact: LICENSE
Means that in case when LICENSE file gets modified then someone from the
legal department has to give `Code-Review+1`. IOW `Code-Review+2`
doesn't have to granted to all project files.
Notes:
* label score definition is global for this file and applies to all
matching files (or files if there are not matchers specified)
* when the `inherited: true` and no label definition then it is
inherited from the parent projects (if specified)
* regular developers still have to give `Code-Review+2` for a
change to be submittable as default submit rule is still evaluated
* single digit (0..9) is allowed as the score value
Bug: Issue 15556
Change-Id: I76627192b8627139a04e1feb6eb13109d8f3717d
diff --git a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/ConfigurationParser.java b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/ConfigurationParser.java
index 95b63d2..2c8c619 100644
--- a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/ConfigurationParser.java
+++ b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/ConfigurationParser.java
@@ -44,7 +44,10 @@
Boolean inherited =
Optional.ofNullable(jsonNode.get("inherited")).map(JsonNode::asBoolean).orElse(true);
ret.setInherited(inherited);
- ret.setLabel(Optional.ofNullable(jsonNode.get("label")).map(JsonNode::asText));
+ ret.setLabel(
+ Optional.ofNullable(jsonNode.get("label"))
+ .map(JsonNode::asText)
+ .flatMap(LabelDefinition::parse));
addClassicMatcher(jsonNode, ret);
addMatchers(jsonNode, ret);
return Optional.of(ret);
diff --git a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/LabelDefinition.java b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/LabelDefinition.java
new file mode 100644
index 0000000..08664fe
--- /dev/null
+++ b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/LabelDefinition.java
@@ -0,0 +1,97 @@
+// Copyright (C) 2023 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.googlesource.gerrit.owners.common;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.LabelId;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Describes the label together with score (the latter is optional) that is configured in the OWNERS
+ * file. File owners have to give the score for change to be submittable.
+ */
+public class LabelDefinition {
+ public static final LabelDefinition CODE_REVIEW = new LabelDefinition(LabelId.CODE_REVIEW, null);
+
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private static final Pattern LABEL_PATTERN =
+ Pattern.compile("^([a-zA-Z0-9-]+)(?:(?:\\s*,\\s*)(\\d))?$");
+
+ private final String name;
+ private final Optional<Short> score;
+
+ LabelDefinition(String name, Short score) {
+ this.name = name;
+ this.score = Optional.ofNullable(score);
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Optional<Short> getScore() {
+ return score;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("LabelDefinition [name=");
+ builder.append(name);
+ builder.append(", score=");
+ builder.append(score);
+ builder.append("]");
+ return builder.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, score);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+
+ if ((obj == null) || getClass() != obj.getClass()) {
+ return false;
+ }
+
+ LabelDefinition other = (LabelDefinition) obj;
+ return Objects.equals(name, other.name) && Objects.equals(score, other.score);
+ }
+
+ public static Optional<LabelDefinition> parse(String definition) {
+ return Optional.ofNullable(definition)
+ .filter(def -> !def.isBlank())
+ .map(
+ def -> {
+ Matcher labelDef = LABEL_PATTERN.matcher(def.trim());
+ if (!labelDef.matches()) {
+ logger.atSevere().log("Parsing label definition [%s] has failed.", def);
+ return null;
+ }
+
+ return new LabelDefinition(
+ labelDef.group(1),
+ Optional.ofNullable(labelDef.group(2)).map(Short::valueOf).orElse(null));
+ });
+ }
+}
diff --git a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/OwnersConfig.java b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/OwnersConfig.java
index dc44ae4..7e7e705 100644
--- a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/OwnersConfig.java
+++ b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/OwnersConfig.java
@@ -38,7 +38,7 @@
private Map<String, Matcher> matchers = Maps.newHashMap();
/** Label that is required for submit. CodeReview if nothing is specified. */
- private Optional<String> label = Optional.empty();
+ private Optional<LabelDefinition> label = Optional.empty();
@Override
public String toString() {
@@ -85,11 +85,11 @@
return this.matchers.put(matcher.path, matcher);
}
- public void setLabel(Optional<String> label) {
+ public void setLabel(Optional<LabelDefinition> label) {
this.label = label;
}
- public Optional<String> getLabel() {
+ public Optional<LabelDefinition> getLabel() {
return label;
}
}
diff --git a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/OwnersMap.java b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/OwnersMap.java
index 7811bd6..760b8a6 100644
--- a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/OwnersMap.java
+++ b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/OwnersMap.java
@@ -31,7 +31,7 @@
private Map<String, Set<Account.Id>> fileOwners = Maps.newHashMap();
private Map<String, Set<Account.Id>> fileReviewers = Maps.newHashMap();
private Map<String, Set<String>> fileGroupOwners = Maps.newHashMap();
- private Optional<String> label = Optional.empty();
+ private Optional<LabelDefinition> label = Optional.empty();
@Override
public String toString() {
@@ -122,11 +122,11 @@
fileGroupOwners.computeIfAbsent(file, (f) -> Sets.newHashSet()).addAll(groupOwners);
}
- public Optional<String> getLabel() {
+ public Optional<LabelDefinition> getLabel() {
return label;
}
- public void setLabel(Optional<String> label) {
+ public void setLabel(Optional<LabelDefinition> label) {
this.label = label;
}
}
diff --git a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PathOwners.java b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PathOwners.java
index 9a28b04..8269ad6 100644
--- a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PathOwners.java
+++ b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PathOwners.java
@@ -90,7 +90,7 @@
private final boolean expandGroups;
- private final Optional<String> label;
+ private final Optional<LabelDefinition> label;
public PathOwners(
Accounts accounts,
@@ -186,7 +186,7 @@
return expandGroups;
}
- public Optional<String> getLabel() {
+ public Optional<LabelDefinition> getLabel() {
return label;
}
@@ -350,7 +350,7 @@
} else {
String ownersPath = partial + "OWNERS";
Optional<OwnersConfig> conf = getOwnersConfig(repository, ownersPath, branch);
- Optional<String> label = currentEntry.getLabel();
+ Optional<LabelDefinition> label = currentEntry.getLabel();
final Set<Id> owners = currentEntry.getOwners();
final Set<Id> reviewers = currentEntry.getReviewers();
Collection<Matcher> inheritedMatchers = currentEntry.getMatchers().values();
diff --git a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PathOwnersEntry.java b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PathOwnersEntry.java
index 03b4403..1fbcc80 100644
--- a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PathOwnersEntry.java
+++ b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PathOwnersEntry.java
@@ -33,7 +33,7 @@
*/
class PathOwnersEntry {
private final boolean inherited;
- private Optional<String> label;
+ private Optional<LabelDefinition> label;
private Set<Account.Id> owners = Sets.newHashSet();
private Set<Account.Id> reviewers = Sets.newHashSet();
private String ownersPath;
@@ -49,7 +49,7 @@
String path,
OwnersConfig config,
Accounts accounts,
- Optional<String> inheritedLabel,
+ Optional<LabelDefinition> inheritedLabel,
Set<Account.Id> inheritedOwners,
Set<Account.Id> inheritedReviewers,
Collection<Matcher> inheritedMatchers,
@@ -141,11 +141,11 @@
return inherited;
}
- public Optional<String> getLabel() {
+ public Optional<LabelDefinition> getLabel() {
return label;
}
- public void setLabel(Optional<String> label) {
+ public void setLabel(Optional<LabelDefinition> label) {
this.label = label;
}
diff --git a/owners-common/src/test/java/com/googlesource/gerrit/owners/common/LabelDefinitionTest.java b/owners-common/src/test/java/com/googlesource/gerrit/owners/common/LabelDefinitionTest.java
new file mode 100644
index 0000000..126e239
--- /dev/null
+++ b/owners-common/src/test/java/com/googlesource/gerrit/owners/common/LabelDefinitionTest.java
@@ -0,0 +1,59 @@
+// Copyright (C) 2023 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.googlesource.gerrit.owners.common;
+
+import static com.google.common.truth.Truth8.assertThat;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Optional;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class LabelDefinitionTest {
+ @Parameterized.Parameters
+ public static Collection<Object[]> labels() {
+ return Arrays.asList(
+ new Object[][] {
+ {null, Optional.empty()},
+ {"", Optional.empty()},
+ {"foo,", Optional.empty()},
+ {"foo", Optional.of(new LabelDefinition("foo", null))},
+ {"foo,1", Optional.of(new LabelDefinition("foo", (short) 1))},
+ {"foo, 1", Optional.of(new LabelDefinition("foo", (short) 1))},
+ {"foo , 1", Optional.of(new LabelDefinition("foo", (short) 1))},
+ {"foo ,1 ", Optional.of(new LabelDefinition("foo", (short) 1))}
+ });
+ }
+
+ private final String input;
+ private final Optional<LabelDefinition> expected;
+
+ public LabelDefinitionTest(String input, Optional<LabelDefinition> expected) {
+ this.input = input;
+ this.expected = expected;
+ }
+
+ @Test
+ public void shouldParseLabelDefinition() {
+ // when
+ Optional<LabelDefinition> result = LabelDefinition.parse(input);
+
+ // then
+ assertThat(result).isEqualTo(expected);
+ }
+}
diff --git a/owners-common/src/test/java/com/googlesource/gerrit/owners/common/PathOwnersTest.java b/owners-common/src/test/java/com/googlesource/gerrit/owners/common/PathOwnersTest.java
index 3f403a4..710a2b7 100644
--- a/owners-common/src/test/java/com/googlesource/gerrit/owners/common/PathOwnersTest.java
+++ b/owners-common/src/test/java/com/googlesource/gerrit/owners/common/PathOwnersTest.java
@@ -148,7 +148,7 @@
// expect that classic configuration takes precedence over `OWNERS` file for the label
// definition
- assertThat(owners2.getLabel()).hasValue(EXPECTED_LABEL);
+ assertThat(owners2.getLabel().map(LabelDefinition::getName)).hasValue(EXPECTED_LABEL);
}
@Test
@@ -183,7 +183,7 @@
assertEquals(2, ownersSet.size());
assertTrue(ownersSet.contains(USER_A_ID));
assertTrue(ownersSet.contains(USER_B_ID));
- assertThat(owners.getLabel()).hasValue(EXPECTED_LABEL);
+ assertThat(owners.getLabel().map(LabelDefinition::getName)).hasValue(EXPECTED_LABEL);
}
@Test
@@ -229,7 +229,7 @@
// expect that `master` configuration overwrites the label definition of both `refs/meta/config`
// and parent repo
- assertThat(owners.getLabel()).hasValue(EXPECTED_LABEL);
+ assertThat(owners.getLabel().map(LabelDefinition::getName)).hasValue(EXPECTED_LABEL);
}
@Test
@@ -278,7 +278,7 @@
assertTrue(ownersSet2.contains(USER_B_ID));
// expect that closer parent (parentRepository1) overwrites the label definition
- assertThat(owners.getLabel()).hasValue(EXPECTED_LABEL);
+ assertThat(owners.getLabel().map(LabelDefinition::getName)).hasValue(EXPECTED_LABEL);
}
private void mockParentRepository(Project.NameKey repositoryName, Repository repository)
@@ -315,19 +315,46 @@
assertTrue(ownersSet.contains(USER_C_ID));
// expect that more specific configuration overwrites the label definition
- assertThat(owners.getLabel()).hasValue(EXPECTED_LABEL);
+ assertThat(owners.getLabel().map(LabelDefinition::getName)).hasValue(EXPECTED_LABEL);
}
@Test
- public void testParsingYaml() {
+ public void testParsingYamlWithLabelWithScore() {
String yamlString =
- "inherited: true\nlabel: " + EXPECTED_LABEL + "\nowners:\n- " + USER_C_EMAIL_COM;
+ "inherited: true\nlabel: " + EXPECTED_LABEL + ",1\nowners:\n- " + USER_C_EMAIL_COM;
Optional<OwnersConfig> config = getOwnersConfig(yamlString);
+
assertTrue(config.isPresent());
+
OwnersConfig ownersConfig = config.get();
assertTrue(ownersConfig.isInherited());
assertThat(ownersConfig.getLabel()).isPresent();
- assertThat(ownersConfig.getLabel().get()).isEqualTo(EXPECTED_LABEL);
+
+ LabelDefinition label = ownersConfig.getLabel().get();
+ assertThat(label.getName()).isEqualTo(EXPECTED_LABEL);
+ assertThat(label.getScore()).hasValue(1);
+
+ Set<String> owners = ownersConfig.getOwners();
+ assertEquals(1, owners.size());
+ assertTrue(owners.contains(USER_C_EMAIL_COM));
+ }
+
+ @Test
+ public void testParsingYamlWithLabelWithoutScore() {
+ String yamlString =
+ "inherited: true\nlabel: " + EXPECTED_LABEL + "\nowners:\n- " + USER_C_EMAIL_COM;
+ Optional<OwnersConfig> config = getOwnersConfig(yamlString);
+
+ assertTrue(config.isPresent());
+
+ OwnersConfig ownersConfig = config.get();
+ assertTrue(ownersConfig.isInherited());
+ assertThat(ownersConfig.getLabel()).isPresent();
+
+ LabelDefinition label = ownersConfig.getLabel().get();
+ assertThat(label.getName()).isEqualTo(EXPECTED_LABEL);
+ assertThat(label.getScore()).isEmpty();
+
Set<String> owners = ownersConfig.getOwners();
assertEquals(1, owners.size());
assertTrue(owners.contains(USER_C_EMAIL_COM));
diff --git a/owners/src/main/java/com/googlesource/gerrit/owners/OwnersSubmitRequirement.java b/owners/src/main/java/com/googlesource/gerrit/owners/OwnersSubmitRequirement.java
index c93c3a4..35201fa 100644
--- a/owners/src/main/java/com/googlesource/gerrit/owners/OwnersSubmitRequirement.java
+++ b/owners/src/main/java/com/googlesource/gerrit/owners/OwnersSubmitRequirement.java
@@ -25,7 +25,6 @@
import com.google.gerrit.entities.Account.Id;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.LabelFunction;
-import com.google.gerrit.entities.LabelId;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.LabelTypes;
import com.google.gerrit.entities.LegacySubmitRequirement;
@@ -47,6 +46,7 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.googlesource.gerrit.owners.common.Accounts;
+import com.googlesource.gerrit.owners.common.LabelDefinition;
import com.googlesource.gerrit.owners.common.PathOwners;
import com.googlesource.gerrit.owners.common.PluginSettings;
import java.io.IOException;
@@ -142,8 +142,8 @@
ChangeNotes notes = cd.notes();
requireNonNull(notes, "notes");
LabelTypes labelTypes = projectState.getLabelTypes(notes);
- String label = resolveLabel(labelTypes, pathOwners.getLabel());
- LabelTypes ownersLabelType = ownersLabel(labelTypes, label, project);
+ LabelDefinition label = resolveLabel(labelTypes, pathOwners.getLabel());
+ Optional<LabelAndScore> ownersLabel = ownersLabel(labelTypes, label, project);
Account.Id uploader = notes.getCurrentPatchSet().uploader();
Map<Account.Id, List<PatchSetApproval>> approvalsByAccount =
Streams.stream(approvalsUtil.byPatchSet(notes, cd.currentPatchSet().id()))
@@ -153,8 +153,12 @@
fileOwners.entrySet().stream()
.filter(
requiredApproval ->
- isApprovalMissing(
- requiredApproval, uploader, approvalsByAccount, ownersLabelType))
+ ownersLabel
+ .map(
+ ol ->
+ isApprovalMissing(
+ requiredApproval, uploader, approvalsByAccount, ol))
+ .orElse(true))
.map(Map.Entry::getKey)
.collect(toSet());
@@ -162,7 +166,7 @@
missingApprovals.isEmpty()
? ok()
: notReady(
- label,
+ label.getName(),
String.format(
"Missing approvals for path(s): [%s]",
Joiner.on(", ").join(missingApprovals))));
@@ -181,82 +185,112 @@
* is configured in the OWNERS file then `Code-Review` will be selected.
*
* @param labelTypes labels configured for project
- * @param label that is configured in the OWNERS file
+ * @param label and score definition that is configured in the OWNERS file
*/
- static String resolveLabel(LabelTypes labelTypes, Optional<String> label) {
- return label.orElse(LabelId.CODE_REVIEW);
+ static LabelDefinition resolveLabel(LabelTypes labelTypes, Optional<LabelDefinition> label) {
+ return label.orElse(LabelDefinition.CODE_REVIEW);
}
/**
- * Create single label LabelTypes if label can be found or empty otherwise.
+ * Create {@link LabelAndScore} definition with a label LabelType if label can be found or empty
+ * otherwise. Note that score definition is copied from the OWNERS.
*
* @param labelTypes labels configured for project
- * @param label that is resolved from the OWNERS file
+ * @param label and score definition (optional) that is resolved from the OWNERS file
* @param project that change is evaluated for
*/
- static LabelTypes ownersLabel(LabelTypes labelTypes, String label, Project.NameKey project) {
- return new LabelTypes(
- labelTypes
- .byLabel(label)
- .map(Collections::singletonList)
- .orElseGet(
- () -> {
- logger.atSevere().log(
- "OWNERS label '%s' is not configured for '%s' project. Change is not submittable.",
- label, project);
- return Collections.emptyList();
- }));
+ static Optional<LabelAndScore> ownersLabel(
+ LabelTypes labelTypes, LabelDefinition label, Project.NameKey project) {
+ return labelTypes
+ .byLabel(label.getName())
+ .map(type -> new LabelAndScore(type, label.getScore()))
+ .or(
+ () -> {
+ logger.atSevere().log(
+ "OWNERS label '%s' is not configured for '%s' project. Change is not submittable.",
+ label, project);
+ return Optional.empty();
+ });
}
static boolean isApprovalMissing(
Map.Entry<String, Set<Account.Id>> requiredApproval,
Account.Id uploader,
Map<Account.Id, List<PatchSetApproval>> approvalsByAccount,
- LabelTypes labelTypes) {
+ LabelAndScore ownersLabel) {
return requiredApproval.getValue().stream()
.noneMatch(
- fileOwner -> isApprovedByOwner(fileOwner, uploader, approvalsByAccount, labelTypes));
+ fileOwner -> isApprovedByOwner(fileOwner, uploader, approvalsByAccount, ownersLabel));
}
static boolean isApprovedByOwner(
Account.Id fileOwner,
Account.Id uploader,
Map<Account.Id, List<PatchSetApproval>> approvalsByAccount,
- LabelTypes labelTypes) {
+ LabelAndScore ownersLabel) {
return Optional.ofNullable(approvalsByAccount.get(fileOwner))
.map(
approvals ->
approvals.stream()
.anyMatch(
approval ->
- hasSufficientApproval(approval, labelTypes, fileOwner, uploader)))
+ hasSufficientApproval(approval, ownersLabel, fileOwner, uploader)))
.orElse(false);
}
static boolean hasSufficientApproval(
- PatchSetApproval approval, LabelTypes labelTypes, Account.Id fileOwner, Account.Id uploader) {
- return labelTypes
- .byLabel(approval.labelId())
- .map(label -> isLabelApproved(label, fileOwner, uploader, approval))
- .orElse(false);
+ PatchSetApproval approval,
+ LabelAndScore ownersLabel,
+ Account.Id fileOwner,
+ Account.Id uploader) {
+ return ownersLabel.getLabelType().getLabelId().equals(approval.labelId())
+ && isLabelApproved(
+ ownersLabel.getLabelType(), ownersLabel.getScore(), fileOwner, uploader, approval);
}
static boolean isLabelApproved(
- LabelType label, Account.Id fileOwner, Account.Id uploader, PatchSetApproval approval) {
+ LabelType label,
+ Optional<Short> score,
+ Account.Id fileOwner,
+ Account.Id uploader,
+ PatchSetApproval approval) {
if (label.isIgnoreSelfApproval() && fileOwner.equals(uploader)) {
return false;
}
- LabelFunction function = label.getFunction();
- if (function.isMaxValueRequired()) {
- return label.isMaxPositive(approval);
+ return score
+ .map(value -> approval.value() >= value)
+ .orElseGet(
+ () -> {
+ LabelFunction function = label.getFunction();
+ if (function.isMaxValueRequired()) {
+ return label.isMaxPositive(approval);
+ }
+
+ if (function.isBlock() && label.isMaxNegative(approval)) {
+ return false;
+ }
+
+ return approval.value() > label.getDefaultValue();
+ });
+ }
+
+ static class LabelAndScore {
+ private final LabelType labelType;
+ private final Optional<Short> score;
+
+ LabelAndScore(LabelType labelType, Optional<Short> score) {
+ this.labelType = labelType;
+ this.score = score;
}
- if (function.isBlock() && label.isMaxNegative(approval)) {
- return false;
+ LabelType getLabelType() {
+ return labelType;
}
- return approval.value() > label.getDefaultValue();
+ Optional<Short> getScore() {
+ return score;
+ }
}
private Map<String, FileDiffOutput> getDiff(Project.NameKey project, ObjectId revision)
diff --git a/owners/src/main/resources/Documentation/config.md b/owners/src/main/resources/Documentation/config.md
index c71b761..62ac73c 100644
--- a/owners/src/main/resources/Documentation/config.md
+++ b/owners/src/main/resources/Documentation/config.md
@@ -45,7 +45,7 @@
```yaml
inherited: true
-label: Code-Review
+label: Code-Review, 1
owners:
- some.email@example.com
- User Name
@@ -94,6 +94,9 @@
> consideration only when `owners.enableSubmitRequirement = true`.
> Owners scores are matched against the label specified in the property in
> question.
+> The required label's score can be provided (by default label's scores
+> configuration is used) so that owners don't have to be granted with the
+> maximum label's score. Note that only single digit (0..9) is allowed.
For example, imagine the following tree:
@@ -197,7 +200,40 @@
### When `owners.enableSubmitRequirement = true`
-This case is not yet covered by the owners submit requirement implementation.
+This case is supported with the `Code-Review` label and `OWNERS` file
+modifications.
+
+The `OWNERS` file requires the label configuration to be added (here is the
+updated version):
+
+```yaml
+inherited: true
+label: Code-Review, 1
+owners:
+- John Doe
+- Doug Smith
+```
+
+But additionally one needs to modify the label on the particular project level
+to the following version:
+
+```
+[label "Code-Review"]
+ function = NoOp
+ defaultValue = 0
+ copyMinScore = true
+ copyAllScoresOnTrivialRebase = true
+ value = -2 This shall not be merged
+ value = -1 I would prefer this is not merged as is
+ value = 0 No score
+ value = +1 Looks good to me, but someone else must approve
+ value = +2 Looks good to me, approved
+```
+
+Note that `function` is set to `NoOp`.
+
+As a result Gerrit would make the change Submittable only if 'John Doe' or
+'Doug Smith' have provided at least a `Code-Review +1`.
### When `owners.enableSubmitRequirement = false` (default)
@@ -285,7 +321,30 @@
A change cannot be submitted until John Doe or Doug Smith add a label
"Owner-Approved", independently from being able to provide any Code-Review.
-## Example 4 - Owners based on matchers
+## Example 4 - OWNERS file without matchers, default Gerrit submit rules and owners Code-Review +1 required
+
+This is a variant of `example 3` when no additional label is created but owners
+shouldn't be granted with `Code-Review +2` for all project files as it might be
+outside of their comptenence/comfort zone.
+
+### When `owners.enableSubmitRequirement = true`
+
+Given an OWNERS configuration of:
+
+```yaml
+inherited: true
+label: Code-Review, 1
+owners:
+- John Doe
+- Doug Smith
+```
+
+A change cannot be submitted until 'John Doe' or 'Doug Smith' add
+`Code-Review+1` score. Note that regular developers still need to cast the
+`Code-Review+2` for a change to be submittable as default submit rule is
+still evaluated.
+
+## Example 5 - Owners based on matchers
Often the ownership comes from the developer's skills and competencies and
cannot be purely defined by the project's directory structure.
@@ -335,7 +394,7 @@
(Mister Dba) and the .css files (either John Creative or Matt Designer) have
provided their Code-Review +2 feedback.
-## Example 5 - Owners details on a per-file basis
+## Example 6 - Owners details on a per-file basis
### When `owners.enableSubmitRequirement = true`
diff --git a/owners/src/test/java/com/googlesource/gerrit/owners/OwnersSubmitRequirementIT.java b/owners/src/test/java/com/googlesource/gerrit/owners/OwnersSubmitRequirementIT.java
index 73d4e3a..c393ebc 100644
--- a/owners/src/test/java/com/googlesource/gerrit/owners/OwnersSubmitRequirementIT.java
+++ b/owners/src/test/java/com/googlesource/gerrit/owners/OwnersSubmitRequirementIT.java
@@ -43,12 +43,11 @@
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
import com.google.gerrit.server.project.ProjectConfig;
+import com.google.gerrit.server.project.testing.TestLabels;
import com.google.inject.Inject;
-import java.io.IOException;
+import com.googlesource.gerrit.owners.common.LabelDefinition;
import java.util.Collection;
import java.util.stream.Stream;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.junit.Test;
@TestPlugin(name = "owners", sysModule = "com.googlesource.gerrit.owners.OwnersModule")
@@ -230,7 +229,7 @@
public void shouldRequireConfiguredLabelByCodeOwner() throws Exception {
TestAccount admin2 = accountCreator.admin2();
String labelId = "Foo";
- addOwnerFileToRoot(true, labelId, admin2);
+ addOwnerFileToRoot(true, LabelDefinition.parse(labelId).get(), admin2);
installLabel(labelId);
@@ -261,7 +260,7 @@
throws Exception {
TestAccount admin2 = accountCreator.admin2();
String notExistinglabelId = "Foo";
- addOwnerFileToRoot(true, notExistinglabelId, admin2);
+ addOwnerFileToRoot(true, LabelDefinition.parse(notExistinglabelId).get(), admin2);
PushOneCommit.Result r = createChange("Add a file", "foo", "bar");
ChangeApi changeApi = forChange(r);
@@ -274,6 +273,94 @@
assertThat(changeStillNotReady.requirements).containsExactly(NOT_READY);
}
+ @Test
+ @GlobalPluginConfig(
+ pluginName = "owners",
+ name = "owners.enableSubmitRequirement",
+ value = "true")
+ public void shouldRequireConfiguredLabelScoreByCodeOwner() throws Exception {
+ TestAccount admin2 = accountCreator.admin2();
+ addOwnerFileToRoot(true, LabelDefinition.parse("Code-Review,1").get(), admin2);
+
+ PushOneCommit.Result r = createChange("Add a file", "foo", "bar");
+ ChangeApi changeApi = forChange(r);
+ ChangeInfo changeNotReady = changeApi.get();
+ assertThat(changeNotReady.submittable).isFalse();
+ assertThat(changeNotReady.requirements).containsExactly(NOT_READY);
+
+ changeApi.current().review(ReviewInput.approve());
+ ChangeInfo changeNotReadyAfterSelfApproval = changeApi.get();
+ assertThat(changeNotReadyAfterSelfApproval.submittable).isFalse();
+ assertThat(changeNotReadyAfterSelfApproval.requirements).containsExactly(NOT_READY);
+
+ requestScopeOperations.setApiUser(admin2.id());
+ forChange(r).current().review(ReviewInput.recommend());
+ ChangeInfo changeReadyWithOwnerScore = forChange(r).get();
+ assertThat(changeReadyWithOwnerScore.submittable).isTrue();
+ assertThat(changeReadyWithOwnerScore.requirements).containsExactly(READY);
+ }
+
+ @Test
+ @GlobalPluginConfig(
+ pluginName = "owners",
+ name = "owners.enableSubmitRequirement",
+ value = "true")
+ public void shouldConfiguredLabelScoreByCodeOwnerBeNotSufficientIfLabelRequiresMaxValue()
+ throws Exception {
+ TestAccount admin2 = accountCreator.admin2();
+ addOwnerFileToRoot(true, LabelDefinition.parse("Code-Review,1").get(), admin2);
+
+ PushOneCommit.Result r = createChange("Add a file", "foo", "bar");
+ ChangeApi changeApi = forChange(r);
+ ChangeInfo changeNotReady = changeApi.get();
+ assertThat(changeNotReady.submittable).isFalse();
+ assertThat(changeNotReady.requirements).containsExactly(NOT_READY);
+
+ requestScopeOperations.setApiUser(admin2.id());
+ forChange(r).current().review(ReviewInput.recommend());
+ ChangeInfo ownersVoteNotSufficient = changeApi.get();
+ assertThat(ownersVoteNotSufficient.submittable).isFalse();
+ assertThat(ownersVoteNotSufficient.requirements).containsExactly(READY);
+ verifyHasSubmitRecord(
+ ownersVoteNotSufficient.submitRecords,
+ LabelId.CODE_REVIEW,
+ SubmitRecordInfo.Label.Status.NEED);
+
+ requestScopeOperations.setApiUser(admin.id());
+ forChange(r).current().review(ReviewInput.approve());
+ ChangeInfo changeReadyWithMaxScore = forChange(r).get();
+ assertThat(changeReadyWithMaxScore.submittable).isTrue();
+ assertThat(changeReadyWithMaxScore.requirements).containsExactly(READY);
+ verifyHasSubmitRecord(
+ changeReadyWithMaxScore.submitRecords,
+ LabelId.CODE_REVIEW,
+ SubmitRecordInfo.Label.Status.OK);
+ }
+
+ @Test
+ @GlobalPluginConfig(
+ pluginName = "owners",
+ name = "owners.enableSubmitRequirement",
+ value = "true")
+ public void shouldConfiguredLabelScoreByCodeOwnersOverwriteSubmitRequirement() throws Exception {
+ installLabel(TestLabels.codeReview().toBuilder().setFunction(LabelFunction.NO_OP).build());
+
+ TestAccount admin2 = accountCreator.admin2();
+ addOwnerFileToRoot(true, LabelDefinition.parse("Code-Review,1").get(), admin2);
+
+ PushOneCommit.Result r = createChange("Add a file", "foo", "bar");
+ ChangeApi changeApi = forChange(r);
+ ChangeInfo changeNotReady = changeApi.get();
+ assertThat(changeNotReady.submittable).isFalse();
+ assertThat(changeNotReady.requirements).containsExactly(NOT_READY);
+
+ requestScopeOperations.setApiUser(admin2.id());
+ forChange(r).current().review(ReviewInput.recommend());
+ ChangeInfo ownersVoteSufficient = forChange(r).get();
+ assertThat(ownersVoteSufficient.submittable).isTrue();
+ assertThat(ownersVoteSufficient.requirements).containsExactly(READY);
+ }
+
private void verifyHasSubmitRecord(
Collection<SubmitRecordInfo> records, String label, SubmitRecordInfo.Label.Status status) {
assertThat(
@@ -284,24 +371,17 @@
.isPresent();
}
- private void installVerifiedLabel()
- throws RepositoryNotFoundException, IOException, ConfigInvalidException {
+ private void installVerifiedLabel() throws Exception {
installLabel(LabelId.VERIFIED);
}
- private void installLabel(String labelId)
- throws IOException, ConfigInvalidException, RepositoryNotFoundException {
+ private void installLabel(String labelId) throws Exception {
LabelType verified =
labelBuilder(labelId, value(1, "Verified"), value(0, "No score"), value(-1, "Fails"))
.setFunction(LabelFunction.MAX_WITH_BLOCK)
.build();
- try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
- ProjectConfig cfg = projectConfigFactory.create(project);
- cfg.load(md);
- cfg.upsertLabelType(verified);
- cfg.commit(md);
- }
- projectCache.evictAndReindex(project);
+
+ installLabel(verified);
String heads = RefNames.REFS_HEADS + "*";
projectOperations
@@ -311,6 +391,16 @@
.update();
}
+ private void installLabel(LabelType label) throws Exception {
+ try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
+ ProjectConfig cfg = projectConfigFactory.create(project);
+ cfg.load(md);
+ cfg.upsertLabelType(label);
+ cfg.commit(md);
+ }
+ projectCache.evictAndReindex(project);
+ }
+
private ChangeApi forChange(PushOneCommit.Result r) throws RestApiException {
return gApi.changes().id(r.getChangeId());
}
@@ -358,11 +448,12 @@
""));
}
- private void addOwnerFileToRoot(boolean inherit, String label, TestAccount u) throws Exception {
+ private void addOwnerFileToRoot(boolean inherit, LabelDefinition label, TestAccount u)
+ throws Exception {
// Add OWNERS file to root:
//
// inherited: true
- // label: label
+ // label: label,score # score is optional
// owners:
// - u.email()
merge(
@@ -371,7 +462,14 @@
"master",
"Add OWNER file",
"OWNERS",
- String.format("inherited: %s\nlabel: %s\nowners:\n- %s\n", inherit, label, u.email()),
+ String.format(
+ "inherited: %s\nlabel: %s\nowners:\n- %s\n",
+ inherit,
+ String.format(
+ "%s%s",
+ label.getName(),
+ label.getScore().map(value -> String.format(",%d", value)).orElse("")),
+ u.email()),
""));
}
}
diff --git a/owners/src/test/java/com/googlesource/gerrit/owners/OwnersSubmitRequirementTest.java b/owners/src/test/java/com/googlesource/gerrit/owners/OwnersSubmitRequirementTest.java
index f8b8870..97113c0 100644
--- a/owners/src/test/java/com/googlesource/gerrit/owners/OwnersSubmitRequirementTest.java
+++ b/owners/src/test/java/com/googlesource/gerrit/owners/OwnersSubmitRequirementTest.java
@@ -24,6 +24,7 @@
import static com.googlesource.gerrit.owners.OwnersSubmitRequirement.isApprovalMissing;
import static com.googlesource.gerrit.owners.OwnersSubmitRequirement.isApprovedByOwner;
import static com.googlesource.gerrit.owners.OwnersSubmitRequirement.isLabelApproved;
+import static com.googlesource.gerrit.owners.OwnersSubmitRequirement.ownersLabel;
import static com.googlesource.gerrit.owners.OwnersSubmitRequirement.resolveLabel;
import static org.mockito.Mockito.mock;
@@ -35,6 +36,8 @@
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.Project;
+import com.googlesource.gerrit.owners.OwnersSubmitRequirement.LabelAndScore;
+import com.googlesource.gerrit.owners.common.LabelDefinition;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.Arrays;
@@ -47,53 +50,54 @@
public class OwnersSubmitRequirementTest {
private static String LABEL_ID = "foo";
+ private static LabelDefinition OWNERS_LABEL = LabelDefinition.parse(LABEL_ID).get();
+ private static LabelDefinition OWNERS_LABEL_WITH_SCORE =
+ LabelDefinition.parse(String.format("%s,1", LABEL_ID)).get();
private static int MAX_LABEL_VALUE = 1;
private static Project.NameKey PROJECT = Project.nameKey("project");
@Test
public void shouldResolveLabelToConfiguredOne() {
// when
- String label = resolveLabel(null, Optional.of(LABEL_ID));
+ LabelDefinition label = resolveLabel(null, Optional.of(OWNERS_LABEL));
// then
- assertThat(label).isEqualTo(LABEL_ID);
+ assertThat(label).isEqualTo(OWNERS_LABEL);
}
@Test
- public void shouldResolveLabelToCodeReviewIfProjectHasCodeReviewLabelConfigured() {
+ public void shouldResolveLabelToCodeReview() {
// given
LabelTypes types =
- new LabelTypes(
- List.of(verified(), label().setFunction(LabelFunction.NO_BLOCK).build(), codeReview()));
+ new LabelTypes(List.of(verified(), label().setFunction(LabelFunction.NO_BLOCK).build()));
// when
- String label = resolveLabel(types, Optional.empty());
+ LabelDefinition label = resolveLabel(types, Optional.empty());
// then
- assertThat(label).isEqualTo(LabelId.CODE_REVIEW);
+ assertThat(label).isEqualTo(LabelDefinition.CODE_REVIEW);
}
@Test
- public void shouldOwnersLabelContainOnlyConfiguredLabel() {
+ public void shouldOwnersLabelContainOnlyConfiguredLabelAndItsScore() {
// when
- LabelTypes result =
- OwnersSubmitRequirement.ownersLabel(
- new LabelTypes(List.of(label().build())), LABEL_ID, PROJECT);
+ Optional<LabelAndScore> result =
+ ownersLabel(new LabelTypes(List.of(label().build())), OWNERS_LABEL_WITH_SCORE, PROJECT);
// then
- assertThat(result.getLabelTypes()).hasSize(1);
- assertThat(result.byLabel(LABEL_ID)).isPresent();
+ assertThat(result.map(label -> label.getLabelType().getName())).hasValue(LABEL_ID);
+ assertThat(result.flatMap(LabelAndScore::getScore))
+ .isEqualTo(OWNERS_LABEL_WITH_SCORE.getScore());
}
@Test
public void shouldOwnersLabelBeEmptyIfNonExistingLabelIsConfigured() {
// when
- LabelTypes result =
- OwnersSubmitRequirement.ownersLabel(
- new LabelTypes(List.of(codeReview())), LABEL_ID, PROJECT);
+ Optional<LabelAndScore> result =
+ ownersLabel(new LabelTypes(List.of(codeReview())), OWNERS_LABEL, PROJECT);
// then
- assertThat(result.getLabelTypes()).isEmpty();
+ assertThat(result).isEmpty();
}
@Test
@@ -101,14 +105,14 @@
// given
Account.Id fileOwner = mock(Account.Id.class);
Account.Id uploader = mock(Account.Id.class);
- LabelTypes labelTypes = maxNoBlockLabelFooTypes();
+ LabelAndScore ownersLabel = maxNoBlockLabelFooOwnersLabel();
Map<Account.Id, List<PatchSetApproval>> uploaderApproval =
Map.of(uploader, List.of(approvedBy(uploader, LABEL_ID, MAX_LABEL_VALUE)));
// when
boolean isApprovalMissing =
isApprovalMissing(
- Map.entry("path", Set.of(fileOwner)), uploader, uploaderApproval, labelTypes);
+ Map.entry("path", Set.of(fileOwner)), uploader, uploaderApproval, ownersLabel);
// then
assertThat(isApprovalMissing).isTrue();
@@ -119,14 +123,14 @@
// given
Account.Id fileOwner = mock(Account.Id.class);
Account.Id uploader = mock(Account.Id.class);
- LabelTypes labelTypes = maxNoBlockLabelFooTypes();
+ LabelAndScore ownersLabel = maxNoBlockLabelFooOwnersLabel();
Map<Account.Id, List<PatchSetApproval>> fileOwnerApproval =
Map.of(fileOwner, List.of(approvedBy(fileOwner, LABEL_ID, MAX_LABEL_VALUE)));
// when
boolean isApprovalMissing =
isApprovalMissing(
- Map.entry("path", Set.of(fileOwner)), uploader, fileOwnerApproval, labelTypes);
+ Map.entry("path", Set.of(fileOwner)), uploader, fileOwnerApproval, ownersLabel);
// then
assertThat(isApprovalMissing).isFalse();
@@ -138,7 +142,7 @@
Account.Id fileOwnerA = mock(Account.Id.class);
Account.Id fileOwnerB = mock(Account.Id.class);
Account.Id uploader = mock(Account.Id.class);
- LabelTypes labelTypes = maxNoBlockLabelFooTypes();
+ LabelAndScore ownersLabel = maxNoBlockLabelFooOwnersLabel();
Map<Account.Id, List<PatchSetApproval>> fileOwnerApproval =
Map.of(fileOwnerA, List.of(approvedBy(fileOwnerA, LABEL_ID, MAX_LABEL_VALUE)));
@@ -148,7 +152,7 @@
Map.entry("path", Set.of(fileOwnerA, fileOwnerB)),
uploader,
fileOwnerApproval,
- labelTypes);
+ ownersLabel);
// then
assertThat(isApprovalMissing).isFalse();
@@ -159,12 +163,13 @@
// given
Account.Id fileOwner = mock(Account.Id.class);
Account.Id uploader = mock(Account.Id.class);
- LabelTypes labelTypes = maxNoBlockLabelFooTypes();
+ LabelAndScore ownersLabel = maxNoBlockLabelFooOwnersLabel();
Map<Account.Id, List<PatchSetApproval>> uploaderApproval =
Map.of(uploader, List.of(approvedBy(uploader, LABEL_ID, MAX_LABEL_VALUE)));
// when
- boolean approvedByOwner = isApprovedByOwner(fileOwner, fileOwner, uploaderApproval, labelTypes);
+ boolean approvedByOwner =
+ isApprovedByOwner(fileOwner, fileOwner, uploaderApproval, ownersLabel);
// then
assertThat(approvedByOwner).isFalse();
@@ -174,15 +179,16 @@
public void shouldNotBeApprovedWhenApprovalGivenForDifferentLabel() {
// given
Account.Id fileOwner = mock(Account.Id.class);
- LabelTypes labelTypes =
- new LabelTypes(
- List.of(label().setName("bar").setFunction(LabelFunction.MAX_NO_BLOCK).build()));
+ LabelAndScore ownersLabel =
+ new LabelAndScore(
+ label().setName("bar").setFunction(LabelFunction.MAX_NO_BLOCK).build(),
+ Optional.empty());
Map<Account.Id, List<PatchSetApproval>> fileOwnerForDifferentLabelApproval =
Map.of(fileOwner, List.of(approvedBy(fileOwner, LABEL_ID, MAX_LABEL_VALUE)));
// when
boolean approvedByOwner =
- isApprovedByOwner(fileOwner, fileOwner, fileOwnerForDifferentLabelApproval, labelTypes);
+ isApprovedByOwner(fileOwner, fileOwner, fileOwnerForDifferentLabelApproval, ownersLabel);
// then
assertThat(approvedByOwner).isFalse();
@@ -192,13 +198,13 @@
public void shouldBeApprovedByOwner() {
// given
Account.Id fileOwner = mock(Account.Id.class);
- LabelTypes labelTypes = maxNoBlockLabelFooTypes();
+ LabelAndScore ownersLabel = maxNoBlockLabelFooOwnersLabel();
Map<Account.Id, List<PatchSetApproval>> fileOwnerApproval =
Map.of(fileOwner, List.of(approvedBy(fileOwner, LABEL_ID, MAX_LABEL_VALUE)));
// when
boolean approvedByOwner =
- isApprovedByOwner(fileOwner, fileOwner, fileOwnerApproval, labelTypes);
+ isApprovedByOwner(fileOwner, fileOwner, fileOwnerApproval, ownersLabel);
// then
assertThat(approvedByOwner).isTrue();
@@ -207,13 +213,13 @@
@Test
public void shouldHaveNotSufficientApprovalWhenLabelIsNotApproved() {
// given
- LabelType maxValueRequired = label().setFunction(LabelFunction.MAX_NO_BLOCK).build();
Account.Id fileOwner = mock(Account.Id.class);
- LabelTypes labelTypes = new LabelTypes(List.of(maxValueRequired));
+ LabelAndScore ownersLabel = maxNoBlockLabelFooOwnersLabel();
// when
boolean hasSufficientApproval =
- hasSufficientApproval(approvedBy(fileOwner, LABEL_ID, 0), labelTypes, fileOwner, fileOwner);
+ hasSufficientApproval(
+ approvedBy(fileOwner, LABEL_ID, 0), ownersLabel, fileOwner, fileOwner);
// then
assertThat(hasSufficientApproval).isFalse();
@@ -223,11 +229,15 @@
public void shouldHaveNotSufficientApprovalWhenLabelDoesntMatch() {
// given
Account.Id fileOwner = mock(Account.Id.class);
- LabelTypes labelTypes = new LabelTypes(Collections.emptyList());
// when
boolean hasSufficientApproval =
- hasSufficientApproval(approvedBy(fileOwner, LABEL_ID, 0), labelTypes, fileOwner, fileOwner);
+ hasSufficientApproval(
+ approvedBy(fileOwner, LABEL_ID, 0),
+ new LabelAndScore(
+ LabelType.builder("foo", Collections.emptyList()).build(), Optional.empty()),
+ fileOwner,
+ fileOwner);
// then
assertThat(hasSufficientApproval).isFalse();
@@ -236,14 +246,13 @@
@Test
public void shouldHaveSufficientApprovalWhenLabelIsApproved() {
// given
- LabelType maxValueRequired = label().setFunction(LabelFunction.MAX_NO_BLOCK).build();
Account.Id fileOwner = mock(Account.Id.class);
- LabelTypes labelTypes = new LabelTypes(List.of(maxValueRequired));
+ LabelAndScore ownersLabel = maxNoBlockLabelFooOwnersLabel();
// when
boolean hasSufficientApproval =
hasSufficientApproval(
- approvedBy(fileOwner, LABEL_ID, MAX_LABEL_VALUE), labelTypes, fileOwner, fileOwner);
+ approvedBy(fileOwner, LABEL_ID, MAX_LABEL_VALUE), ownersLabel, fileOwner, fileOwner);
// then
assertThat(hasSufficientApproval).isTrue();
@@ -259,6 +268,7 @@
boolean approved =
isLabelApproved(
ignoreSelfApproval,
+ Optional.empty(),
fileOwner,
fileOwner,
approvedBy(fileOwner, LABEL_ID, MAX_LABEL_VALUE));
@@ -275,7 +285,12 @@
// when
boolean approved =
- isLabelApproved(maxValueRequired, fileOwner, fileOwner, approvedBy(fileOwner, LABEL_ID, 0));
+ isLabelApproved(
+ maxValueRequired,
+ Optional.empty(),
+ fileOwner,
+ fileOwner,
+ approvedBy(fileOwner, LABEL_ID, 0));
// then
assertThat(approved).isFalse();
@@ -291,6 +306,7 @@
boolean approved =
isLabelApproved(
maxValueRequired,
+ Optional.empty(),
fileOwner,
fileOwner,
approvedBy(fileOwner, LABEL_ID, MAX_LABEL_VALUE));
@@ -300,6 +316,59 @@
}
@Test
+ public void labelShouldBeApprovedWhenMaxValueIsRequiredButLowerScoreIsConfiguredForOwner() {
+ // given
+ LabelType maxValueRequired = codeReview();
+ Short ownersScore = 1;
+ Optional<Short> requiredOwnerScore = Optional.of(ownersScore);
+ Account.Id fileOwner = mock(Account.Id.class);
+
+ // when
+ boolean approved =
+ isLabelApproved(
+ maxValueRequired,
+ requiredOwnerScore,
+ fileOwner,
+ fileOwner,
+ approvedBy(fileOwner, LabelId.CODE_REVIEW, ownersScore));
+
+ // then
+ assertThat(approved).isTrue();
+ }
+
+ @Test
+ public void
+ labelShouldNotBeApprovedWhenMaxValueIsRequiredLowerScoreIsConfiguredForOwnerAndTooLowScoreIsCast() {
+ // given
+ LabelType anyWithBlock =
+ label()
+ .setValues(
+ Arrays.asList(
+ value(3, "Great"),
+ value(2, "Approved"),
+ value(1, "Maybe"),
+ value(0, "No score"),
+ value(-1, "Blocked")))
+ .setFunction(LabelFunction.ANY_WITH_BLOCK)
+ .build();
+ Optional<Short> requiredOwnerScore = Optional.of((short) 2);
+ Short ownersScore = 1;
+ Account.Id fileOwner = mock(Account.Id.class);
+
+ // when
+ boolean approved =
+ isLabelApproved(
+ anyWithBlock,
+ requiredOwnerScore,
+ fileOwner,
+ fileOwner,
+ approvedBy(fileOwner, LabelId.CODE_REVIEW, ownersScore));
+
+ // then
+ assertThat(approved).isFalse();
+ }
+
+ @Test
public void labelShouldNotBeApprovedWhenAnyValueWithBlockIsConfiguredAndMaxNegativeIsProvided() {
// given
LabelType anyWithBlock = label().setFunction(LabelFunction.ANY_WITH_BLOCK).build();
@@ -307,7 +376,12 @@
// when
boolean approved =
- isLabelApproved(anyWithBlock, fileOwner, fileOwner, approvedBy(fileOwner, LABEL_ID, -1));
+ isLabelApproved(
+ anyWithBlock,
+ Optional.empty(),
+ fileOwner,
+ fileOwner,
+ approvedBy(fileOwner, LABEL_ID, -1));
// then
assertThat(approved).isFalse();
@@ -330,7 +404,12 @@
// when
boolean approved =
- isLabelApproved(anyWithBlock, fileOwner, fileOwner, approvedBy(fileOwner, LABEL_ID, 1));
+ isLabelApproved(
+ anyWithBlock,
+ Optional.empty(),
+ fileOwner,
+ fileOwner,
+ approvedBy(fileOwner, LABEL_ID, 1));
// then
assertThat(approved).isTrue();
@@ -353,15 +432,20 @@
// when
boolean approved =
- isLabelApproved(anyWithBlock, fileOwner, fileOwner, approvedBy(fileOwner, LABEL_ID, 0));
+ isLabelApproved(
+ anyWithBlock,
+ Optional.empty(),
+ fileOwner,
+ fileOwner,
+ approvedBy(fileOwner, LABEL_ID, 0));
// then
assertThat(approved).isFalse();
}
- private static final LabelTypes maxNoBlockLabelFooTypes() {
+ private static final LabelAndScore maxNoBlockLabelFooOwnersLabel() {
LabelType maxValueRequired = label().setFunction(LabelFunction.MAX_NO_BLOCK).build();
- return new LabelTypes(List.of(maxValueRequired));
+ return new LabelAndScore(maxValueRequired, Optional.empty());
}
private static final LabelType.Builder label() {