Merge branch 'stable-3.14' * stable-3.14: Add ESLint configuration Adapt prolog-runtime dependency to rules_jvm_external Allow matchers to override auto-owners-approved Refactor tests in copy-condition IT Remove unneeded @Nullable on rootEntry in PathOwners Avoid nullable PathOwnersEntry associated with projects Add AlreadyApprovedByCopyConditionsIT hierarchy OWNERS test cases Add auto-owners-approved to OWNERS Make fields PathOwners field final Ignore commit message diffs in already-approved-by_owners Replace use of static EMPTY ReadOnlyPathOwnersEntry with Optional<> Avoid passing rootEntry to calculateCurrentEntry() Fix import of com.google.common.collect.Sets Remove definition of owners-common as a plugin Open JDK internals for PowerMock in owners-common tests Disambiguate Truth assertions in owners-common tests Reformat with GJF 1.24 Change-Id: I19ce5ba0649caf19b2531499db4dc4ec698957a1
diff --git a/owners-common/BUILD b/owners-common/BUILD index 0f12783..faf2d08 100644 --- a/owners-common/BUILD +++ b/owners-common/BUILD
@@ -11,16 +11,15 @@ deps = PLUGIN_DEPS_NEVERLINK + EXTERNAL_DEPS, ) -gerrit_plugin( - name = "owners-common__plugin", - srcs = glob(["src/main/java/**/*.java"]), - resources = glob(["src/main/resources/**/*"]), -) - junit_tests( name = "test", testonly = 1, srcs = glob(["src/test/java/**/*.java"]), + jvm_flags = [ + "--add-opens=java.base/java.lang=ALL-UNNAMED", + "--add-opens=java.base/java.util=ALL-UNNAMED", + "--add-opens=java.base/java.util.stream=ALL-UNNAMED", + ], deps = EXTERNAL_TEST_DEPS + PLUGIN_DEPS + PLUGIN_TEST_DEPS + [ ":owners-common", ],
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 54c624e..921d967 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
@@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.google.gerrit.entities.Account.Id; +import com.google.gerrit.extensions.client.InheritableBoolean; import java.io.IOException; import java.util.Optional; import java.util.Set; @@ -47,11 +48,22 @@ Optional.ofNullable(jsonNode.get("label")) .map(JsonNode::asText) .flatMap(LabelDefinition::parse)); + Optional<InheritableBoolean> autoOwnersApproved = parseAutoOwnersApproved(jsonNode); + + autoOwnersApproved.ifPresent(ret::setAutoOwnersApproved); + addClassicMatcher(jsonNode, ret); addMatchers(jsonNode, ret); return ret; } + private Optional<InheritableBoolean> parseAutoOwnersApproved(JsonNode jsonNode) { + return Optional.ofNullable(jsonNode.get("auto-owners-approved")) + .map(JsonNode::asText) + .map(String::toUpperCase) + .map(InheritableBoolean::valueOf); + } + private void addClassicMatcher(JsonNode jsonNode, OwnersConfig ret) { ret.setOwners(toClassicOwnersList(jsonNode, "owners").collect(Collectors.toSet())); ret.setReviewers(toClassicOwnersList(jsonNode, "reviewers").collect(Collectors.toSet())); @@ -109,17 +121,27 @@ .flatMap(o -> accounts.find(o).stream()) .collect(Collectors.toSet()); + InheritableBoolean autoOwnersApproved = + parseAutoOwnersApproved(node).orElse(InheritableBoolean.INHERIT); + Optional<Matcher> suffixMatcher = - getText(node, "suffix").map(el -> new SuffixMatcher(el, owners, reviewers, groupOwners)); + getText(node, "suffix") + .map(el -> new SuffixMatcher(el, owners, reviewers, groupOwners, autoOwnersApproved)); Optional<Matcher> regexMatcher = - getText(node, "regex").map(el -> new RegExMatcher(el, owners, reviewers, groupOwners)); + getText(node, "regex") + .map(el -> new RegExMatcher(el, owners, reviewers, groupOwners, autoOwnersApproved)); Optional<Matcher> partialRegexMatcher = getText(node, "partial_regex") - .map(el -> new PartialRegExMatcher(el, owners, reviewers, groupOwners)); + .map( + el -> + new PartialRegExMatcher( + el, owners, reviewers, groupOwners, autoOwnersApproved)); Optional<Matcher> exactMatcher = - getText(node, "exact").map(el -> new ExactMatcher(el, owners, reviewers, groupOwners)); + getText(node, "exact") + .map(el -> new ExactMatcher(el, owners, reviewers, groupOwners, autoOwnersApproved)); Optional<Matcher> genericMatcher = - getText(node, "generic").map(el -> new GenericMatcher(el, owners, reviewers, groupOwners)); + getText(node, "generic") + .map(el -> new GenericMatcher(el, owners, reviewers, groupOwners, autoOwnersApproved)); return Optional.ofNullable( suffixMatcher.orElseGet(
diff --git a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/ExactMatcher.java b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/ExactMatcher.java index 7088289..02b9ab5 100644 --- a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/ExactMatcher.java +++ b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/ExactMatcher.java
@@ -17,12 +17,17 @@ import com.google.gerrit.entities.Account; import com.google.gerrit.entities.Account.Id; +import com.google.gerrit.extensions.client.InheritableBoolean; import java.util.Set; public class ExactMatcher extends Matcher { public ExactMatcher( - String path, Set<Account.Id> owners, Set<Account.Id> reviewers, Set<String> groupOwners) { - super(path, owners, reviewers, groupOwners); + String path, + Set<Account.Id> owners, + Set<Account.Id> reviewers, + Set<String> groupOwners, + InheritableBoolean autoOwnersApproved) { + super(path, owners, reviewers, groupOwners, autoOwnersApproved); } @Override @@ -31,7 +36,11 @@ } @Override - protected Matcher clone(Set<Id> owners, Set<Id> reviewers, Set<String> groupOwners) { - return new ExactMatcher(path, owners, reviewers, groupOwners); + protected Matcher clone( + Set<Id> owners, + Set<Id> reviewers, + Set<String> groupOwners, + InheritableBoolean autoOwnersApproved) { + return new ExactMatcher(path, owners, reviewers, groupOwners, autoOwnersApproved); } }
diff --git a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/GenericMatcher.java b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/GenericMatcher.java index e37065b..f46ee7f 100644 --- a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/GenericMatcher.java +++ b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/GenericMatcher.java
@@ -16,12 +16,17 @@ package com.googlesource.gerrit.owners.common; import com.google.gerrit.entities.Account; +import com.google.gerrit.extensions.client.InheritableBoolean; import java.util.Set; public class GenericMatcher extends RegExMatcher { public GenericMatcher( - String path, Set<Account.Id> owners, Set<Account.Id> reviewers, Set<String> groupOwners) { - super(path, owners, reviewers, groupOwners); + String path, + Set<Account.Id> owners, + Set<Account.Id> reviewers, + Set<String> groupOwners, + InheritableBoolean autoOwnersApproved) { + super(path, owners, reviewers, groupOwners, autoOwnersApproved); } }
diff --git a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/Matcher.java b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/Matcher.java index 321b588..278e75d 100644 --- a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/Matcher.java +++ b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/Matcher.java
@@ -17,6 +17,7 @@ import com.google.common.collect.ImmutableSet; import com.google.gerrit.entities.Account; import com.google.gerrit.entities.Account.Id; +import com.google.gerrit.extensions.client.InheritableBoolean; import java.util.Set; public abstract class Matcher { @@ -24,13 +25,19 @@ private Set<Account.Id> reviewers; private Set<String> groupOwners; protected String path; + private InheritableBoolean autoOwnersApproved; public Matcher( - String key, Set<Account.Id> owners, Set<Account.Id> reviewers, Set<String> groupOwners) { + String key, + Set<Account.Id> owners, + Set<Account.Id> reviewers, + Set<String> groupOwners, + InheritableBoolean autoOwnersApproved) { this.path = key; this.owners = owners; this.reviewers = reviewers; this.groupOwners = groupOwners; + this.autoOwnersApproved = autoOwnersApproved; } @Override @@ -43,6 +50,8 @@ + groupOwners + ", reviewers=" + reviewers + + ", autoOwnersApproved=" + + autoOwnersApproved + "]"; } @@ -74,6 +83,10 @@ return path; } + public InheritableBoolean getAutoOwnersApproved() { + return autoOwnersApproved; + } + public abstract boolean matches(String pathToMatch); public Matcher merge(Matcher other) { @@ -84,10 +97,17 @@ return clone( mergeSet(owners, other.owners), mergeSet(reviewers, other.reviewers), - mergeSet(groupOwners, other.groupOwners)); + mergeSet(groupOwners, other.groupOwners), + other.autoOwnersApproved == InheritableBoolean.INHERIT + ? autoOwnersApproved + : other.getAutoOwnersApproved()); } - protected abstract Matcher clone(Set<Id> owners, Set<Id> reviewers, Set<String> groupOwners); + protected abstract Matcher clone( + Set<Id> owners, + Set<Id> reviewers, + Set<String> groupOwners, + InheritableBoolean autoOwnersApproved); private <T> Set<T> mergeSet(Set<T> set1, Set<T> set2) { ImmutableSet.Builder<T> setBuilder = ImmutableSet.builder();
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 7e7e705..1fce900 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
@@ -16,8 +16,11 @@ package com.googlesource.gerrit.owners.common; +import static com.google.gerrit.extensions.client.InheritableBoolean.INHERIT; + import com.google.common.collect.Maps; import com.google.common.collect.Sets; +import com.google.gerrit.extensions.client.InheritableBoolean; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -40,6 +43,9 @@ /** Label that is required for submit. CodeReview if nothing is specified. */ private Optional<LabelDefinition> label = Optional.empty(); + /** Ability to enable or disable the owners auto approval, when configured */ + private InheritableBoolean autoOwnersApproved = INHERIT; + @Override public String toString() { return "OwnersConfig [inherited=" @@ -50,6 +56,8 @@ + matchers + ", label=" + label + + ", autoOwnersApproved=" + + autoOwnersApproved + "]"; } @@ -92,4 +100,12 @@ public Optional<LabelDefinition> getLabel() { return label; } + + public InheritableBoolean getAutoOwnersApproved() { + return autoOwnersApproved; + } + + public void setAutoOwnersApproved(InheritableBoolean autoOwnersApproved) { + this.autoOwnersApproved = autoOwnersApproved; + } }
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 760b8a6..102f954 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,6 +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 Set<String> fileOwnersBannedAutoApproval = Sets.newHashSet(); private Optional<LabelDefinition> label = Optional.empty(); @Override @@ -86,6 +87,10 @@ return fileGroupOwners; } + public Set<String> getFileOwnersBannedAutoApproval() { + return fileOwnersBannedAutoApproval; + } + public void addFileOwners(String file, Set<Id> owners) { if (owners.isEmpty()) { return; @@ -122,6 +127,14 @@ fileGroupOwners.computeIfAbsent(file, (f) -> Sets.newHashSet()).addAll(groupOwners); } + public void banFileFromOwnersAutoApproval(String file) { + fileOwnersBannedAutoApproval.add(file); + } + + public void allowFileForAutoApproval(String file) { + fileOwnersBannedAutoApproval.remove(file); + } + public Optional<LabelDefinition> getLabel() { return label; }
diff --git a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PartialRegExMatcher.java b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PartialRegExMatcher.java index 479a287..8fe7812 100644 --- a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PartialRegExMatcher.java +++ b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PartialRegExMatcher.java
@@ -17,6 +17,7 @@ import com.google.gerrit.entities.Account; import com.google.gerrit.entities.Account.Id; +import com.google.gerrit.extensions.client.InheritableBoolean; import java.util.Set; import java.util.regex.Pattern; @@ -25,8 +26,12 @@ Pattern pattern; public PartialRegExMatcher( - String path, Set<Account.Id> owners, Set<Account.Id> reviewers, Set<String> groupOwners) { - super(path, owners, reviewers, groupOwners); + String path, + Set<Account.Id> owners, + Set<Account.Id> reviewers, + Set<String> groupOwners, + InheritableBoolean autoOwnersApproved) { + super(path, owners, reviewers, groupOwners, autoOwnersApproved); pattern = Pattern.compile(".*" + path + ".*"); } @@ -36,7 +41,11 @@ } @Override - protected Matcher clone(Set<Id> owners, Set<Id> reviewers, Set<String> groupOwners) { - return new PartialRegExMatcher(path, owners, reviewers, groupOwners); + protected Matcher clone( + Set<Id> owners, + Set<Id> reviewers, + Set<String> groupOwners, + InheritableBoolean autoOwnersApproved) { + return new PartialRegExMatcher(path, owners, reviewers, groupOwners, autoOwnersApproved); } }
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 1e5023b..c7d33fe 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
@@ -32,6 +32,7 @@ import com.google.gerrit.entities.Patch; import com.google.gerrit.entities.Project; import com.google.gerrit.entities.RefNames; +import com.google.gerrit.extensions.client.InheritableBoolean; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.patch.DiffSummary; import com.google.gerrit.server.patch.filediff.FileDiffOutput; @@ -84,11 +85,13 @@ private final GitRepositoryManager repositoryManager; - private Map<String, Matcher> matchers; + private final Map<String, Matcher> matchers; - private Map<String, Set<Id>> fileOwners; + private final Map<String, Set<Id>> fileOwners; - private Map<String, Set<String>> fileGroupOwners; + private final Map<String, Set<String>> fileGroupOwners; + + private final Set<String> fileOwnersBannedAutoApproval; private final boolean expandGroups; @@ -175,8 +178,10 @@ matchers = map.getMatchers(); fileOwners = map.getFileOwners(); fileGroupOwners = map.getFileGroupOwners(); + fileOwnersBannedAutoApproval = map.getFileOwnersBannedAutoApproval(); label = globalLabel.or(map::getLabel); } + /** * Returns a read only view of the paths to owners mapping. * @@ -207,6 +212,10 @@ return fileGroupOwners; } + public Set<String> getFileOwnersBannedAutoApproval() { + return fileOwnersBannedAutoApproval; + } + public boolean expandGroups() { return expandGroups; } @@ -234,7 +243,7 @@ // Using a `map` would have needed a try/catch inside the lamba, resulting in more code List<ReadOnlyPathOwnersEntry> parentsPathOwnersEntries = getPathOwnersEntries(parentProjectsNames, RefNames.REFS_CONFIG, cache); - ReadOnlyPathOwnersEntry projectEntry = + Optional<ReadOnlyPathOwnersEntry> projectEntry = getPathOwnersEntryOrEmpty(project, repository, RefNames.REFS_CONFIG, cache); PathOwnersEntry rootEntry = getPathOwnersEntryOrNew(project, repository, branch, cache); @@ -256,6 +265,9 @@ ownersMap.addFileOwners(path, currentEntry.getOwners()); ownersMap.addFileReviewers(path, currentEntry.getReviewers()); ownersMap.addFileGroupOwners(path, currentEntry.getGroupOwners()); + if (!currentEntry.isAutoOwnersApproved()) { + ownersMap.banFileFromOwnersAutoApproval(path); + } // Only add the path to the OWNERS file to reduce the number of // entries in the result @@ -293,20 +305,20 @@ ImmutableList.Builder<ReadOnlyPathOwnersEntry> pathOwnersEntries = ImmutableList.builder(); for (Project.NameKey projectName : projectNames) { try (Repository repo = repositoryManager.openRepository(projectName)) { - pathOwnersEntries = - pathOwnersEntries.add( - getPathOwnersEntryOrEmpty(projectName.get(), repo, branch, cache)); + Optional<ReadOnlyPathOwnersEntry> pathOwnersEntry = + getPathOwnersEntryOrEmpty(projectName.get(), repo, branch, cache); + if (pathOwnersEntry.isPresent()) { + pathOwnersEntries = pathOwnersEntries.add(pathOwnersEntry.get()); + } } } return pathOwnersEntries.build(); } - private ReadOnlyPathOwnersEntry getPathOwnersEntryOrEmpty( + private Optional<ReadOnlyPathOwnersEntry> getPathOwnersEntryOrEmpty( String project, Repository repo, String branch, PathOwnersEntriesCache cache) throws InvalidOwnersFileException, ExecutionException { - return getPathOwnersEntry(project, repo, branch, cache) - .map(v -> (ReadOnlyPathOwnersEntry) v) - .orElse(PathOwnersEntry.EMPTY); + return getPathOwnersEntry(project, repo, branch, cache).map(v -> (ReadOnlyPathOwnersEntry) v); } private PathOwnersEntry getPathOwnersEntryOrNew( @@ -336,6 +348,7 @@ Optional.empty(), Collections.emptySet(), Collections.emptySet(), + Optional.empty(), Collections.emptySet(), Collections.emptySet()))); } @@ -378,6 +391,22 @@ ownersMap.addFileOwners(path, matcher.getOwners()); ownersMap.addFileGroupOwners(path, matcher.getGroupOwners()); ownersMap.addFileReviewers(path, matcher.getReviewers()); + switch (matcher.getAutoOwnersApproved()) { + // We have an explicit allowance for this matcher + // Make sure that anything added at OWNERS level is removed + case InheritableBoolean.TRUE: + ownersMap.allowFileForAutoApproval(path); + break; + // We have an explicit ban for this matcher + case InheritableBoolean.FALSE: + ownersMap.banFileFromOwnersAutoApproval(path); + break; + + // There is no matcher-level specification of auto-owner-approved + // therefore the global OWNER-level still applies + default: + break; + } matchingFound = true; } } @@ -388,26 +417,30 @@ String project, String path, String branch, - ReadOnlyPathOwnersEntry projectEntry, + Optional<ReadOnlyPathOwnersEntry> projectEntry, List<ReadOnlyPathOwnersEntry> parentsPathOwnersEntries, PathOwnersEntry rootEntry, Map<String, PathOwnersEntry> entries, PathOwnersEntriesCache cache) throws InvalidOwnersFileException, ExecutionException { String[] parts = path.split("/"); - PathOwnersEntry currentEntry = rootEntry; StringBuilder builder = new StringBuilder(); // Inherit from Project if OWNER in root enables inheritance - calculateCurrentEntry(rootEntry, projectEntry, currentEntry); + if (rootEntry.isInherited()) { + projectEntry.ifPresent(pe -> calculateCurrentEntry(pe, rootEntry)); + } // Inherit from Parent Project if OWNER in Project enables inheritance for (ReadOnlyPathOwnersEntry parentPathOwnersEntry : parentsPathOwnersEntries) { - calculateCurrentEntry(projectEntry, parentPathOwnersEntry, currentEntry); + if (projectEntry.isEmpty() || projectEntry.get().isInherited()) { + calculateCurrentEntry(parentPathOwnersEntry, rootEntry); + } } // Iterate through the parent paths, not including the file name // itself + PathOwnersEntry currentEntry = rootEntry; for (int i = 0; i < parts.length - 1; i++) { String part = parts[i]; builder.append(part).append("/"); @@ -443,6 +476,7 @@ label, owners, reviewers, + Optional.of(pathFallbackEntry.isAutoOwnersApproved()), inheritedMatchers, groupOwners); }) @@ -454,24 +488,23 @@ } private void calculateCurrentEntry( - ReadOnlyPathOwnersEntry rootEntry, - ReadOnlyPathOwnersEntry projectEntry, - PathOwnersEntry currentEntry) { - if (rootEntry.isInherited()) { - for (Matcher matcher : projectEntry.getMatchers().values()) { - if (!currentEntry.hasMatcher(matcher.getPath())) { - currentEntry.addMatcher(matcher); - } + ReadOnlyPathOwnersEntry projectEntry, PathOwnersEntry currentEntry) { + for (Matcher matcher : projectEntry.getMatchers().values()) { + if (!currentEntry.hasMatcher(matcher.getPath())) { + currentEntry.addMatcher(matcher); } - if (currentEntry.getOwners().isEmpty()) { - currentEntry.setOwners(projectEntry.getOwners()); - } - if (currentEntry.getOwnersPath() == null) { - currentEntry.setOwnersPath(projectEntry.getOwnersPath()); - } - if (currentEntry.getLabel().isEmpty()) { - currentEntry.setLabel(projectEntry.getLabel()); - } + } + if (currentEntry.getOwners().isEmpty()) { + currentEntry.setOwners(projectEntry.getOwners()); + } + if (currentEntry.getOwnersPath() == null) { + currentEntry.setOwnersPath(projectEntry.getOwnersPath()); + } + if (currentEntry.getLabel().isEmpty()) { + currentEntry.setLabel(projectEntry.getLabel()); + } + if (!currentEntry.hasExplicitAutoOwnersApproved()) { + currentEntry.setAutoOwnersApproved(projectEntry.isAutoOwnersApproved()); } }
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 8e4c53c..7b42709 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
@@ -20,6 +20,7 @@ import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.gerrit.entities.Account; +import com.google.gerrit.extensions.client.InheritableBoolean; import java.util.Collection; import java.util.Map; import java.util.Optional; @@ -32,8 +33,6 @@ * <p>Used internally by PathOwners to represent and compute the owners at a specific path. */ class PathOwnersEntry extends ReadOnlyPathOwnersEntry { - static final ReadOnlyPathOwnersEntry EMPTY = new ReadOnlyPathOwnersEntry(true) {}; - public PathOwnersEntry() { super(true); } @@ -45,9 +44,11 @@ Optional<LabelDefinition> inheritedLabel, Set<Account.Id> inheritedOwners, Set<Account.Id> inheritedReviewers, + Optional<Boolean> autoOwnersApproved, Collection<Matcher> inheritedMatchers, Set<String> inheritedGroupOwners) { super(config.isInherited()); + this.explicitAutoOwnersApproved = config.getAutoOwnersApproved() != InheritableBoolean.INHERIT; this.ownersPath = path; this.owners = config.getOwners().stream() @@ -67,12 +68,19 @@ this.owners.addAll(inheritedOwners); this.groupOwners.addAll(inheritedGroupOwners); this.reviewers.addAll(inheritedReviewers); + if (config.getAutoOwnersApproved() == InheritableBoolean.INHERIT) { + autoOwnersApproved.ifPresent(this::setAutoOwnersApproved); + } else { + setAutoOwnersApproved(config.getAutoOwnersApproved() == InheritableBoolean.TRUE); + } for (Matcher matcher : inheritedMatchers) { addMatcher(matcher); } this.label = config.getLabel().or(() -> inheritedLabel); } else { this.label = config.getLabel(); + // Default to true unless the OWNERS file explicitly sets it to false. + this.setAutoOwnersApproved(config.getAutoOwnersApproved() != InheritableBoolean.FALSE); } } @@ -121,6 +129,8 @@ protected String ownersPath; protected Map<String, Matcher> matchers = Maps.newHashMap(); protected Set<String> groupOwners = Sets.newHashSet(); + protected boolean autoOwnersApproved = true; + protected boolean explicitAutoOwnersApproved; protected ReadOnlyPathOwnersEntry(boolean inherited) { this.inherited = inherited; @@ -155,6 +165,18 @@ return label; } + public boolean isAutoOwnersApproved() { + return autoOwnersApproved; + } + + public boolean hasExplicitAutoOwnersApproved() { + return explicitAutoOwnersApproved; + } + + public void setAutoOwnersApproved(boolean autoOwnersApproved) { + this.autoOwnersApproved = autoOwnersApproved; + } + public boolean hasMatcher(String path) { return this.matchers.containsKey(path); }
diff --git a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PluginSettings.java b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PluginSettings.java index 1fd7403..951b133 100644 --- a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PluginSettings.java +++ b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PluginSettings.java
@@ -87,7 +87,9 @@ return expandGroups; } - /** @return <code>true</code> when OWNERS file should be evaluated through the submit rule */ + /** + * @return <code>true</code> when OWNERS file should be evaluated through the submit rule + */ public boolean enableSubmitRequirement() { return enableSubmitRequirement; }
diff --git a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/RegExMatcher.java b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/RegExMatcher.java index 6d1df24..d7920a8 100644 --- a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/RegExMatcher.java +++ b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/RegExMatcher.java
@@ -17,6 +17,7 @@ import com.google.gerrit.entities.Account; import com.google.gerrit.entities.Account.Id; +import com.google.gerrit.extensions.client.InheritableBoolean; import java.util.Set; import java.util.regex.Pattern; @@ -24,8 +25,12 @@ Pattern pattern; public RegExMatcher( - String path, Set<Account.Id> owners, Set<Account.Id> reviewers, Set<String> groupOwners) { - super(path, owners, reviewers, groupOwners); + String path, + Set<Account.Id> owners, + Set<Account.Id> reviewers, + Set<String> groupOwners, + InheritableBoolean autoOwnersApproved) { + super(path, owners, reviewers, groupOwners, autoOwnersApproved); pattern = Pattern.compile(path); } @@ -35,7 +40,11 @@ } @Override - protected Matcher clone(Set<Id> owners, Set<Id> reviewers, Set<String> groupOwners) { - return new RegExMatcher(path, owners, reviewers, groupOwners); + protected Matcher clone( + Set<Id> owners, + Set<Id> reviewers, + Set<String> groupOwners, + InheritableBoolean autoOwnersApproved) { + return new RegExMatcher(path, owners, reviewers, groupOwners, autoOwnersApproved); } }
diff --git a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/SuffixMatcher.java b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/SuffixMatcher.java index 5dec4c5..b9a2fdb 100644 --- a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/SuffixMatcher.java +++ b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/SuffixMatcher.java
@@ -17,12 +17,17 @@ import com.google.gerrit.entities.Account; import com.google.gerrit.entities.Account.Id; +import com.google.gerrit.extensions.client.InheritableBoolean; import java.util.Set; public class SuffixMatcher extends Matcher { public SuffixMatcher( - String path, Set<Account.Id> owners, Set<Account.Id> reviewers, Set<String> groupOwners) { - super(path, owners, reviewers, groupOwners); + String path, + Set<Account.Id> owners, + Set<Account.Id> reviewers, + Set<String> groupOwners, + InheritableBoolean autoOwnersApproved) { + super(path, owners, reviewers, groupOwners, autoOwnersApproved); } @Override @@ -31,7 +36,11 @@ } @Override - protected Matcher clone(Set<Id> owners, Set<Id> reviewers, Set<String> groupOwners) { - return new SuffixMatcher(path, owners, reviewers, groupOwners); + protected Matcher clone( + Set<Id> owners, + Set<Id> reviewers, + Set<String> groupOwners, + InheritableBoolean autoOwnersApproved) { + return new SuffixMatcher(path, owners, reviewers, groupOwners, autoOwnersApproved); } }
diff --git a/owners-common/src/test/java/com/googlesource/gerrit/owners/common/MatcherMergeTest.java b/owners-common/src/test/java/com/googlesource/gerrit/owners/common/MatcherMergeTest.java new file mode 100644 index 0000000..a36383a --- /dev/null +++ b/owners-common/src/test/java/com/googlesource/gerrit/owners/common/MatcherMergeTest.java
@@ -0,0 +1,83 @@ +// 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.googlesource.gerrit.owners.common; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gerrit.entities.Account; +import com.google.gerrit.extensions.client.InheritableBoolean; +import java.util.Set; +import org.junit.Test; + +public class MatcherMergeTest { + @Test + public void mergeAutoOwnersApprovedKeepsCurrentWhenOtherUnset() { + assertThatMatchersMergeAs( + InheritableBoolean.TRUE, InheritableBoolean.INHERIT, InheritableBoolean.TRUE); + } + + @Test + public void mergeAutoOwnersApprovedUsesOtherWhenOtherIsFalse() { + assertThatMatchersMergeAs( + InheritableBoolean.INHERIT, InheritableBoolean.FALSE, InheritableBoolean.FALSE); + } + + @Test + public void mergeAutoOwnersApprovedUsesOtherWhenOtherIsTrue() { + assertThatMatchersMergeAs( + InheritableBoolean.FALSE, InheritableBoolean.TRUE, InheritableBoolean.TRUE); + } + + @Test + public void mergeUnsetAutoOwnersApprovedIsUnset() { + assertThatMatchersMergeAs( + InheritableBoolean.INHERIT, InheritableBoolean.INHERIT, InheritableBoolean.INHERIT); + } + + private void assertThatMatchersMergeAs( + InheritableBoolean baseMatcherBool, + InheritableBoolean otherMatcherBool, + InheritableBoolean mergedMatcherBool) { + Matcher baseTrue = new TestMatcher(baseMatcherBool); + Matcher otherUnset = new TestMatcher(otherMatcherBool); + Matcher mergedUnsetOther = baseTrue.merge(otherUnset); + assertThat(mergedUnsetOther.getAutoOwnersApproved()).isEqualTo(mergedMatcherBool); + } + + private static class TestMatcher extends Matcher { + private TestMatcher(InheritableBoolean autoOwnersApproved) { + super( + "test", + java.util.Set.of(Account.id(1)), + java.util.Set.of(Account.id(2)), + java.util.Set.of("grp"), + autoOwnersApproved); + } + + @Override + public boolean matches(String pathToMatch) { + return true; + } + + @Override + protected Matcher clone( + Set<Account.Id> owners, + Set<Account.Id> reviewers, + Set<String> groupOwners, + InheritableBoolean autoOwnersApproval) { + return new TestMatcher(autoOwnersApproval); + } + } +}
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 7ab294f..ca09d57 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
@@ -15,7 +15,6 @@ package com.googlesource.gerrit.owners.common; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; import static com.googlesource.gerrit.owners.common.MatcherConfig.suffixMatcher; import static java.util.Collections.emptyList; import static org.easymock.EasyMock.eq; @@ -26,6 +25,7 @@ import static org.junit.Assert.assertTrue; import static org.powermock.api.easymock.PowerMock.replayAll; +import com.google.common.truth.Truth8; import com.google.gerrit.entities.Account; import com.google.gerrit.entities.Project; import com.google.gerrit.entities.RefNames; @@ -91,7 +91,7 @@ assertTrue(ownersSet.contains(USER_A_ID)); assertTrue(ownersSet.contains(USER_B_ID)); assertTrue(owners.expandGroups()); - assertThat(owners.getLabel()).isEmpty(); + Truth8.assertThat(owners.getLabel()).isEmpty(); } @Test @@ -110,7 +110,7 @@ "foo", CACHE_MOCK, Optional.of(EXPECTED_LABEL_DEFINITION)); - assertThat(owners.getLabel()).hasValue(EXPECTED_LABEL_DEFINITION); + Truth8.assertThat(owners.getLabel()).hasValue(EXPECTED_LABEL_DEFINITION); } @Test @@ -188,7 +188,7 @@ // expect that classic configuration takes precedence over `OWNERS` file for the label // definition - assertThat(owners2.getLabel().map(LabelDefinition::getName)).hasValue(EXPECTED_LABEL); + Truth8.assertThat(owners2.getLabel().map(LabelDefinition::getName)).hasValue(EXPECTED_LABEL); } @Test @@ -210,7 +210,7 @@ "foo", CACHE_MOCK, Optional.of(EXPECTED_LABEL_DEFINITION)); - assertThat(owners2.getLabel()).hasValue(EXPECTED_LABEL_DEFINITION); + Truth8.assertThat(owners2.getLabel()).hasValue(EXPECTED_LABEL_DEFINITION); } @Test @@ -232,7 +232,7 @@ "foo", CACHE_MOCK, Optional.of(EXPECTED_LABEL_DEFINITION)); - assertThat(owners2.getLabel()).hasValue(EXPECTED_LABEL_DEFINITION); + Truth8.assertThat(owners2.getLabel()).hasValue(EXPECTED_LABEL_DEFINITION); } @Test @@ -270,7 +270,7 @@ assertEquals(2, ownersSet.size()); assertTrue(ownersSet.contains(USER_A_ID)); assertTrue(ownersSet.contains(USER_B_ID)); - assertThat(owners.getLabel().map(LabelDefinition::getName)).hasValue(EXPECTED_LABEL); + Truth8.assertThat(owners.getLabel().map(LabelDefinition::getName)).hasValue(EXPECTED_LABEL); } @Test @@ -319,7 +319,7 @@ // expect that `master` configuration overwrites the label definition of both `refs/meta/config` // and parent repo - assertThat(owners.getLabel().map(LabelDefinition::getName)).hasValue(EXPECTED_LABEL); + Truth8.assertThat(owners.getLabel().map(LabelDefinition::getName)).hasValue(EXPECTED_LABEL); } @Test @@ -371,7 +371,7 @@ assertTrue(ownersSet2.contains(USER_B_ID)); // expect that closer parent (parentRepository1) overwrites the label definition - assertThat(owners.getLabel().map(LabelDefinition::getName)).hasValue(EXPECTED_LABEL); + Truth8.assertThat(owners.getLabel().map(LabelDefinition::getName)).hasValue(EXPECTED_LABEL); } private void mockParentRepository(Project.NameKey repositoryName, Repository repository) @@ -411,7 +411,7 @@ assertTrue(ownersSet.contains(USER_C_ID)); // expect that more specific configuration overwrites the label definition - assertThat(owners.getLabel().map(LabelDefinition::getName)).hasValue(EXPECTED_LABEL); + Truth8.assertThat(owners.getLabel().map(LabelDefinition::getName)).hasValue(EXPECTED_LABEL); } @Test @@ -421,11 +421,11 @@ OwnersConfig ownersConfig = getOwnersConfig(yamlString); assertTrue(ownersConfig.isInherited()); - assertThat(ownersConfig.getLabel()).isPresent(); + Truth8.assertThat(ownersConfig.getLabel()).isPresent(); LabelDefinition label = ownersConfig.getLabel().get(); assertThat(label.getName()).isEqualTo(EXPECTED_LABEL); - assertThat(label.getScore()).hasValue(1); + Truth8.assertThat(label.getScore()).hasValue(1); Set<String> owners = ownersConfig.getOwners(); assertEquals(1, owners.size()); @@ -439,11 +439,11 @@ OwnersConfig ownersConfig = getOwnersConfig(yamlString); assertTrue(ownersConfig.isInherited()); - assertThat(ownersConfig.getLabel()).isPresent(); + Truth8.assertThat(ownersConfig.getLabel()).isPresent(); LabelDefinition label = ownersConfig.getLabel().get(); assertThat(label.getName()).isEqualTo(EXPECTED_LABEL); - assertThat(label.getScore()).isEmpty(); + Truth8.assertThat(label.getScore()).isEmpty(); Set<String> owners = ownersConfig.getOwners(); assertEquals(1, owners.size()); @@ -493,6 +493,138 @@ assertThat(cacheMock.hit).isEqualTo(expectedCacheCalls); } + @Test + public void testAutoOwnersMisconfigured() throws Exception { + expectConfig("OWNERS", "inherited: true\nauto-owners-approved: \"some wrong value\""); + + replayAll(); + + PathOwners owners = + new PathOwners( + accounts, + repositoryManager, + repository, + emptyList(), + branch, + Set.of("file.txt"), + EXPAND_GROUPS, + "foo", + CACHE_MOCK, + Optional.empty()); + + assertThat(owners.getFileOwnersBannedAutoApproval()).isEmpty(); + assertThat(owners.getFileOwners()).isEmpty(); + } + + @Test + public void testAutoOwnersApprovedInheritedFromRoot() throws Exception { + expectConfig( + "OWNERS", + "inherited: true\nauto-owners-approved: false\nowners:\n- " + USER_A_EMAIL_COM + "\n"); + expectConfig("dir/OWNERS", "inherited: true\nowners:\n- " + USER_B_EMAIL_COM + "\n"); + + replayAll(); + + PathOwners owners = + new PathOwners( + accounts, + repositoryManager, + repository, + emptyList(), + branch, + Set.of("dir/file.txt"), + EXPAND_GROUPS, + "foo", + CACHE_MOCK, + Optional.empty()); + + assertThat(owners.getFileOwnersBannedAutoApproval()).contains("dir/file.txt"); + } + + @Test + public void testAutoOwnersApprovedDefaultsWhenInheritanceStopped() throws Exception { + expectConfig( + "OWNERS", + "inherited: true\nauto-owners-approved: false\nowners:\n- " + USER_A_EMAIL_COM + "\n"); + expectConfig("dir/OWNERS", "inherited: false\nowners:\n- " + USER_B_EMAIL_COM + "\n"); + + replayAll(); + + PathOwners owners = + new PathOwners( + accounts, + repositoryManager, + repository, + emptyList(), + branch, + Set.of("dir/file.txt"), + EXPAND_GROUPS, + "foo", + CACHE_MOCK, + Optional.empty()); + + assertThat(owners.getFileOwnersBannedAutoApproval()).isEmpty(); + } + + @Test + public void testAutoOwnersApprovedInheritedFromParentProjectOwners() throws Exception { + expectConfig("OWNERS", "master", createConfig(true, owners())); + expectConfig("OWNERS", RefNames.REFS_CONFIG, repository, createConfig(true, owners())); + expectConfig( + "OWNERS", + RefNames.REFS_CONFIG, + parentRepository1, + "inherited: true\nauto-owners-approved: false\nowners:\n- " + USER_A_EMAIL_COM + "\n"); + + mockParentRepository(parentRepository1NameKey, parentRepository1); + replayAll(); + + PathOwners owners = + new PathOwners( + accounts, + repositoryManager, + repository, + Arrays.asList(parentRepository1NameKey), + branch, + Set.of("file.txt"), + EXPAND_GROUPS, + "foo", + CACHE_MOCK, + Optional.empty()); + + assertThat(owners.getFileOwnersBannedAutoApproval()).contains("file.txt"); + } + + @Test + public void testExplicitAutoOwnersApprovedInRootOverridesProjectOwners() throws Exception { + expectConfig( + "OWNERS", + "master", + "inherited: true\nauto-owners-approved: false\nowners:\n- " + USER_A_EMAIL_COM + "\n"); + expectConfig( + "OWNERS", + RefNames.REFS_CONFIG, + repository, + "inherited: true\nauto-owners-approved: true\nowners:\n- " + USER_B_EMAIL_COM + "\n"); + + replayAll(); + + PathOwners owners = + new PathOwners( + accounts, + repositoryManager, + repository, + emptyList(), + branch, + Set.of("file.txt"), + EXPAND_GROUPS, + "foo", + CACHE_MOCK, + Optional.empty()); + + assertThat(owners.getFileOwnersBannedAutoApproval()).contains("file.txt"); + } + private void mockOwners(String... owners) throws IOException { expectNoConfig("OWNERS"); expectConfig(CLASSIC_OWNERS, createConfig(false, owners(owners)));
diff --git a/owners-common/src/test/java/com/googlesource/gerrit/owners/common/RegexMatcherTest.java b/owners-common/src/test/java/com/googlesource/gerrit/owners/common/RegexMatcherTest.java index 67c4ab6..d157ebb 100644 --- a/owners-common/src/test/java/com/googlesource/gerrit/owners/common/RegexMatcherTest.java +++ b/owners-common/src/test/java/com/googlesource/gerrit/owners/common/RegexMatcherTest.java
@@ -23,18 +23,18 @@ public class RegexMatcherTest { @Test public void testRegex() { - RegExMatcher matcher = new RegExMatcher(".*/a.*", null, null, null); + RegExMatcher matcher = new RegExMatcher(".*/a.*", null, null, null, null); assertTrue(matcher.matches("xxxxxx/axxxx")); assertFalse(matcher.matches("axxxx")); assertFalse(matcher.matches("xxxxx/bxxxx")); - RegExMatcher matcher2 = new RegExMatcher("a.*.sql", null, null, null); + RegExMatcher matcher2 = new RegExMatcher("a.*.sql", null, null, null, null); assertFalse(matcher2.matches("xxxxxx/alfa.sql")); } @Test public void testFloatingRegex() { - PartialRegExMatcher matcher = new PartialRegExMatcher("a.*.sql", null, null, null); + PartialRegExMatcher matcher = new PartialRegExMatcher("a.*.sql", null, null, null, null); assertTrue(matcher.matches("xxxxxxx/alfa.sql")); assertTrue(matcher.matches("alfa.sqlxxxxx")); assertFalse(matcher.matches("alfa.bar"));
diff --git a/owners-common/src/test/java/com/googlesource/gerrit/owners/common/RegexTest.java b/owners-common/src/test/java/com/googlesource/gerrit/owners/common/RegexTest.java index 821db6a..a6e10a6 100644 --- a/owners-common/src/test/java/com/googlesource/gerrit/owners/common/RegexTest.java +++ b/owners-common/src/test/java/com/googlesource/gerrit/owners/common/RegexTest.java
@@ -15,7 +15,6 @@ package com.googlesource.gerrit.owners.common; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; import static com.googlesource.gerrit.owners.common.MatcherConfig.exactMatcher; import static com.googlesource.gerrit.owners.common.MatcherConfig.genericMatcher; import static com.googlesource.gerrit.owners.common.MatcherConfig.partialRegexMatcher; @@ -28,6 +27,7 @@ import static org.powermock.api.easymock.PowerMock.replayAll; import com.google.gerrit.entities.Account; +import com.google.gerrit.extensions.client.InheritableBoolean; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -271,4 +271,36 @@ Set<String> ownedFiles = owners.getFileOwners().keySet(); assertThat(ownedFiles).containsExactly("project/file.sql"); } + + @Test + public void testParsingAutoOwnersApprovedFlags() throws Exception { + replayAll(); + + String configString = + "inherited: true\n" + + "auto-owners-approved: false\n" + + "owners:\n" + + "- a\n" + + "matchers:\n" + + "- suffix: .sql\n" + + " auto-owners-approved: true\n" + + " owners:\n" + + " - b\n" + + "- exact: project/a.txt\n" + + " auto-owners-approved: false\n" + + " owners:\n" + + " - c\n" + + "- regex: .*\\.md\n" + + " owners:\n" + + " - d\n"; + + OwnersConfig config = getOwnersConfig(configString); + + assertThat(config.getMatchers().get(".sql").getAutoOwnersApproved()) + .isEqualTo(InheritableBoolean.TRUE); + assertThat(config.getMatchers().get("project/a.txt").getAutoOwnersApproved()) + .isEqualTo(InheritableBoolean.FALSE); + assertThat(config.getMatchers().get(".*\\.md").getAutoOwnersApproved()) + .isEqualTo(InheritableBoolean.INHERIT); + } }
diff --git a/owners/BUILD b/owners/BUILD index c1dac46..59a8c5d 100644 --- a/owners/BUILD +++ b/owners/BUILD
@@ -13,10 +13,16 @@ srcs = PROLOG_PREDICATES, deps = [ "//owners-common", - "@prolog-runtime//jar:neverlink", + ":prolog-runtime-neverlink", ] + PLUGIN_DEPS_NEVERLINK, ) +java_library( + name = "prolog-runtime-neverlink", + neverlink = 1, + exports = ["@external_deps//:com_googlecode_prolog_cafe_prolog_runtime"], +) + prolog_cafe_library( name = "gerrit-owners-prolog-rules", srcs = glob(["src/main/prolog/*.pl"]),
diff --git a/owners/src/main/java/com/googlesource/gerrit/owners/AlreadyApprovedByPredicate.java b/owners/src/main/java/com/googlesource/gerrit/owners/AlreadyApprovedByPredicate.java index bf4aede..e74e402 100644 --- a/owners/src/main/java/com/googlesource/gerrit/owners/AlreadyApprovedByPredicate.java +++ b/owners/src/main/java/com/googlesource/gerrit/owners/AlreadyApprovedByPredicate.java
@@ -21,6 +21,7 @@ import com.google.common.flogger.FluentLogger; import com.google.gerrit.entities.Account; +import com.google.gerrit.entities.Patch; import com.google.gerrit.entities.PatchSet; import com.google.gerrit.entities.Project; import com.google.gerrit.exceptions.StorageException; @@ -75,6 +76,8 @@ Project.NameKey project = ctx.changeData().project(); PatchSet targetPatchSet = ctx.targetPatchSet(); PatchSet sourcePatchSet = ctx.changeNotes().getPatchSets().get(ctx.sourcePatchSetId()); + Account.Id changeOwner = ctx.changeNotes().getChange().getOwner(); + Account.Id uploader = targetPatchSet.uploader(); checkState( predicateField == UserInPredicate.Field.APPROVER, @@ -91,8 +94,18 @@ project); Map<String, FileDiffOutput> priorVsCurrent = - diffOperations.listModifiedFiles( - project, sourcePatchSet.commitId(), targetPatchSet.commitId(), DO_NOT_IGNORE_REBASE); + diffOperations + .listModifiedFiles( + project, + sourcePatchSet.commitId(), + targetPatchSet.commitId(), + DO_NOT_IGNORE_REBASE) + .entrySet() + .stream() + // COMMIT_MSG has never an owner, we don't ever want to consider it, even if it + // was modified as part of this patch-set. + .filter(entry -> !Patch.COMMIT_MSG.equals(entry.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); // We can't simply look at keys because it won't contain the old name of renamed-files. Set<String> allFilePathsInDiff = @@ -102,12 +115,20 @@ .map(Optional::get) .collect(Collectors.toSet()); + String branch = ctx.changeData().branchOrThrow().branch(); Set<String> filesOwnedByApprover = - getFilesOwners.filterFilesOwnedBy( - currentApprover, - allFilePathsInDiff, - project, - ctx.changeData().branchOrThrow().branch()); + getFilesOwners.filterFilesOwnedBy(currentApprover, allFilePathsInDiff, project, branch); + + if (isApproverAlsoOwnerAndUploader(currentApprover, changeOwner, uploader) + && allTouchedFilesAreOwned(filesOwnedByApprover, allFilePathsInDiff) + && getFilesOwners.noOwnedFileIsBannedFromAutoApproval( + filesOwnedByApprover, project, branch)) { + logger.atFinest().log( + "Approver '%s' is change owner and uploader. only owned files have been modified and" + + " none of them has auto-owners-approved=false. Label WILL be copied.", + currentApprover); + return true; + } if (!filesOwnedByApprover.isEmpty()) { logger.atFinest().log( @@ -198,6 +219,17 @@ return !d.oldPath().equals(d.newPath()); } + private static boolean isApproverAlsoOwnerAndUploader( + Account.Id currentApprover, Account.Id changeOwner, Account.Id uploader) { + return currentApprover.equals(changeOwner) && currentApprover.equals(uploader); + } + + private static boolean allTouchedFilesAreOwned( + Set<String> filesOwnedByApprover, Set<String> allFilePathsInDiff) { + return !filesOwnedByApprover.isEmpty() + && filesOwnedByApprover.size() == allFilePathsInDiff.size(); + } + private int getParentNum(ObjectId objectId, RevWalk revWalk) { try { RevCommit commit = revWalk.parseCommit(objectId);
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 64638bc..33a4b3f 100644 --- a/owners/src/main/java/com/googlesource/gerrit/owners/OwnersSubmitRequirement.java +++ b/owners/src/main/java/com/googlesource/gerrit/owners/OwnersSubmitRequirement.java
@@ -251,7 +251,8 @@ .or( () -> { logger.atSevere().log( - "OWNERS label '%s' is not configured for '%s' project. Change is not submittable.", + "OWNERS label '%s' is not configured for '%s' project. Change is not" + + " submittable.", label, project); return Optional.empty(); });
diff --git a/owners/src/main/java/com/googlesource/gerrit/owners/restapi/GetFilesOwners.java b/owners/src/main/java/com/googlesource/gerrit/owners/restapi/GetFilesOwners.java index c395179..af62387 100644 --- a/owners/src/main/java/com/googlesource/gerrit/owners/restapi/GetFilesOwners.java +++ b/owners/src/main/java/com/googlesource/gerrit/owners/restapi/GetFilesOwners.java
@@ -108,6 +108,15 @@ .collect(Collectors.toSet()); } + public boolean noOwnedFileIsBannedFromAutoApproval( + Set<String> ownedPaths, Project.NameKey project, String branch) + throws IOException, InvalidOwnersFileException { + PathOwners owners = getPathOwners(project, branch, ownedPaths); + Set<String> filesBannedFromAutoOwnersApproval = owners.getFileOwnersBannedAutoApproval(); + + return ownedPaths.stream().noneMatch(filesBannedFromAutoOwnersApproval::contains); + } + @Override public Response<FilesOwnersResponse> apply(RevisionResource revision) throws AuthException, BadRequestException, ResourceConflictException, Exception {
diff --git a/owners/src/main/resources/Documentation/config.md b/owners/src/main/resources/Documentation/config.md index 4b94ac3..553e540 100644 --- a/owners/src/main/resources/Documentation/config.md +++ b/owners/src/main/resources/Documentation/config.md
@@ -18,8 +18,9 @@ ``` owners.expandGroups -: Expand owners and groups into account ids. If set to `false` all owners are left untouched, apart from e-mail - addresses which have the domain dropped. Defaults to `true`. +: Expand owners and groups into account ids. If set to `false` all owners are left untouched, +apart from e-mail +addresses which have the domain dropped. Defaults to `true`. Example: @@ -30,8 +31,8 @@ owners.label : Global override for the label and score, separated by a comma, to use by - the owners of changes for approving them. When defined, it overrides any - other label definition set by the OWNERS at any level in any project. +the owners of changes for approving them. When defined, it overrides any +other label definition set by the OWNERS at any level in any project. > **NOTE:** Compulsory when the selected label's function is NoBlock/NoOp. @@ -44,10 +45,10 @@ <a name="owners.enableSubmitRequirement">owners.enableSubmitRequirement</a> : If set to `true` the approvals are evaluated through the owners plugin - default submit requirement, named "Code-Review-from-Owners", without a need of - prolog predicate being added to a project or submit requirement configured - in the `project.config` as it is automatically applied to all projects. - Defaults to `false`. +default submit requirement, named "Code-Review-from-Owners", without a need of +prolog predicate being added to a project or submit requirement configured +in the `project.config` as it is automatically applied to all projects. +Defaults to `false`. Example: @@ -80,19 +81,18 @@ > submittableIf = has:approval_owners > ``` - cache."owners.path_owners_entries".memoryLimit : The cache is used to hold the parsed version of `OWNERS` files in the - repository so that when submit rules are calculated (either through prolog - or through submit requirements) it is not read over and over again. The - cache entry gets invalidated when `OWNERS` file branch is updated. - By default it follows default Gerrit's cache memory limit but it makes - sense to adjust it as a function of number of project that use the `owners` - plugin multiplied by average number of active branches (plus 1 for the - refs/meta/config) and average number of directories (as directory hierarchy - back to root is checked for the `OWNERS` file existence). - _Note that in opposite to the previous settings the modification needs to be - performed in the `$GERRIT_SITE/etc/gerrit.config` file._ +repository so that when submit rules are calculated (either through prolog +or through submit requirements) it is not read over and over again. The +cache entry gets invalidated when `OWNERS` file branch is updated. +By default it follows default Gerrit's cache memory limit but it makes +sense to adjust it as a function of number of project that use the `owners` +plugin multiplied by average number of active branches (plus 1 for the +refs/meta/config) and average number of directories (as directory hierarchy +back to root is checked for the `OWNERS` file existence). +_Note that in opposite to the previous settings the modification needs to be +performed in the `$GERRIT_SITE/etc/gerrit.config` file._ Example @@ -112,20 +112,20 @@ inherited: true label: Code-Review, 1 owners: -- some.email@example.com -- User Name -- group/Group of Users + - some.email@example.com + - User Name + - group/Group of Users matchers: -- suffix: .java - owners: - [...] -- regex: .*/README.* - owners: - [...] -- partial_regex: example - owners: - [...] -- exact: path/to/file.txt + - suffix: .java + owners: + [ ... ] + - regex: .*/README.* + owners: + [ ... ] + - partial_regex: example + owners: + [ ... ] + - exact: path/to/file.txt [...] ``` @@ -190,9 +190,9 @@ ```yaml matchers: -- suffix: .config - owners: - - Configuration Managers + - suffix: .config + owners: + - Configuration Managers ``` Global refs/meta/config OWNERS configuration is inherited only when the OWNERS file @@ -204,6 +204,61 @@ If the global project OWNERS has the 'inherited: true', it will check for a global project OWNERS in all parent projects up to All-Projects. +## auto-owners-approved + +The `auto-owners-approved` field controls a specific exception to the default +`approverin:already-approved-by_owners` behavior. It applies when a new patch-set updates only files +that are owned by the change owner or patch-set committer, in a situation where the normal +`approverin:already-approved-by_owners` logic would otherwise drop that owner's previous vote. + +The rationale is simple: if an owner already approved a change that stays entirely within code they +own, and the next patch set is uploaded by that same owner, forcing that same person to re-apply +the same vote adds little review value. + +See [copy-conditions.md](copy-conditions.md) for predicate evaluation details. + +This field can be configured at `OWNERS` file level and on individual matchers. +If it is not set, it defaults to `true`. + +If `auto-owners-approved` is `false` for any touched file, the predicate does not use that +self-update shortcut for the patch set. The usual `approverin:already-approved-by_owners` logic +still applies. + +When a matcher defines `auto-owners-approved`, that matcher-specific value takes precedence for the +files it matches over the surrounding `OWNERS` value. + +### Inheritance + +The usual `OWNERS` [inheritance](#global-project-owners) logic applies to `auto-owners-approved` as +well. This includes directory `OWNERS` lookup, project `refs/meta/config` `OWNERS`, and +parent project `OWNERS` when inheritance continues up the project hierarchy. + +### auto-owners-approved example + +Disable at `OWNERS` level: + + inherited: true + auto-owners-approved: false + +With this setting, the predicate will not copy an owner's vote just because the owner is updating +only files they own on their own change. Paths under that `OWNERS` file still participate in the +normal copy-condition behavior. + +Override that setting for matched files: + +```yaml +inherited: true +auto-owners-approved: false +matchers: + - suffix: .java + auto-owners-approved: true + owners: + - user-backend +``` + +Here, `.java` files matched by that rule use `auto-owners-approved: true` even though the enclosing +`OWNERS` file sets it to `false`. + ## Example 1 - OWNERS file without matchers Given an OWNERS configuration of: @@ -211,8 +266,8 @@ ```yaml inherited: true owners: -- John Doe -- Doug Smith + - John Doe + - Doug Smith ``` In this case the owners plugin will assume the default label configuration,`Code-Review @@ -222,6 +277,7 @@ change, you can then either enable `owners.enableSubmitRequirement = true` in your `gerrit.config` or define a submit requirement in your `project.config` that uses the `has:approval_owners` in the `submittableIf` section, like so: + ``` [submit-requirement "Owner-Approval"] description = Files needs to be approved by owners @@ -263,8 +319,8 @@ inherited: true label: Owner-Approved, 1 owners: -- John Doe -- Doug Smith + - John Doe + - Doug Smith ``` This will mean that, a change cannot be submitted until 'John Doe' or 'Doug @@ -279,6 +335,7 @@ If you no longer wish to require a `Code-Review +2` and would rather only use the custom submit requirement, you have two options: + - change the definition of the `Code-Review` label in `All-Projects`'s `project.config` so that `function = NoOp`. - set an `overrideIf` clause in your custom submit requirement definition @@ -302,13 +359,13 @@ ```yaml inherited: true matchers: -- suffix: .sql - owners: - - Mister Dba -- regex: .*Test.* - owners: - - John Bug - - Matt Free + - suffix: .sql + owners: + - Mister Dba + - regex: .*Test.* + owners: + - John Bug + - Matt Free ``` You can then either enable `owners.enableSubmitRequirement = true` in your
diff --git a/owners/src/main/resources/Documentation/copy-conditions.md b/owners/src/main/resources/Documentation/copy-conditions.md index b7f617b..b318028 100644 --- a/owners/src/main/resources/Documentation/copy-conditions.md +++ b/owners/src/main/resources/Documentation/copy-conditions.md
@@ -118,3 +118,13 @@ value = +2 Looks good to me, approved copyCondition = approverin:already-approved-by_owners ``` + +## Customizing the copy condition behaviour with the `auto-owners-approved` in `OWNERS` + +The `approverin:already-approved-by_owners` can be fine-grained enabled or disabled using +`auto-owners-approved` in `OWNERS`, and more narrowly on individual matchers. +When both are present, the matcher value wins for the files that matcher selects. +See the examples in [config.md](./config.md#auto-owners-approved). + +Details on the `auto-owners-approved` behaviour can be +found [here](./config.md#auto-owners-approved).
diff --git a/owners/src/test/java/com/googlesource/gerrit/owners/AlreadyApprovedByCopyConditionIT.java b/owners/src/test/java/com/googlesource/gerrit/owners/AlreadyApprovedByCopyConditionIT.java index ec0e92e..70ded46 100644 --- a/owners/src/test/java/com/googlesource/gerrit/owners/AlreadyApprovedByCopyConditionIT.java +++ b/owners/src/test/java/com/googlesource/gerrit/owners/AlreadyApprovedByCopyConditionIT.java
@@ -33,7 +33,7 @@ import com.google.gerrit.acceptance.testsuite.change.ChangeOperations; import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; -import com.google.gerrit.entities.Change; +import com.google.gerrit.entities.Account; import com.google.gerrit.entities.LabelId; import com.google.gerrit.entities.LabelType; import com.google.gerrit.entities.Permission; @@ -63,10 +63,14 @@ private TestAccount FRONTEND_FILES_OWNER; private TestAccount BACKEND_FILES_OWNER; + private TestAccount NON_OWNER; + private static final String BACKEND_OWNED_FILE_PATH = "path/to/"; private static final String FRONTEND_OWNED_FILE = "foo.js"; - private static final String BACKEND_OWNED_FILE = "foo.java"; + private static final String BACKEND_OWNED_FILE = BACKEND_OWNED_FILE_PATH + "foo.java"; private static final String FILE_WITH_NO_OWNERS = "foo.txt"; + private static final boolean AUTO_OWNERS_APPROVED_ENABLED = true; + private static final boolean AUTO_OWNERS_APPROVAL_DISABLED = false; private static final String FILE_CONTENT = IntStream.rangeClosed(1, 10) @@ -88,6 +92,7 @@ FRONTEND_FILES_OWNER = accountCreator.create("user-frontend"); BACKEND_FILES_OWNER = accountCreator.create("user-backend"); + NON_OWNER = accountCreator.create("user-non-owner"); addOwnerFileWithMatchersToRoot( Map.of( @@ -99,105 +104,71 @@ @Test public void shouldCopyOwnerApprovalOnlyWhenOwnedFilesAreUnchanged() throws Exception { - ChangeIdentifier changeId = - changeOperations - .newChange() - .project(project) - .file(FRONTEND_OWNED_FILE) - .content("some frontend change") - .create(); + ChangeIdentifier changeId = createChange(FRONTEND_OWNED_FILE, "some frontend change"); vote(FRONTEND_FILES_OWNER, changeId.toString(), 2); vote(BACKEND_FILES_OWNER, changeId.toString(), 2); - changeOperations - .change(changeId) - .newPatchset() - .file(BACKEND_OWNED_FILE) - .content("some java content") - .create(); + createPatchSet(changeId, BACKEND_OWNED_FILE, "some java content"); - ChangeInfo c = detailedChange(changeId.toString()); - assertVotes(c, FRONTEND_FILES_OWNER, 2); - assertVotes(c, BACKEND_FILES_OWNER, 0); + assertVote(changeId, FRONTEND_FILES_OWNER, 2); + assertVote(changeId, BACKEND_FILES_OWNER, 0); } @Test public void shouldNotCopyApprovalForOwnerWhenNoOwnedFileExists() throws Exception { - ChangeIdentifier changeId = - changeOperations - .newChange() - .project(project) - .file(FILE_WITH_NO_OWNERS) - .content("file with no owners") - .create(); + ChangeIdentifier changeId = createChange(FILE_WITH_NO_OWNERS, "file with no owners"); vote(FRONTEND_FILES_OWNER, changeId.toString(), 2); - changeOperations - .change(changeId) - .newPatchset() - .file(FILE_WITH_NO_OWNERS) - .content("updated text") - .create(); + createPatchSet(changeId, FILE_WITH_NO_OWNERS, "updated text"); - ChangeInfo c = detailedChange(changeId.toString()); - - assertVotes(c, FRONTEND_FILES_OWNER, 0); + assertVote(changeId, FRONTEND_FILES_OWNER, 0); } @Test public void shouldNotCopyApprovalWhenOwnedFilesAreIntroducedInLaterPatchSet() throws Exception { - ChangeIdentifier changeId = - changeOperations - .newChange() - .project(project) - .file(FILE_WITH_NO_OWNERS) - .content("file with no owners") - .create(); + ChangeIdentifier changeId = createChange(FILE_WITH_NO_OWNERS, "file with no owners"); + + vote(BACKEND_FILES_OWNER, changeId.toString(), 2); + + createPatchSet(changeId, BACKEND_OWNED_FILE, "some java content"); + + assertVote(changeId, BACKEND_FILES_OWNER, 0); + } + + @Test + public void shouldCopyApprovalWhenOnlyCommitMessageChangesInNextPatchSet() throws Exception { + String ownedFile = "file.txt"; + pushOwnersToMaster(ownersConfigFor(BACKEND_FILES_OWNER)); + + ChangeIdentifier changeId = createChange(ownedFile, "java content"); vote(BACKEND_FILES_OWNER, changeId.toString(), 2); changeOperations .change(changeId) .newPatchset() - .file(BACKEND_OWNED_FILE) - .content("some java content") + .commitMessage("Updated commit message") .create(); - ChangeInfo c = detailedChange(changeId.toString()); - - assertVotes(c, BACKEND_FILES_OWNER, 0); + assertVote(changeId, BACKEND_FILES_OWNER, 2); } @Test public void shouldNotCopyApprovalWhenOwnedFileIsDeleted() throws Exception { - ChangeIdentifier changeId = - changeOperations - .newChange() - .project(project) - .file(BACKEND_OWNED_FILE) - .content("java content") - .create(); + ChangeIdentifier changeId = createChange(BACKEND_OWNED_FILE, "java content"); vote(BACKEND_FILES_OWNER, changeId.toString(), 2); changeOperations.change(changeId).newPatchset().file(BACKEND_OWNED_FILE).delete().create(); - ChangeInfo c = detailedChange(changeId.toString()); - - assertVotes(c, BACKEND_FILES_OWNER, 0); + assertVote(changeId, BACKEND_FILES_OWNER, 0); } @Test public void shouldNotCopyApprovalWhenOwnedFileIsRenamedToOwnedFile() throws Exception { - ChangeIdentifier changeId = - changeOperations - .newChange() - .project(project) - .file(BACKEND_OWNED_FILE) - .content("java content") - .create(); + ChangeIdentifier changeId = createChange(BACKEND_OWNED_FILE, "java content"); vote(BACKEND_FILES_OWNER, changeId.toString(), 2); @@ -208,20 +179,12 @@ .renameTo("renamed-" + BACKEND_OWNED_FILE) .create(); - ChangeInfo c = detailedChange(changeId.toString()); - - assertVotes(c, BACKEND_FILES_OWNER, 0); + assertVote(changeId, BACKEND_FILES_OWNER, 0); } @Test public void shouldNotCopyApprovalWhenOwnedFileIsRenamedToNonOwnedFile() throws Exception { - ChangeIdentifier changeId = - changeOperations - .newChange() - .project(project) - .file(BACKEND_OWNED_FILE) - .content("java content") - .create(); + ChangeIdentifier changeId = createChange(BACKEND_OWNED_FILE, "java content"); vote(BACKEND_FILES_OWNER, changeId.toString(), 2); @@ -232,9 +195,7 @@ .renameTo(FILE_WITH_NO_OWNERS) .create(); - ChangeInfo c = detailedChange(changeId.toString()); - - assertVotes(c, BACKEND_FILES_OWNER, 0); + assertVote(changeId, BACKEND_FILES_OWNER, 0); } @Test @@ -250,8 +211,7 @@ rebaseChangeOn(amendL3.getChangeId(), amendL7.getCommit().getId()); - ChangeInfo c = detailedChange(amendL3.getChangeId()); - assertVotes(c, BACKEND_FILES_OWNER, 2); + assertVote(amendL3.getChangeId(), BACKEND_FILES_OWNER, 2); } @Test @@ -300,8 +260,7 @@ amendL3.getChangeId(), "Rebased with changes", "unrelated.txt", "Unrelated change"); rebasedL30WithUnrelatedChanges.assertOkStatus(); - ChangeInfo c = detailedChange(amendL3.getChangeId()); - assertVotes(c, BACKEND_FILES_OWNER, 2); + assertVote(amendL3.getChangeId(), BACKEND_FILES_OWNER, 2); } @Test @@ -327,8 +286,7 @@ "Line 1\n"); rebasedL30PlusOtherChangesToOwnedFile.assertOkStatus(); - ChangeInfo c = detailedChange(amendL3.getChangeId()); - assertVotes(c, FRONTEND_FILES_OWNER, 0); + assertVote(amendL3.getChangeId(), FRONTEND_FILES_OWNER, 0); } @Test @@ -353,8 +311,7 @@ FILE_CONTENT.replace("Line 5\n", "Line five\n")); rebasedL3WithUnrelatedChanges.assertOkStatus(); - ChangeInfo c = detailedChange(amendL3.getChangeId()); - assertVotes(c, BACKEND_FILES_OWNER, 0); + assertVote(amendL3.getChangeId(), BACKEND_FILES_OWNER, 0); } @Test @@ -362,38 +319,405 @@ createInitialContentFor(BACKEND_OWNED_FILE); ObjectId latestMasterCommitId = createInitialContentFor("another-owned.java"); - ChangeIdentifier removedFileChangeId = - changeOperations.newChange().project(project).file("another-owned.java").delete().create(); + ChangeIdentifier removedFileChangeId = newChangeDeletingFile("another-owned.java"); vote(BACKEND_FILES_OWNER, removedFileChangeId.toString(), 2); testRepo.reset(latestMasterCommitId); - ChangeIdentifier baseRemovedFileChangeId = - changeOperations.newChange().project(project).file(BACKEND_OWNED_FILE).delete().create(); + ChangeIdentifier baseRemovedFileChangeId = newChangeDeletingFile(BACKEND_OWNED_FILE); rebaseChangeOn(removedFileChangeId.toString(), commitOf(baseRemovedFileChangeId)); - ChangeInfo c = detailedChange(removedFileChangeId.toString()); - assertVotes(c, BACKEND_FILES_OWNER, 2); + assertVote(removedFileChangeId, BACKEND_FILES_OWNER, 2); } @Test public void shouldCopyApprovalWhenRemovingOwnedFileAndRebaseOnChangeThatRemovesTheSameFile() throws Exception { ObjectId initialCommitId = createInitialContentFor(BACKEND_OWNED_FILE); - ChangeIdentifier removedFileChangeId = - changeOperations.newChange().project(project).file(BACKEND_OWNED_FILE).delete().create(); + ChangeIdentifier removedFileChangeId = newChangeDeletingFile(BACKEND_OWNED_FILE); vote(BACKEND_FILES_OWNER, removedFileChangeId.toString(), 2); testRepo.reset(initialCommitId); - ChangeIdentifier baseRemovedFileChangeId = - changeOperations.newChange().project(project).file(BACKEND_OWNED_FILE).delete().create(); + ChangeIdentifier baseRemovedFileChangeId = newChangeDeletingFile(BACKEND_OWNED_FILE); rebaseChangeOn(removedFileChangeId.toString(), commitOf(baseRemovedFileChangeId)); - ChangeInfo c = detailedChange(removedFileChangeId.toString()); - assertVotes(c, BACKEND_FILES_OWNER, 2); + assertVote(removedFileChangeId, BACKEND_FILES_OWNER, 2); + } + + @Test + public void shouldCopyApprovalWhenAllModifiedFilesAreOwnedAndAutoOwnersApprovedIsDefault() + throws Exception { + pushOwnersToMaster(ownersConfigFor(BACKEND_FILES_OWNER)); + + ChangeIdentifier changeId = + createChange(BACKEND_FILES_OWNER.id(), BACKEND_OWNED_FILE, "java content"); + + vote(BACKEND_FILES_OWNER, changeId.toString(), 2); + createPatchSet(changeId, BACKEND_FILES_OWNER.id(), BACKEND_OWNED_FILE, "updated java content"); + + assertVote(changeId, BACKEND_FILES_OWNER, 2); + } + + @Test + public void + shouldCopyApprovalWhenAllModifiedFilesAreOwnedAndAutoOwnersApprovedIsDisabledOnRepoAndEnabledOnRoot() + throws Exception { + pushOwnersToRef( + "inherited: true\nauto-owners-approved: false\n", "OWNERS", RefNames.REFS_CONFIG); + + pushOwnersToMaster(ownersConfigFor(BACKEND_FILES_OWNER, AUTO_OWNERS_APPROVED_ENABLED)); + + ChangeIdentifier changeId = + createChange(BACKEND_FILES_OWNER.id(), BACKEND_OWNED_FILE, "java content"); + + vote(BACKEND_FILES_OWNER, changeId.toString(), 2); + createPatchSet(changeId, BACKEND_FILES_OWNER.id(), BACKEND_OWNED_FILE, "updated java content"); + + assertVote(changeId, BACKEND_FILES_OWNER, 2); + } + + @Test + public void + shouldCopyApprovalWhenAllModifiedFilesAreOwnedAndAutoOwnersApprovedIsDisabledOnRootAndEnabledOnPath() + throws Exception { + pushOwnersToRef( + "inherited: true\nauto-owners-approved: false\n", "OWNERS", RefNames.fullName("master")); + + pushOwnersToRef( + ownersConfigFor(BACKEND_FILES_OWNER, AUTO_OWNERS_APPROVED_ENABLED), + BACKEND_OWNED_FILE_PATH + "OWNERS", + RefNames.fullName("master")); + + ChangeIdentifier changeId = + createChange(BACKEND_FILES_OWNER.id(), BACKEND_OWNED_FILE, "java content"); + + vote(BACKEND_FILES_OWNER, changeId.toString(), 2); + createPatchSet(changeId, BACKEND_FILES_OWNER.id(), BACKEND_OWNED_FILE, "updated java content"); + + assertVote(changeId, BACKEND_FILES_OWNER, 2); + } + + @Test + public void shouldNotCopyApprovalWhenAllModifiedFilesAreOwnedButApproverIsNotChangeOwner() + throws Exception { + pushOwnersToMaster(ownersConfigFor(BACKEND_FILES_OWNER)); + + ChangeIdentifier changeId = createChange(NON_OWNER.id(), BACKEND_OWNED_FILE, "java content"); + + vote(BACKEND_FILES_OWNER, changeId.toString(), 2); + createPatchSet(changeId, BACKEND_FILES_OWNER.id(), BACKEND_OWNED_FILE, "updated java content"); + + assertVote(changeId, BACKEND_FILES_OWNER, 0); + } + + @Test + public void shouldNotCopyApprovalWhenAllModifiedFilesAreOwnedButUploaderNotOwner() + throws Exception { + pushOwnersToMaster(ownersConfigFor(BACKEND_FILES_OWNER)); + + ChangeIdentifier changeId = + createChange(BACKEND_FILES_OWNER.id(), BACKEND_OWNED_FILE, "java content"); + + vote(BACKEND_FILES_OWNER, changeId.toString(), 2); + createPatchSet(changeId, NON_OWNER.id(), BACKEND_OWNED_FILE, "updated java content"); + + assertVote(changeId, BACKEND_FILES_OWNER, 0); + } + + @Test + public void shouldNotCopyApprovalWhenAllModifiedFilesAreOwnedButAutoOwnersApprovedIsFalse() + throws Exception { + pushOwnersToMaster(ownersConfigFor(BACKEND_FILES_OWNER, AUTO_OWNERS_APPROVAL_DISABLED)); + + ChangeIdentifier changeId = + createChange(BACKEND_FILES_OWNER.id(), BACKEND_OWNED_FILE, "java content"); + + vote(BACKEND_FILES_OWNER, changeId.toString(), 2); + createPatchSet(changeId, BACKEND_FILES_OWNER.id(), BACKEND_OWNED_FILE, "updated java content"); + + assertVote(changeId, BACKEND_FILES_OWNER, 0); + } + + @Test + public void shouldCopyApprovalWhenAllModifiedFilesAreOwnedAndAutoOwnersApprovedIsTrue() + throws Exception { + pushOwnersToMaster(ownersConfigFor(BACKEND_FILES_OWNER, AUTO_OWNERS_APPROVED_ENABLED)); + + ChangeIdentifier changeId = + createChange(BACKEND_FILES_OWNER.id(), BACKEND_OWNED_FILE, "java content"); + + vote(BACKEND_FILES_OWNER, changeId.toString(), 2); + createPatchSet(changeId, BACKEND_FILES_OWNER.id(), BACKEND_OWNED_FILE, "updated java content"); + + assertVote(changeId, BACKEND_FILES_OWNER, 2); + } + + @Test + public void shouldCopyApprovalWhenAutoOwnersApprovedIsFalseButOwnedEditsAreRebaseOnly() + throws Exception { + pushOwnersToMaster(ownersConfigFor(BACKEND_FILES_OWNER, AUTO_OWNERS_APPROVAL_DISABLED)); + + ObjectId initialCommitId = createInitialContentFor(BACKEND_OWNED_FILE); + PushOneCommit.Result amendL3 = + createChangeWithReplacedContent(BACKEND_OWNED_FILE, "Line 3\n", "Line three\n"); + vote(BACKEND_FILES_OWNER, amendL3.getChangeId(), 2); + + testRepo.reset(initialCommitId); + PushOneCommit.Result amendL7 = + createChangeWithReplacedContent(BACKEND_OWNED_FILE, "Line 7\n", "Line seven\n"); + + rebaseChangeOn(amendL3.getChangeId(), amendL7.getCommit().getId()); + + assertVote(amendL3.getChangeId(), BACKEND_FILES_OWNER, 2); + } + + @Test + public void shouldCopyApprovalWhenMatcherEnablesAutoOwnersApproved() throws Exception { + pushOwnersToMaster( + matcherOwnersConfig( + suffixMatcherConfig(".java", BACKEND_FILES_OWNER, AUTO_OWNERS_APPROVED_ENABLED))); + + ChangeIdentifier changeId = + createChange(BACKEND_FILES_OWNER.id(), BACKEND_OWNED_FILE, "java content"); + vote(BACKEND_FILES_OWNER, changeId.toString(), 2); + + createPatchSet(changeId, BACKEND_FILES_OWNER.id(), BACKEND_OWNED_FILE, "updated java content"); + + assertVote(changeId, BACKEND_FILES_OWNER, 2); + } + + @Test + public void shouldNotCopyApprovalWhenMatcherDisablesAutoOwnersApproved() throws Exception { + pushOwnersToMaster( + matcherOwnersConfig( + suffixMatcherConfig(".java", BACKEND_FILES_OWNER, AUTO_OWNERS_APPROVAL_DISABLED))); + + ChangeIdentifier changeId = + createChange(BACKEND_FILES_OWNER.id(), BACKEND_OWNED_FILE, "java content"); + vote(BACKEND_FILES_OWNER, changeId.toString(), 2); + + createPatchSet(changeId, BACKEND_FILES_OWNER.id(), BACKEND_OWNED_FILE, "updated java content"); + + assertVote(changeId, BACKEND_FILES_OWNER, 0); + } + + @Test + public void shouldCopyApprovalWhenMatcherOverridesRootFalse() throws Exception { + pushOwnersToMaster( + matcherOwnersConfig( + AUTO_OWNERS_APPROVAL_DISABLED, + suffixMatcherConfig(".java", BACKEND_FILES_OWNER, AUTO_OWNERS_APPROVED_ENABLED))); + + ChangeIdentifier changeId = + createChange(BACKEND_FILES_OWNER.id(), BACKEND_OWNED_FILE, "java content"); + vote(BACKEND_FILES_OWNER, changeId.toString(), 2); + + createPatchSet(changeId, BACKEND_FILES_OWNER.id(), BACKEND_OWNED_FILE, "updated java content"); + + assertVote(changeId, BACKEND_FILES_OWNER, 2); + } + + @Test + public void shouldNotCopyApprovalWhenMatcherOverridesRootTrue() throws Exception { + pushOwnersToMaster( + matcherOwnersConfig( + AUTO_OWNERS_APPROVED_ENABLED, + suffixMatcherConfig(".java", BACKEND_FILES_OWNER, AUTO_OWNERS_APPROVAL_DISABLED))); + + ChangeIdentifier changeId = + createChange(BACKEND_FILES_OWNER.id(), BACKEND_OWNED_FILE, "java content"); + vote(BACKEND_FILES_OWNER, changeId.toString(), 2); + + createPatchSet(changeId, BACKEND_FILES_OWNER.id(), BACKEND_OWNED_FILE, "updated java content"); + + assertVote(changeId, BACKEND_FILES_OWNER, 0); + } + + @Test + public void shouldCopyApprovalWhenChildMatcherInheritsParentTrue() throws Exception { + pushOwnersToMaster(ownersConfigFor(BACKEND_FILES_OWNER, AUTO_OWNERS_APPROVED_ENABLED)); + pushOwnersToRef( + matcherOwnersConfig(suffixMatcherConfig(".java", BACKEND_FILES_OWNER)), + BACKEND_OWNED_FILE_PATH + "OWNERS", + RefNames.fullName("master")); + + ChangeIdentifier changeId = + createChange(BACKEND_FILES_OWNER.id(), BACKEND_OWNED_FILE, "java content"); + vote(BACKEND_FILES_OWNER, changeId.toString(), 2); + + createPatchSet(changeId, BACKEND_FILES_OWNER.id(), BACKEND_OWNED_FILE, "updated java content"); + + assertVote(changeId, BACKEND_FILES_OWNER, 2); + } + + @Test + public void shouldNotCopyApprovalWhenChildMatcherInheritsParentFalse() throws Exception { + pushOwnersToMaster(ownersConfigFor(BACKEND_FILES_OWNER, AUTO_OWNERS_APPROVAL_DISABLED)); + pushOwnersToRef( + matcherOwnersConfig(suffixMatcherConfig(".java", BACKEND_FILES_OWNER)), + BACKEND_OWNED_FILE_PATH + "OWNERS", + RefNames.fullName("master")); + + ChangeIdentifier changeId = + createChange(BACKEND_FILES_OWNER.id(), BACKEND_OWNED_FILE, "java content"); + vote(BACKEND_FILES_OWNER, changeId.toString(), 2); + + createPatchSet(changeId, BACKEND_FILES_OWNER.id(), BACKEND_OWNED_FILE, "updated java content"); + + assertVote(changeId, BACKEND_FILES_OWNER, 0); + } + + @Test + public void shouldCopyApprovalWhenChildMatcherOverridesParentFalse() throws Exception { + pushOwnersToMaster(ownersConfigFor(BACKEND_FILES_OWNER, AUTO_OWNERS_APPROVAL_DISABLED)); + pushOwnersToRef( + matcherOwnersConfig( + suffixMatcherConfig(".java", BACKEND_FILES_OWNER, AUTO_OWNERS_APPROVED_ENABLED)), + BACKEND_OWNED_FILE_PATH + "OWNERS", + RefNames.fullName("master")); + + ChangeIdentifier changeId = + createChange(BACKEND_FILES_OWNER.id(), BACKEND_OWNED_FILE, "java content"); + vote(BACKEND_FILES_OWNER, changeId.toString(), 2); + + createPatchSet(changeId, BACKEND_FILES_OWNER.id(), BACKEND_OWNED_FILE, "updated java content"); + + assertVote(changeId, BACKEND_FILES_OWNER, 2); + } + + @Test + public void shouldNotCopyApprovalWhenChildMatcherOverridesParentTrue() throws Exception { + pushOwnersToMaster(ownersConfigFor(BACKEND_FILES_OWNER, AUTO_OWNERS_APPROVED_ENABLED)); + pushOwnersToRef( + matcherOwnersConfig( + suffixMatcherConfig(".java", BACKEND_FILES_OWNER, AUTO_OWNERS_APPROVAL_DISABLED)), + BACKEND_OWNED_FILE_PATH + "OWNERS", + RefNames.fullName("master")); + + ChangeIdentifier changeId = + createChange(BACKEND_FILES_OWNER.id(), BACKEND_OWNED_FILE, "java content"); + vote(BACKEND_FILES_OWNER, changeId.toString(), 2); + + createPatchSet(changeId, BACKEND_FILES_OWNER.id(), BACKEND_OWNED_FILE, "updated java content"); + + assertVote(changeId, BACKEND_FILES_OWNER, 0); + } + + @Test + public void shouldCopyApprovalWhenChildMatcherOverridesParentMatcherFalse() throws Exception { + pushOwnersToMaster( + matcherOwnersConfig( + suffixMatcherConfig(".java", BACKEND_FILES_OWNER, AUTO_OWNERS_APPROVAL_DISABLED))); + pushOwnersToRef( + matcherOwnersConfig( + suffixMatcherConfig(".java", BACKEND_FILES_OWNER, AUTO_OWNERS_APPROVED_ENABLED)), + BACKEND_OWNED_FILE_PATH + "OWNERS", + RefNames.fullName("master")); + + ChangeIdentifier changeId = + createChange(BACKEND_FILES_OWNER.id(), BACKEND_OWNED_FILE, "java content"); + vote(BACKEND_FILES_OWNER, changeId.toString(), 2); + + createPatchSet(changeId, BACKEND_FILES_OWNER.id(), BACKEND_OWNED_FILE, "updated java content"); + + assertVote(changeId, BACKEND_FILES_OWNER, 2); + } + + @Test + public void shouldNotCopyApprovalWhenChildMatcherOverridesParentMatcherTrue() throws Exception { + pushOwnersToMaster( + matcherOwnersConfig( + suffixMatcherConfig(".java", BACKEND_FILES_OWNER, AUTO_OWNERS_APPROVED_ENABLED))); + pushOwnersToRef( + matcherOwnersConfig( + suffixMatcherConfig(".java", BACKEND_FILES_OWNER, AUTO_OWNERS_APPROVAL_DISABLED)), + BACKEND_OWNED_FILE_PATH + "OWNERS", + RefNames.fullName("master")); + + ChangeIdentifier changeId = + createChange(BACKEND_FILES_OWNER.id(), BACKEND_OWNED_FILE, "java content"); + vote(BACKEND_FILES_OWNER, changeId.toString(), 2); + + createPatchSet(changeId, BACKEND_FILES_OWNER.id(), BACKEND_OWNED_FILE, "updated java content"); + + assertVote(changeId, BACKEND_FILES_OWNER, 0); + } + + @Test + public void shouldNotCopyApprovalWhenPathAndMatcherFilesDoNotAllAllowAutoOwnersApproved() + throws Exception { + pushOwnersToMaster(ownersConfigFor(BACKEND_FILES_OWNER, AUTO_OWNERS_APPROVAL_DISABLED)); + pushOwnersToRef( + matcherOwnersConfig( + suffixMatcherConfig(".java", BACKEND_FILES_OWNER, AUTO_OWNERS_APPROVED_ENABLED)), + BACKEND_OWNED_FILE_PATH + "OWNERS", + RefNames.fullName("master")); + + String directoryOwnedFile = "directory-owned.txt"; + ChangeIdentifier changeId = + changeOperations + .newChange() + .project(project) + .owner(BACKEND_FILES_OWNER.id()) + .file(directoryOwnedFile) + .content("foo") + .file(BACKEND_OWNED_FILE) + .content("bar") + .create(); + + vote(BACKEND_FILES_OWNER, changeId.toString(), 2); + + changeOperations + .change(changeId) + .newPatchset() + .uploader(BACKEND_FILES_OWNER.id()) + .file(directoryOwnedFile) + .content("updated foo") + .file(BACKEND_OWNED_FILE) + .content("updated bar") + .create(); + + assertVote(changeId, BACKEND_FILES_OWNER, 0); + } + + @Test + public void shouldCopyApprovalWhenPathAndMatcherFilesAllAllowAutoOwnersApproved() + throws Exception { + pushOwnersToMaster(ownersConfigFor(BACKEND_FILES_OWNER, AUTO_OWNERS_APPROVED_ENABLED)); + pushOwnersToRef( + matcherOwnersConfig( + AUTO_OWNERS_APPROVED_ENABLED, suffixMatcherConfig(".java", BACKEND_FILES_OWNER)), + BACKEND_OWNED_FILE_PATH + "OWNERS", + RefNames.fullName("master")); + + String directoryOwnedFile = "directory-owned.txt"; + ChangeIdentifier changeId = + changeOperations + .newChange() + .project(project) + .owner(BACKEND_FILES_OWNER.id()) + .file(directoryOwnedFile) + .content("foo") + .file(BACKEND_OWNED_FILE) + .content("bar") + .create(); + + vote(BACKEND_FILES_OWNER, changeId.toString(), 2); + + changeOperations + .change(changeId) + .newPatchset() + .uploader(BACKEND_FILES_OWNER.id()) + .file(directoryOwnedFile) + .content("updated foo") + .file(BACKEND_OWNED_FILE) + .content("updated bar") + .create(); + + assertVote(changeId, BACKEND_FILES_OWNER, 2); } private PushOneCommit.Result createChangeWithReplacedContent( @@ -416,8 +740,7 @@ } private ObjectId commitOf(ChangeIdentifier changeId) throws Exception { - RevisionInfo currentRevision = - gApi.changes().id(changeId).get().getCurrentRevision(); + RevisionInfo currentRevision = gApi.changes().id(changeId).get().getCurrentRevision(); return ObjectId.fromString(currentRevision.commit.commit); } @@ -446,6 +769,84 @@ assertThat(vote).isEqualTo(expectedVote); } + private void assertVote(ChangeIdentifier changeId, TestAccount user, int expectedVote) + throws Exception { + assertVotes(detailedChange(changeId.toString()), user, expectedVote); + } + + private void assertVote(String changeId, TestAccount user, int expectedVote) throws Exception { + assertVotes(detailedChange(changeId), user, expectedVote); + } + + private ChangeIdentifier createChange(String file, String content) throws Exception { + return changeOperations.newChange().project(project).file(file).content(content).create(); + } + + private ChangeIdentifier createChange(Account.Id owner, String file, String content) + throws Exception { + return changeOperations + .newChange() + .project(project) + .owner(owner) + .file(file) + .content(content) + .create(); + } + + private ChangeIdentifier newChangeDeletingFile(String file) throws Exception { + return changeOperations.newChange().project(project).file(file).delete().create(); + } + + private void createPatchSet(ChangeIdentifier changeId, String file, String content) + throws Exception { + changeOperations.change(changeId).newPatchset().file(file).content(content).create(); + } + + private void createPatchSet( + ChangeIdentifier changeId, Account.Id uploader, String file, String content) + throws Exception { + changeOperations + .change(changeId) + .newPatchset() + .uploader(uploader) + .file(file) + .content(content) + .create(); + } + + private String ownersConfigFor(TestAccount owner) { + return String.format("inherited: true\nowners:\n- %s\n", owner.username()); + } + + private String ownersConfigFor(TestAccount owner, boolean autoOwnersApproved) { + return String.format( + "inherited: true\nauto-owners-approved: %s\nowners:\n- %s\n", + autoOwnersApproved, owner.username()); + } + + private String matcherOwnersConfig(String... matchers) { + return matcherOwnersConfig(null, matchers); + } + + private String matcherOwnersConfig(Boolean autoOwnersApproved, String... matchers) { + String rootAutoOwnersApproved = + autoOwnersApproved == null + ? "" + : String.format("auto-owners-approved: %s\n", autoOwnersApproved); + return String.format( + "inherited: true\n%smatchers:\n%s", rootAutoOwnersApproved, String.join("", matchers)); + } + + private String suffixMatcherConfig(String suffix, TestAccount owner, boolean autoOwnersApproved) { + return String.format( + "- suffix: %s\n auto-owners-approved: %s\n owners:\n - %s\n", + suffix, autoOwnersApproved, owner.username()); + } + + private String suffixMatcherConfig(String suffix, TestAccount owner) { + return String.format("- suffix: %s\n owners:\n - %s\n", suffix, owner.username()); + } + private void vote(TestAccount user, String changeId, int vote) throws Exception { requestScopeOperations.setApiUser(user.id()); gApi.changes() @@ -493,10 +894,38 @@ pushOwnersToMaster(String.format("inherited: %s\nmatchers:\n%s", true, matchersYaml)); } + private void addOwnerFileWithMatchersToRoot( + Map<String, List<TestAccount>> ownersBySuffix, boolean autoOwnersApproved) throws Exception { + String matchersYaml = + ownersBySuffix.entrySet().stream() + .map( + entry -> { + String suffix = entry.getKey(); + List<TestAccount> users = entry.getValue(); + String ownersYaml = + users.stream() + .map(user -> String.format(" - %s\n", user.username())) + .collect(joining()); + return String.format("- suffix: %s\n owners:\n%s", suffix, ownersYaml); + }) + .collect(joining()); + + pushOwnersToMaster( + String.format( + "inherited: %s\nauto-owners-approved: %s\nmatchers:\n%s", + true, autoOwnersApproved, matchersYaml)); + } + private void pushOwnersToMaster(String owners) throws Exception { + pushOwnersToRef(owners, "OWNERS", RefNames.fullName("master")); + } + + private void pushOwnersToRef(String owners, String ownerPath, String ref) throws Exception { + testRepo.git().fetch().setRemote("origin").setRefSpecs(ref + ":" + ref).call(); + testRepo.reset(ref); pushFactory - .create(admin.newIdent(), testRepo, "Add OWNER file", "OWNERS", owners) - .to(RefNames.fullName("master")) + .create(admin.newIdent(), testRepo, "Add OWNER file", ownerPath, owners) + .to(ref) .assertOkStatus(); } }
diff --git a/owners/src/test/java/com/googlesource/gerrit/owners/OwnersSubmitRequirementITAbstract.java b/owners/src/test/java/com/googlesource/gerrit/owners/OwnersSubmitRequirementITAbstract.java index e7baba6..c318fe7 100644 --- a/owners/src/test/java/com/googlesource/gerrit/owners/OwnersSubmitRequirementITAbstract.java +++ b/owners/src/test/java/com/googlesource/gerrit/owners/OwnersSubmitRequirementITAbstract.java
@@ -39,7 +39,6 @@ import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.common.SubmitRecordInfo; import com.google.gerrit.extensions.restapi.RestApiException; -import com.google.gerrit.server.project.testing.TestLabels; import com.google.inject.Inject; import com.googlesource.gerrit.owners.common.LabelDefinition; import java.util.Collection;
diff --git a/owners/src/test/java/com/googlesource/gerrit/owners/restapi/GetFilesOwnersITAbstract.java b/owners/src/test/java/com/googlesource/gerrit/owners/restapi/GetFilesOwnersITAbstract.java index 7682434..ad46da6 100644 --- a/owners/src/test/java/com/googlesource/gerrit/owners/restapi/GetFilesOwnersITAbstract.java +++ b/owners/src/test/java/com/googlesource/gerrit/owners/restapi/GetFilesOwnersITAbstract.java
@@ -18,6 +18,7 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.gerrit.testing.GerritJUnit.assertThrows; +import com.google.common.collect.Sets; import com.google.gerrit.acceptance.GitUtil; import com.google.gerrit.acceptance.LightweightPluginDaemonTest; import com.google.gerrit.acceptance.PushOneCommit.Result; @@ -44,7 +45,6 @@ import com.googlesource.gerrit.owners.restapi.GetFilesOwners.LabelNotFoundException; import java.util.Map; import javax.servlet.http.HttpServletResponse; -import org.apache.commons.compress.utils.Sets; import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.transport.FetchResult;
diff --git a/owners/src/test/java/com/googlesource/gerrit/owners/restapi/GetFilesOwnersSubmitRequirementsIT.java b/owners/src/test/java/com/googlesource/gerrit/owners/restapi/GetFilesOwnersSubmitRequirementsIT.java index c316757..3915997 100644 --- a/owners/src/test/java/com/googlesource/gerrit/owners/restapi/GetFilesOwnersSubmitRequirementsIT.java +++ b/owners/src/test/java/com/googlesource/gerrit/owners/restapi/GetFilesOwnersSubmitRequirementsIT.java
@@ -19,7 +19,7 @@ import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel; import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; -import com.google.gerrit.acceptance.TestAccount; +import com.google.common.collect.Sets; import com.google.gerrit.acceptance.TestPlugin; import com.google.gerrit.acceptance.UseLocalDisk; import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; @@ -37,7 +37,6 @@ import java.nio.file.Files; import java.nio.file.StandardOpenOption; import java.util.Map; -import org.apache.commons.compress.utils.Sets; import org.eclipse.jgit.lib.Config; import org.junit.Test;
diff --git a/owners/web/eslint.config.js b/owners/web/eslint.config.js new file mode 100644 index 0000000..69ddb56 --- /dev/null +++ b/owners/web/eslint.config.js
@@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +const {defineConfig} = require('eslint/config'); + +// eslint-disable-next-line no-undef +__plugindir = 'owners/web'; + +const gerritEslint = require('../../eslint.config.js'); + +module.exports = defineConfig([ + { + extends: [gerritEslint], + }, +]);