Merge branch 'stable-3.5' into stable-3.6 * stable-3.5: Make sure that `owners` are JDK8 compatible in `stable-3.4` Extend 'GetFilesOwners' with Owners submit requirements Simplify `GetFilesOwners` Turn 'codeReviewLabelValue' to 'Stream' from 'Optional' Filter out the files owners based on the owners' code-review value Fix 'Redundant specification of type arguments <PathOwners>' warn Clean up GetFileOwners from unused variables Split owners integration tests into a separate build targets Follow whole chain of parents for OWNERS in submit requirements Avoid using Repository.resolve() when accessing exact ref-names Fix `OWNERS` file over-caching problem Follow the whole chain of parent projects for OWNERS Make code_review_user/1 a native plugin predicate Reuse the code_review_user Prolog rule across gerrit_owners.pl Change-Id: Ie499186cc5f7744e8f604798703a0788072ea34a
diff --git a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/JgitWrapper.java b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/JgitWrapper.java index 0f0db10..17b8af9 100644 --- a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/JgitWrapper.java +++ b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/JgitWrapper.java
@@ -19,9 +19,11 @@ import static org.eclipse.jgit.lib.FileMode.TYPE_FILE; import static org.eclipse.jgit.lib.FileMode.TYPE_MASK; +import com.google.gerrit.entities.RefNames; import java.io.IOException; import java.util.Optional; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; @@ -34,13 +36,14 @@ public static Optional<byte[]> getBlobAsBytes(Repository repository, String revision, String path) throws IOException { - ObjectId objectId = repository.resolve(revision); - if (objectId == null) { + Ref ref = repository.getRefDatabase().exactRef(RefNames.fullName(revision)); + if (ref == null) { return Optional.empty(); } try (final TreeWalk w = - TreeWalk.forPath(repository, path, parseCommit(repository, objectId).getTree())) { + TreeWalk.forPath( + repository, path, parseCommit(repository, ref.getLeaf().getObjectId()).getTree())) { return Optional.ofNullable(w) .filter(walk -> (walk.getRawMode(0) & TYPE_MASK) == TYPE_FILE)
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 ab3a7f1..b904715 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
@@ -35,6 +35,7 @@ import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.patch.DiffSummary; import com.google.gerrit.server.patch.filediff.FileDiffOutput; +import com.google.gerrit.server.project.ProjectState; import java.io.IOException; import java.util.Collection; import java.util.Collections; @@ -204,6 +205,12 @@ return label; } + public static List<Project.NameKey> getParents(ProjectState projectState) { + return projectState.parents().stream() + .map(ProjectState::getNameKey) + .collect(Collectors.toList()); + } + /** * Fetched the owners for the associated patch list. * @@ -213,11 +220,11 @@ OwnersMap ownersMap = new OwnersMap(); try { // Using a `map` would have needed a try/catch inside the lamba, resulting in more code - List<PathOwnersEntry> parentsPathOwnersEntries = + List<ReadOnlyPathOwnersEntry> parentsPathOwnersEntries = getPathOwnersEntries(parentProjectsNames, RefNames.REFS_CONFIG, cache); - PathOwnersEntry projectEntry = - getPathOwnersEntry(project, repository, RefNames.REFS_CONFIG, cache); - PathOwnersEntry rootEntry = getPathOwnersEntry(project, repository, branch, cache); + ReadOnlyPathOwnersEntry projectEntry = + getPathOwnersEntryOrEmpty(project, repository, RefNames.REFS_CONFIG, cache); + PathOwnersEntry rootEntry = getPathOwnersEntryOrNew(project, repository, branch, cache); Map<String, PathOwnersEntry> entries = new HashMap<>(); PathOwnersEntry currentEntry = null; @@ -267,41 +274,51 @@ } } - private List<PathOwnersEntry> getPathOwnersEntries( + private List<ReadOnlyPathOwnersEntry> getPathOwnersEntries( List<Project.NameKey> projectNames, String branch, PathOwnersEntriesCache cache) throws IOException, ExecutionException { - ImmutableList.Builder<PathOwnersEntry> pathOwnersEntries = ImmutableList.builder(); + ImmutableList.Builder<ReadOnlyPathOwnersEntry> pathOwnersEntries = ImmutableList.builder(); for (Project.NameKey projectName : projectNames) { try (Repository repo = repositoryManager.openRepository(projectName)) { pathOwnersEntries = - pathOwnersEntries.add(getPathOwnersEntry(projectName.get(), repo, branch, cache)); + pathOwnersEntries.add( + getPathOwnersEntryOrEmpty(projectName.get(), repo, branch, cache)); } } return pathOwnersEntries.build(); } - private PathOwnersEntry getPathOwnersEntry( + private ReadOnlyPathOwnersEntry getPathOwnersEntryOrEmpty( + String project, Repository repo, String branch, PathOwnersEntriesCache cache) + throws ExecutionException { + return getPathOwnersEntry(project, repo, branch, cache) + .map(v -> (ReadOnlyPathOwnersEntry) v) + .orElse(PathOwnersEntry.EMPTY); + } + + private PathOwnersEntry getPathOwnersEntryOrNew( + String project, Repository repo, String branch, PathOwnersEntriesCache cache) + throws ExecutionException { + return getPathOwnersEntry(project, repo, branch, cache).orElseGet(PathOwnersEntry::new); + } + + private Optional<PathOwnersEntry> getPathOwnersEntry( String project, Repository repo, String branch, PathOwnersEntriesCache cache) throws ExecutionException { String rootPath = "OWNERS"; - return cache.get( - project, - branch, - rootPath, - () -> - getOwnersConfig(repo, rootPath, branch) - .map( - conf -> - new PathOwnersEntry( - rootPath, - conf, - accounts, - Optional.empty(), - Collections.emptySet(), - Collections.emptySet(), - Collections.emptySet(), - Collections.emptySet())) - .orElse(PathOwnersEntry.EMPTY)); + return cache + .get(project, branch, rootPath, () -> getOwnersConfig(repo, rootPath, branch)) + .map( + conf -> + new PathOwnersEntry( + rootPath, + conf, + accounts, + Optional.empty(), + Collections.emptySet(), + Collections.emptySet(), + Collections.emptySet(), + Collections.emptySet())); } private void processMatcherPerPath( @@ -352,8 +369,8 @@ String project, String path, String branch, - PathOwnersEntry projectEntry, - List<PathOwnersEntry> parentsPathOwnersEntries, + ReadOnlyPathOwnersEntry projectEntry, + List<ReadOnlyPathOwnersEntry> parentsPathOwnersEntries, PathOwnersEntry rootEntry, Map<String, PathOwnersEntry> entries, PathOwnersEntriesCache cache) @@ -366,7 +383,7 @@ calculateCurrentEntry(rootEntry, projectEntry, currentEntry); // Inherit from Parent Project if OWNER in Project enables inheritance - for (PathOwnersEntry parentPathOwnersEntry : parentsPathOwnersEntries) { + for (ReadOnlyPathOwnersEntry parentPathOwnersEntry : parentsPathOwnersEntries) { calculateCurrentEntry(projectEntry, parentPathOwnersEntry, currentEntry); } @@ -384,31 +401,31 @@ String ownersPath = partial + "OWNERS"; PathOwnersEntry pathFallbackEntry = currentEntry; currentEntry = - cache.get( - project, - branch, - ownersPath, - () -> - getOwnersConfig(repository, ownersPath, branch) - .map( - c -> { - Optional<LabelDefinition> label = pathFallbackEntry.getLabel(); - final Set<Id> owners = pathFallbackEntry.getOwners(); - final Set<Id> reviewers = pathFallbackEntry.getReviewers(); - Collection<Matcher> inheritedMatchers = - pathFallbackEntry.getMatchers().values(); - Set<String> groupOwners = pathFallbackEntry.getGroupOwners(); - return new PathOwnersEntry( - ownersPath, - c, - accounts, - label, - owners, - reviewers, - inheritedMatchers, - groupOwners); - }) - .orElse(pathFallbackEntry)); + cache + .get( + project, + branch, + ownersPath, + () -> getOwnersConfig(repository, ownersPath, branch)) + .map( + c -> { + Optional<LabelDefinition> label = pathFallbackEntry.getLabel(); + final Set<Id> owners = pathFallbackEntry.getOwners(); + final Set<Id> reviewers = pathFallbackEntry.getReviewers(); + Collection<Matcher> inheritedMatchers = + pathFallbackEntry.getMatchers().values(); + Set<String> groupOwners = pathFallbackEntry.getGroupOwners(); + return new PathOwnersEntry( + ownersPath, + c, + accounts, + label, + owners, + reviewers, + inheritedMatchers, + groupOwners); + }) + .orElse(pathFallbackEntry); entries.put(partial, currentEntry); } } @@ -416,7 +433,9 @@ } private void calculateCurrentEntry( - PathOwnersEntry rootEntry, PathOwnersEntry projectEntry, PathOwnersEntry currentEntry) { + ReadOnlyPathOwnersEntry rootEntry, + ReadOnlyPathOwnersEntry projectEntry, + PathOwnersEntry currentEntry) { if (rootEntry.isInherited()) { for (Matcher matcher : projectEntry.getMatchers().values()) { if (!currentEntry.hasMatcher(matcher.getPath())) {
diff --git a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PathOwnersEntriesCache.java b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PathOwnersEntriesCache.java index 69584ab..901dbc8 100644 --- a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PathOwnersEntriesCache.java +++ b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PathOwnersEntriesCache.java
@@ -25,7 +25,9 @@ import com.google.inject.Inject; import com.google.inject.Module; import com.google.inject.Singleton; +import com.google.inject.TypeLiteral; import java.util.Objects; +import java.util.Optional; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; @@ -36,7 +38,7 @@ return new CacheModule() { @Override protected void configure() { - cache(CACHE_NAME, Key.class, PathOwnersEntry.class); + cache(CACHE_NAME, Key.class, new TypeLiteral<Optional<OwnersConfig>>() {}); bind(PathOwnersEntriesCache.class).to(PathOwnersEntriesCacheImpl.class); DynamicSet.bind(binder(), GitBatchRefUpdateListener.class) .to(OwnersRefUpdateListener.class); @@ -45,7 +47,8 @@ }; } - PathOwnersEntry get(String project, String branch, String path, Callable<PathOwnersEntry> loader) + Optional<OwnersConfig> get( + String project, String branch, String path, Callable<Optional<OwnersConfig>> loader) throws ExecutionException; void invalidate(String project, String branch);
diff --git a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PathOwnersEntriesCacheImpl.java b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PathOwnersEntriesCacheImpl.java index 74f9585..3af7a58 100644 --- a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PathOwnersEntriesCacheImpl.java +++ b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PathOwnersEntriesCacheImpl.java
@@ -25,18 +25,19 @@ import com.google.inject.name.Named; import java.time.Duration; import java.util.Collection; +import java.util.Optional; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; @Singleton class PathOwnersEntriesCacheImpl implements PathOwnersEntriesCache { - private final Cache<Key, PathOwnersEntry> cache; + private final Cache<Key, Optional<OwnersConfig>> cache; private final Multimap<String, Key> keysIndex; private final LoadingCache<String, Object> keyLocks; @Inject - PathOwnersEntriesCacheImpl(@Named(CACHE_NAME) Cache<Key, PathOwnersEntry> cache) { + PathOwnersEntriesCacheImpl(@Named(CACHE_NAME) Cache<Key, Optional<OwnersConfig>> cache) { this.cache = cache; this.keysIndex = HashMultimap.create(); this.keyLocks = @@ -46,14 +47,14 @@ } @Override - public PathOwnersEntry get( - String project, String branch, String path, Callable<PathOwnersEntry> loader) + public Optional<OwnersConfig> get( + String project, String branch, String path, Callable<Optional<OwnersConfig>> loader) throws ExecutionException { Key key = new Key(project, branch, path); return cache.get( key, () -> { - PathOwnersEntry entry = loader.call(); + Optional<OwnersConfig> entry = loader.call(); String indexKey = indexKey(project, branch); synchronized (keyLocks.getUnchecked(indexKey)) { keysIndex.put(indexKey, key);
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 1ede8a6..8e4c53c 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
@@ -31,20 +31,11 @@ * * <p>Used internally by PathOwners to represent and compute the owners at a specific path. */ -class PathOwnersEntry { - static final PathOwnersEntry EMPTY = new PathOwnersEntry(); - - private final boolean inherited; - private Optional<LabelDefinition> label; - private Set<Account.Id> owners = Sets.newHashSet(); - private Set<Account.Id> reviewers = Sets.newHashSet(); - private String ownersPath; - private Map<String, Matcher> matchers = Maps.newHashMap(); - private Set<String> groupOwners = Sets.newHashSet(); +class PathOwnersEntry extends ReadOnlyPathOwnersEntry { + static final ReadOnlyPathOwnersEntry EMPTY = new ReadOnlyPathOwnersEntry(true) {}; public PathOwnersEntry() { - inherited = true; - label = Optional.empty(); + super(true); } public PathOwnersEntry( @@ -56,6 +47,7 @@ Set<Account.Id> inheritedReviewers, Collection<Matcher> inheritedMatchers, Set<String> inheritedGroupOwners) { + super(config.isInherited()); this.ownersPath = path; this.owners = config.getOwners().stream() @@ -82,20 +74,6 @@ } else { this.label = config.getLabel(); } - this.inherited = config.isInherited(); - } - - @Override - public String toString() { - return "PathOwnersEntry [ownersPath=" - + ownersPath - + ", owners=" - + owners - + ", matchers=" - + matchers - + ", label=" - + label - + "]"; } public void addMatcher(Matcher matcher) { @@ -103,6 +81,52 @@ this.matchers.put(matcher.getPath(), matcher.merge(currMatchers)); } + public void setOwners(Set<Account.Id> owners) { + this.owners = owners; + } + + public void setReviewers(Set<Account.Id> reviewers) { + this.reviewers = reviewers; + } + + public void setOwnersPath(String ownersPath) { + this.ownersPath = ownersPath; + } + + public void setMatchers(Map<String, Matcher> matchers) { + this.matchers = matchers; + } + + public void setLabel(Optional<LabelDefinition> label) { + this.label = label; + } + + public void addMatchers(Collection<Matcher> values) { + for (Matcher matcher : values) { + addMatcher(matcher); + } + } + + @Override + protected String className() { + return getClass().getSimpleName(); + } +} + +abstract class ReadOnlyPathOwnersEntry { + protected final boolean inherited; + protected Optional<LabelDefinition> label; + protected Set<Account.Id> owners = Sets.newHashSet(); + protected Set<Account.Id> reviewers = Sets.newHashSet(); + protected String ownersPath; + protected Map<String, Matcher> matchers = Maps.newHashMap(); + protected Set<String> groupOwners = Sets.newHashSet(); + + protected ReadOnlyPathOwnersEntry(boolean inherited) { + this.inherited = inherited; + label = Optional.empty(); + } + public Map<String, Matcher> getMatchers() { return matchers; } @@ -115,30 +139,14 @@ return groupOwners; } - public void setOwners(Set<Account.Id> owners) { - this.owners = owners; - } - public Set<Account.Id> getReviewers() { return reviewers; } - public void setReviewers(Set<Account.Id> reviewers) { - this.reviewers = reviewers; - } - public String getOwnersPath() { return ownersPath; } - public void setOwnersPath(String ownersPath) { - this.ownersPath = ownersPath; - } - - public void setMatchers(Map<String, Matcher> matchers) { - this.matchers = matchers; - } - public boolean isInherited() { return inherited; } @@ -147,16 +155,6 @@ return label; } - public void setLabel(Optional<LabelDefinition> label) { - this.label = label; - } - - public void addMatchers(Collection<Matcher> values) { - for (Matcher matcher : values) { - addMatcher(matcher); - } - } - public boolean hasMatcher(String path) { return this.matchers.containsKey(path); } @@ -164,4 +162,22 @@ public static String stripOwnerDomain(String owner) { return Splitter.on('@').split(owner).iterator().next(); } + + @Override + public String toString() { + return className() + + " [ownersPath=" + + ownersPath + + ", owners=" + + owners + + ", matchers=" + + matchers + + ", label=" + + label + + "]"; + } + + protected String className() { + return "ReadOnlyPathOwnersEntry"; + } }
diff --git a/owners-common/src/test/java/com/googlesource/gerrit/owners/common/PathOwnersEntriesCacheMock.java b/owners-common/src/test/java/com/googlesource/gerrit/owners/common/PathOwnersEntriesCacheMock.java index 1eac62c..752bf7f 100644 --- a/owners-common/src/test/java/com/googlesource/gerrit/owners/common/PathOwnersEntriesCacheMock.java +++ b/owners-common/src/test/java/com/googlesource/gerrit/owners/common/PathOwnersEntriesCacheMock.java
@@ -14,6 +14,7 @@ package com.googlesource.gerrit.owners.common; +import java.util.Optional; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import org.junit.Ignore; @@ -30,8 +31,8 @@ public void invalidateIndexKey(Key key) {} @Override - public PathOwnersEntry get( - String project, String branch, String path, Callable<PathOwnersEntry> loader) + public Optional<OwnersConfig> get( + String project, String branch, String path, Callable<Optional<OwnersConfig>> loader) throws ExecutionException { try { hit++;
diff --git a/owners/BUILD b/owners/BUILD index 402bd34..f08ec84 100644 --- a/owners/BUILD +++ b/owners/BUILD
@@ -12,8 +12,8 @@ name = "gerrit-owners-predicates", srcs = PROLOG_PREDICATES, deps = [ + "//owners-common", "@prolog-runtime//jar:neverlink", - "//owners-common:owners-common", ] + PLUGIN_DEPS_NEVERLINK, ) @@ -50,15 +50,37 @@ java_library( name = "owners__plugin_test_deps", testonly = 1, + srcs = glob( + ["src/test/java/**/*.java"], + exclude = [ + "src/test/java/**/*Test.java", + "src/test/java/**/*IT.java", + ], + ), visibility = ["//visibility:public"], exports = PLUGIN_DEPS + PLUGIN_TEST_DEPS + [ ":owners", ], + deps = PLUGIN_DEPS + PLUGIN_TEST_DEPS + [ + ":owners", + ], ) junit_tests( name = "owners_tests", - srcs = glob(["src/test/java/**/*.java"]), + srcs = glob(["src/test/java/**/*Test.java"]), tags = ["owners"], - deps = ["owners__plugin_test_deps"], + deps = [ + ":owners__plugin_test_deps", + ], ) + +[junit_tests( + name = f[:f.index(".")].replace("/", "_"), + srcs = [f], + tags = ["owners"], + visibility = ["//visibility:public"], + deps = [ + ":owners__plugin_test_deps", + ], +) for f in glob(["src/test/java/**/*IT.java"])]
diff --git a/owners/src/main/java/com/googlesource/gerrit/owners/OwnersStoredValues.java b/owners/src/main/java/com/googlesource/gerrit/owners/OwnersStoredValues.java index 02ed2f8..c4d27e4 100644 --- a/owners/src/main/java/com/googlesource/gerrit/owners/OwnersStoredValues.java +++ b/owners/src/main/java/com/googlesource/gerrit/owners/OwnersStoredValues.java
@@ -28,8 +28,6 @@ import com.googlesource.gerrit.owners.common.PathOwners; import com.googlesource.gerrit.owners.common.PathOwnersEntriesCache; import com.googlesource.gerrit.owners.common.PluginSettings; -import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; @@ -53,7 +51,7 @@ } log.info("Initializing OwnerStoredValues"); PATH_OWNERS = - new StoredValue<PathOwners>() { + new StoredValue<>() { @Override protected PathOwners createValue(Prolog engine) { Map<String, FileDiffOutput> patchList = StoredValues.DIFF_LIST.get(engine); @@ -63,17 +61,13 @@ metrics.countConfigLoads.increment(); try (Timer0.Context ctx = metrics.loadConfig.start()) { - List<Project.NameKey> maybeParentProjectNameKey = - Optional.ofNullable(projectState.getProject().getParent()) - .map(Arrays::asList) - .orElse(Collections.emptyList()); - + List<Project.NameKey> parentProjectsNameKeys = PathOwners.getParents(projectState); String branch = StoredValues.getChange(engine).getDest().branch(); return new PathOwners( accounts, gitRepositoryManager, repository, - maybeParentProjectNameKey, + parentProjectsNameKeys, settings.isBranchDisabled(branch) ? Optional.empty() : Optional.of(branch), patchList, settings.expandGroups(),
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 f26ae71..3935a4b 100644 --- a/owners/src/main/java/com/googlesource/gerrit/owners/OwnersSubmitRequirement.java +++ b/owners/src/main/java/com/googlesource/gerrit/owners/OwnersSubmitRequirement.java
@@ -52,8 +52,6 @@ import com.googlesource.gerrit.owners.common.PathOwnersEntriesCache; import com.googlesource.gerrit.owners.common.PluginSettings; import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; @@ -201,11 +199,7 @@ try (Timer0.Context ctx = metrics.loadConfig.start()) { String branch = cd.change().getDest().branch(); - List<Project.NameKey> parents = - Optional.<Project.NameKey>ofNullable(projectState.getProject().getParent()) - .map(Arrays::asList) - .orElse(Collections.emptyList()); - + List<Project.NameKey> parents = PathOwners.getParents(projectState); Project.NameKey nameKey = projectState.getNameKey(); try (Repository repo = repoManager.openRepository(nameKey)) { PathOwners pathOwners =
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 d5d403b..5f5b1fb 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
@@ -15,11 +15,11 @@ package com.googlesource.gerrit.owners.restapi; -import com.google.common.base.Predicates; import com.google.common.collect.Maps; +import com.google.common.flogger.FluentLogger; import com.google.gerrit.entities.Account; import com.google.gerrit.entities.Change; -import com.google.gerrit.entities.PatchSet; +import com.google.gerrit.entities.LabelId; import com.google.gerrit.entities.Project; import com.google.gerrit.extensions.api.GerritApi; import com.google.gerrit.extensions.client.ListChangesOption; @@ -27,13 +27,13 @@ import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.Response; import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.extensions.restapi.RestReadView; import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.change.RevisionResource; import com.google.gerrit.server.git.GitRepositoryManager; -import com.google.gerrit.server.patch.PatchListCache; import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.query.change.ChangeData; import com.google.inject.Inject; @@ -45,65 +45,62 @@ import com.googlesource.gerrit.owners.entities.FilesOwnersResponse; import com.googlesource.gerrit.owners.entities.GroupOwner; import com.googlesource.gerrit.owners.entities.Owner; -import java.util.*; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.eclipse.jgit.lib.Repository; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; @Singleton public class GetFilesOwners implements RestReadView<RevisionResource> { - private static final Logger log = LoggerFactory.getLogger(GetFilesOwners.class); - - private final PatchListCache patchListCache; private final Accounts accounts; private final AccountCache accountCache; private final ProjectCache projectCache; private final GitRepositoryManager repositoryManager; private final PluginSettings pluginSettings; private final GerritApi gerritApi; - private final ChangeData.Factory changeDataFactory; private final PathOwnersEntriesCache cache; + static final String MISSING_CODE_REVIEW_LABEL = + "Cannot calculate file owners state when review label is not configured"; + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + @Inject GetFilesOwners( - PatchListCache patchListCache, Accounts accounts, AccountCache accountCache, ProjectCache projectCache, GitRepositoryManager repositoryManager, PluginSettings pluginSettings, GerritApi gerritApi, - ChangeData.Factory changeDataFactory, PathOwnersEntriesCache cache) { - this.patchListCache = patchListCache; this.accounts = accounts; this.accountCache = accountCache; this.projectCache = projectCache; this.repositoryManager = repositoryManager; this.pluginSettings = pluginSettings; this.gerritApi = gerritApi; - this.changeDataFactory = changeDataFactory; this.cache = cache; } @Override public Response<FilesOwnersResponse> apply(RevisionResource revision) throws AuthException, BadRequestException, ResourceConflictException, Exception { - PatchSet ps = revision.getPatchSet(); Change change = revision.getChange(); - int id = revision.getChangeResource().getChange().getChangeId(); + ChangeData changeData = revision.getChangeResource().getChangeData(); Project.NameKey project = change.getProject(); - List<Project.NameKey> maybeParentProjectNameKey = - projectCache - .get(project) - .map(p -> Arrays.asList(p.getProject().getParent())) - .filter(Predicates.notNull()) - .orElse(Collections.emptyList()); + List<Project.NameKey> projectParents = + projectCache.get(project).map(PathOwners::getParents).orElse(Collections.emptyList()); try (Repository repository = repositoryManager.openRepository(project)) { - Set<String> changePaths = new HashSet<>(changeDataFactory.create(change).currentFilePaths()); + Set<String> changePaths = new HashSet<>(changeData.currentFilePaths()); String branch = change.getDest().branch(); PathOwners owners = @@ -111,31 +108,101 @@ accounts, repositoryManager, repository, - maybeParentProjectNameKey, + projectParents, pluginSettings.isBranchDisabled(branch) ? Optional.empty() : Optional.of(branch), changePaths, pluginSettings.expandGroups(), project.get(), cache); + Map<String, Set<GroupOwner>> fileExpandedOwners = + Maps.transformValues( + owners.getFileOwners(), + ids -> + ids.stream() + .map(this::getOwnerFromAccountId) + .flatMap(Optional::stream) + .collect(Collectors.toSet())); + Map<String, Set<GroupOwner>> fileToOwners = pluginSettings.expandGroups() - ? Maps.transformValues( - owners.getFileOwners(), - ids -> - ids.stream() - .map(this::getOwnerFromAccountId) - .flatMap(Optional::stream) - .collect(Collectors.toSet())) + ? fileExpandedOwners : Maps.transformValues( owners.getFileGroupOwners(), groupNames -> groupNames.stream().map(GroupOwner::new).collect(Collectors.toSet())); - return Response.ok(new FilesOwnersResponse(getLabels(id), fileToOwners)); + Map<Integer, Map<String, Integer>> ownersLabels = getLabels(change.getChangeId()); + + LabelAndScore label = getLabelDefinition(owners, changeData); + + Map<String, Set<GroupOwner>> filesWithPendingOwners = + Maps.filterEntries( + fileToOwners, + (fileOwnerEntry) -> + !isApprovedByOwner( + fileExpandedOwners.get(fileOwnerEntry.getKey()), ownersLabels, label)); + + return Response.ok(new FilesOwnersResponse(ownersLabels, filesWithPendingOwners)); } } + private LabelAndScore getLabelDefinition(PathOwners owners, ChangeData changeData) + throws ResourceNotFoundException { + + try { + return Optional.of(pluginSettings.enableSubmitRequirement()) + .filter(Boolean::booleanValue) + .flatMap(enabled -> getLabelFromOwners(owners, changeData)) + .orElseGet( + () -> + new LabelAndScore( + LabelId.CODE_REVIEW, getMaxScoreForLabel(changeData, LabelId.CODE_REVIEW))); + } catch (LabelNotFoundException e) { + logger.atInfo().withCause(e).log("Invalid configuration"); + throw new ResourceNotFoundException(MISSING_CODE_REVIEW_LABEL, e); + } + } + + private Optional<LabelAndScore> getLabelFromOwners(PathOwners owners, ChangeData changeData) + throws LabelNotFoundException { + return owners + .getLabel() + .map( + label -> + new LabelAndScore( + label.getName(), + label + .getScore() + .orElseGet(() -> getMaxScoreForLabel(changeData, label.getName())))); + } + + private short getMaxScoreForLabel(ChangeData changeData, String labelId) + throws LabelNotFoundException { + return changeData + .getLabelTypes() + .byLabel(labelId) + .map(label -> label.getMaxPositive()) + .orElseThrow(() -> new LabelNotFoundException(changeData.change().getProject(), labelId)); + } + + private boolean isApprovedByOwner( + Set<GroupOwner> fileOwners, + Map<Integer, Map<String, Integer>> ownersLabels, + LabelAndScore label) { + return fileOwners.stream() + .filter(owner -> owner instanceof Owner) + .map(owner -> ((Owner) owner).getId()) + .flatMap(ownerId -> codeReviewLabelValue(ownersLabels, ownerId, label.getLabelId())) + .anyMatch(value -> value >= label.getScore()); + } + + private Stream<Integer> codeReviewLabelValue( + Map<Integer, Map<String, Integer>> ownersLabels, int ownerId, String labelId) { + return Stream.ofNullable(ownersLabels.get(ownerId)) + .flatMap(m -> Stream.ofNullable(m.get(labelId))); + } + /** * This method returns ta Map representing the "owners_labels" object of the response. When * serialized the Map, has to to return the following JSON: the following JSON: @@ -184,4 +251,30 @@ .get(accountId) .map(as -> new Owner(as.account().fullName(), as.account().id().get())); } + + static class LabelNotFoundException extends RuntimeException { + private static final long serialVersionUID = 1L; + + LabelNotFoundException(Project.NameKey project, String labelId) { + super(String.format("Project %s has no %s label defined", project, labelId)); + } + } + + private static class LabelAndScore { + private final String labelId; + private final short score; + + private LabelAndScore(String labelId, short score) { + this.labelId = labelId; + this.score = score; + } + + private String getLabelId() { + return labelId; + } + + private short getScore() { + return score; + } + } }
diff --git a/owners/src/main/java/gerrit_owners/PRED_code_review_user_1.java b/owners/src/main/java/gerrit_owners/PRED_code_review_user_1.java new file mode 100644 index 0000000..a2dd06b --- /dev/null +++ b/owners/src/main/java/gerrit_owners/PRED_code_review_user_1.java
@@ -0,0 +1,117 @@ +// 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 gerrit_owners; + +import com.google.gerrit.entities.Account; +import com.google.gerrit.entities.LabelId; +import com.google.gerrit.entities.LabelType; +import com.google.gerrit.entities.LabelValue; +import com.google.gerrit.server.query.change.ChangeData; +import com.google.gerrit.server.rules.StoredValues; +import com.googlecode.prolog_cafe.exceptions.PrologException; +import com.googlecode.prolog_cafe.lang.IntegerTerm; +import com.googlecode.prolog_cafe.lang.JavaObjectTerm; +import com.googlecode.prolog_cafe.lang.Operation; +import com.googlecode.prolog_cafe.lang.Predicate; +import com.googlecode.prolog_cafe.lang.Prolog; +import com.googlecode.prolog_cafe.lang.Term; +import java.util.Iterator; +import java.util.Optional; +import java.util.stream.Collectors; + +/** 'code_review_user'(-User) */ +public class PRED_code_review_user_1 extends Predicate.P1 { + + private static final PRED_code_review_user_check CODE_REVIEW_USER_CHECK = + new PRED_code_review_user_check(); + private static final PRED_code_review_user_empty CODE_REVIEW_USER_EMPTY = + new PRED_code_review_user_empty(); + private static final PRED_code_review_user_next CODE_REVIEW_USER_NEXT = + new PRED_code_review_user_next(); + + public PRED_code_review_user_1(Term a1, Operation n) { + this.arg1 = a1; + this.cont = n; + } + + @Override + public Operation exec(Prolog engine) throws PrologException { + engine.cont = cont; + engine.setB0(); + + ChangeData cd = StoredValues.CHANGE_DATA.get(engine); + Optional<LabelValue> codeReviewMaxValue = + cd.getLabelTypes().byLabel(LabelId.CODE_REVIEW).map(LabelType::getMax); + + Iterator<Account.Id> approvalsAccounts = + cd.currentApprovals().stream() + .filter(a -> LabelId.CODE_REVIEW.equalsIgnoreCase(a.labelId().get())) + .filter(a -> codeReviewMaxValue.filter(val -> val.getValue() == a.value()).isPresent()) + .map(a -> a.accountId()) + .collect(Collectors.toList()) + .iterator(); + + engine.r1 = arg1; + engine.r2 = new JavaObjectTerm(approvalsAccounts); + + return engine.jtry2(CODE_REVIEW_USER_CHECK, CODE_REVIEW_USER_NEXT); + } + + private static class PRED_code_review_user_check extends Operation { + + @Override + public Operation exec(Prolog engine) throws PrologException { + Term a1 = engine.r1; + Term a2 = engine.r2; + + @SuppressWarnings("unchecked") + Iterator<Account.Id> iter = (Iterator<Account.Id>) ((JavaObjectTerm) a2).object(); + while (iter.hasNext()) { + Account.Id accountId = iter.next(); + IntegerTerm accountIdTerm = new IntegerTerm(accountId.get()); + if (!a1.unify(accountIdTerm, engine.trail)) { + continue; + } + return engine.cont; + } + return engine.fail(); + } + } + + private static class PRED_code_review_user_next extends Operation { + + @Override + public Operation exec(Prolog engine) throws PrologException { + return engine.trust(CODE_REVIEW_USER_EMPTY); + } + } + + private static class PRED_code_review_user_empty extends Operation { + + @Override + public Operation exec(Prolog engine) throws PrologException { + Term a2 = engine.r2; + + @SuppressWarnings("unchecked") + Iterator<Account.Id> iter = (Iterator<Account.Id>) ((JavaObjectTerm) a2).object(); + if (!iter.hasNext()) { + return engine.fail(); + } + + return engine.jtry2(CODE_REVIEW_USER_CHECK, CODE_REVIEW_USER_NEXT); + } + } +}
diff --git a/owners/src/main/prolog/gerrit_owners.pl b/owners/src/main/prolog/gerrit_owners.pl index 1fe2a22..5fba7e7 100644 --- a/owners/src/main/prolog/gerrit_owners.pl +++ b/owners/src/main/prolog/gerrit_owners.pl
@@ -38,8 +38,8 @@ add_owner_approval(_, In, Out) :- In = Out. owner_approved(Path) :- - owner(Path, User), - gerrit:commit_label(label('Code-Review', 2), User), + owner(Path, user(US)), + code_review_user(US), !. owner_approved(Users, Path) :- @@ -56,9 +56,6 @@ findall(US,code_review_user(US),Approvers), matcher_needed(Approvers,F,FileAndUser). -code_review_user(U) :- - gerrit:commit_label(label('Code-Review', 2), user(U)). - % this loops over all the paths and if for any % we have some labels generated then add a single % Owner-Code-Review need to block submit button
diff --git a/owners/src/main/resources/Documentation/rest-api.md b/owners/src/main/resources/Documentation/rest-api.md index 4fb4285..68fb463 100644 --- a/owners/src/main/resources/Documentation/rest-api.md +++ b/owners/src/main/resources/Documentation/rest-api.md
@@ -1,7 +1,7 @@ # Rest API -The @PLUGIN@ exposes a Rest API endpoint to list the owners associated to each file and, for each owner, -its associated labels and votes: +The @PLUGIN@ exposes a Rest API endpoint to list the owners associated to each file that +needs a review, and, for each owner, its current labels and votes: ```bash GET /changes/{change-id}/revisions/{revision-id}/owners~files-owners @@ -29,4 +29,7 @@ } } -``` \ No newline at end of file +``` + +> __NOTE__: The API does not work in the case when custom label is in +> rules.pl configuration as described in [the config.md docs](https://gerrit.googlesource.com/plugins/owners/+/refs/heads/stable-3.4/owners/src/main/resources/Documentation/config.md#example-3-owners-file-without-matchers-and-custom-owner_approves-label) \ No newline at end of file
diff --git a/owners/src/test/java/com/googlesource/gerrit/owners/OwnersApprovalHasOperandIT.java b/owners/src/test/java/com/googlesource/gerrit/owners/OwnersApprovalHasOperandIT.java index 9a65c45..55bfa3c 100644 --- a/owners/src/test/java/com/googlesource/gerrit/owners/OwnersApprovalHasOperandIT.java +++ b/owners/src/test/java/com/googlesource/gerrit/owners/OwnersApprovalHasOperandIT.java
@@ -21,6 +21,8 @@ import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.TestAccount; +import com.google.gerrit.acceptance.TestPlugin; +import com.google.gerrit.acceptance.UseLocalDisk; import com.google.gerrit.acceptance.config.GlobalPluginConfig; import com.google.gerrit.entities.SubmitRequirement; import com.google.gerrit.entities.SubmitRequirementExpression; @@ -35,7 +37,9 @@ import org.junit.Before; import org.junit.Test; -public class OwnersApprovalHasOperandIT extends OwnersSubmitRequirementIT { +@TestPlugin(name = "owners", sysModule = "com.googlesource.gerrit.owners.OwnersModule") +@UseLocalDisk +public class OwnersApprovalHasOperandIT extends OwnersSubmitRequirementITAbstract { private static final String REQUIREMENT_NAME = "Owner-Approval"; @Before
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 002dc4b..0d5c2c5 100644 --- a/owners/src/test/java/com/googlesource/gerrit/owners/OwnersSubmitRequirementIT.java +++ b/owners/src/test/java/com/googlesource/gerrit/owners/OwnersSubmitRequirementIT.java
@@ -15,444 +15,9 @@ package com.googlesource.gerrit.owners; -import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; -import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel; -import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; -import static com.google.gerrit.server.project.testing.TestLabels.labelBuilder; -import static com.google.gerrit.server.project.testing.TestLabels.value; -import static java.util.stream.Collectors.joining; - -import com.google.gerrit.acceptance.LightweightPluginDaemonTest; -import com.google.gerrit.acceptance.PushOneCommit; -import com.google.gerrit.acceptance.TestAccount; import com.google.gerrit.acceptance.TestPlugin; import com.google.gerrit.acceptance.UseLocalDisk; -import com.google.gerrit.acceptance.config.GlobalPluginConfig; -import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; -import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; -import com.google.gerrit.entities.LabelFunction; -import com.google.gerrit.entities.LabelId; -import com.google.gerrit.entities.LabelType; -import com.google.gerrit.entities.RefNames; -import com.google.gerrit.extensions.api.changes.ChangeApi; -import com.google.gerrit.extensions.api.changes.ReviewInput; -import com.google.gerrit.extensions.common.ChangeInfo; -import com.google.gerrit.extensions.common.LegacySubmitRequirementInfo; -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; -import java.util.stream.Stream; -import org.junit.Test; @TestPlugin(name = "owners", sysModule = "com.googlesource.gerrit.owners.OwnersModule") @UseLocalDisk -public class OwnersSubmitRequirementIT extends LightweightPluginDaemonTest { - private static final LegacySubmitRequirementInfo NOT_READY = - new LegacySubmitRequirementInfo("NOT_READY", "Owners", "owners"); - private static final LegacySubmitRequirementInfo READY = - new LegacySubmitRequirementInfo("OK", "Owners", "owners"); - - @Inject protected RequestScopeOperations requestScopeOperations; - @Inject private ProjectOperations projectOperations; - - @Test - @GlobalPluginConfig( - pluginName = "owners", - name = "owners.enableSubmitRequirement", - value = "true") - public void shouldRequireAtLeastOneApprovalForMatchingPathFromOwner() throws Exception { - TestAccount admin2 = accountCreator.admin2(); - TestAccount user1 = accountCreator.user1(); - addOwnerFileWithMatchersToRoot(true, ".md", admin2, user1); - - PushOneCommit.Result r = createChange("Add a file", "README.md", "foo"); - 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.approve()); - ChangeInfo changeReady = forChange(r).get(); - assertThat(changeReady.submittable).isTrue(); - assertThat(changeReady.requirements).containsExactly(READY); - } - - @Test - @GlobalPluginConfig( - pluginName = "owners", - name = "owners.enableSubmitRequirement", - value = "true") - public void shouldNotRequireApprovalForNotMatchingPath() throws Exception { - TestAccount admin2 = accountCreator.admin2(); - addOwnerFileWithMatchersToRoot(true, ".md", admin2); - - PushOneCommit.Result r = createChange("Add a file", "README.txt", "foo"); - ChangeApi changeApi = forChange(r); - ChangeInfo changeNotReady = changeApi.get(); - assertThat(changeNotReady.submittable).isFalse(); - assertThat(changeNotReady.requirements).isEmpty(); - - changeApi.current().review(ReviewInput.approve()); - ChangeInfo changeReady = changeApi.get(); - assertThat(changeReady.submittable).isTrue(); - assertThat(changeReady.requirements).isEmpty(); - } - - @Test - @GlobalPluginConfig( - pluginName = "owners", - name = "owners.enableSubmitRequirement", - value = "true") - public void shouldRequireApprovalFromRootOwner() throws Exception { - TestAccount admin2 = accountCreator.admin2(); - addOwnerFileToRoot(true, 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.approve()); - ChangeInfo changeReady = forChange(r).get(); - assertThat(changeReady.submittable).isTrue(); - assertThat(changeReady.requirements).containsExactly(READY); - } - - @Test - @GlobalPluginConfig( - pluginName = "owners", - name = "owners.enableSubmitRequirement", - value = "true") - public void shouldBlockOwnersApprovalForMaxNegativeVote() throws Exception { - TestAccount admin2 = accountCreator.admin2(); - addOwnerFileToRoot(true, 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.approve()); - ChangeInfo changeReady = forChange(r).get(); - assertThat(changeReady.submittable).isTrue(); - assertThat(changeReady.requirements).containsExactly(READY); - - changeApi.current().review(ReviewInput.reject()); - assertThat(forChange(r).get().submittable).isFalse(); - } - - @Test - @GlobalPluginConfig( - pluginName = "owners", - name = "owners.enableSubmitRequirement", - value = "true") - public void shouldRequireVerifiedApprovalEvenIfCodeOwnerApproved() throws Exception { - TestAccount admin2 = accountCreator.admin2(); - addOwnerFileToRoot(true, admin2); - - installVerifiedLabel(); - - PushOneCommit.Result r = createChange("Add a file", "foo", "bar"); - ChangeApi changeApi = forChange(r); - assertThat(changeApi.get().submittable).isFalse(); - assertThat(changeApi.get().requirements).containsExactly(NOT_READY); - - requestScopeOperations.setApiUser(admin2.id()); - forChange(r).current().review(ReviewInput.approve()); - assertThat(forChange(r).get().submittable).isFalse(); - assertThat(forChange(r).get().requirements).containsExactly(READY); - verifyHasSubmitRecord( - forChange(r).get().submitRecords, LabelId.VERIFIED, SubmitRecordInfo.Label.Status.NEED); - - changeApi.current().review(new ReviewInput().label(LabelId.VERIFIED, 1)); - assertThat(changeApi.get().submittable).isTrue(); - verifyHasSubmitRecord( - changeApi.get().submitRecords, LabelId.VERIFIED, SubmitRecordInfo.Label.Status.OK); - } - - @Test - @GlobalPluginConfig( - pluginName = "owners", - name = "owners.enableSubmitRequirement", - value = "true") - public void shouldRequireCodeOwnerApprovalEvenIfVerifiedWasApproved() throws Exception { - TestAccount admin2 = accountCreator.admin2(); - addOwnerFileToRoot(true, admin2); - - installVerifiedLabel(); - - PushOneCommit.Result r = createChange("Add a file", "foo", "bar"); - ChangeApi changeApi = forChange(r); - assertThat(changeApi.get().submittable).isFalse(); - assertThat(changeApi.get().requirements).containsExactly(NOT_READY); - - requestScopeOperations.setApiUser(admin2.id()); - forChange(r).current().review(new ReviewInput().label(LabelId.VERIFIED, 1)); - ChangeInfo changeNotReady = forChange(r).get(); - assertThat(changeNotReady.submittable).isFalse(); - assertThat(changeNotReady.requirements).containsExactly(NOT_READY); - verifyHasSubmitRecord( - changeNotReady.submitRecords, LabelId.VERIFIED, SubmitRecordInfo.Label.Status.OK); - - forChange(r).current().review(ReviewInput.approve()); - ChangeInfo changeReady = forChange(r).get(); - assertThat(changeReady.submittable).isTrue(); - assertThat(changeReady.requirements).containsExactly(READY); - } - - @Test - @GlobalPluginConfig( - pluginName = "owners", - name = "owners.enableSubmitRequirement", - value = "true") - public void shouldRequireConfiguredLabelByCodeOwner() throws Exception { - TestAccount admin2 = accountCreator.admin2(); - String labelId = "Foo"; - addOwnerFileToRoot(true, LabelDefinition.parse(labelId).get(), admin2); - - installLabel(labelId); - - PushOneCommit.Result r = createChange("Add a file", "foo", "bar"); - ChangeApi changeApi = forChange(r); - assertThat(changeApi.get().submittable).isFalse(); - assertThat(changeApi.get().requirements).containsExactly(NOT_READY); - - changeApi.current().review(ReviewInput.approve()); - ChangeInfo changeStillNotReady = changeApi.get(); - assertThat(changeStillNotReady.submittable).isFalse(); - assertThat(changeStillNotReady.requirements).containsExactly(NOT_READY); - - requestScopeOperations.setApiUser(admin2.id()); - forChange(r).current().review(new ReviewInput().label(labelId, 1)); - ChangeInfo changeReady = forChange(r).get(); - assertThat(changeReady.submittable).isTrue(); - assertThat(changeReady.requirements).containsExactly(READY); - verifyHasSubmitRecord(changeReady.submitRecords, labelId, SubmitRecordInfo.Label.Status.OK); - } - - @Test - @GlobalPluginConfig( - pluginName = "owners", - name = "owners.enableSubmitRequirement", - value = "true") - public void shouldRequireConfiguredLabelByCodeOwnerEvenIfItIsNotConfiguredForProject() - throws Exception { - TestAccount admin2 = accountCreator.admin2(); - String notExistinglabelId = "Foo"; - addOwnerFileToRoot(true, LabelDefinition.parse(notExistinglabelId).get(), admin2); - - PushOneCommit.Result r = createChange("Add a file", "foo", "bar"); - ChangeApi changeApi = forChange(r); - assertThat(changeApi.get().submittable).isFalse(); - assertThat(changeApi.get().requirements).containsExactly(NOT_READY); - - changeApi.current().review(ReviewInput.approve()); - ChangeInfo changeStillNotReady = changeApi.get(); - assertThat(changeStillNotReady.submittable).isFalse(); - 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( - records.stream() - .flatMap(record -> record.labels.stream()) - .filter(l -> l.label.equals(label) && l.status == status) - .findAny()) - .isPresent(); - } - - private void installVerifiedLabel() throws Exception { - installLabel(LabelId.VERIFIED); - } - - 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(); - - installLabel(verified); - - String heads = RefNames.REFS_HEADS + "*"; - projectOperations - .project(project) - .forUpdate() - .add(allowLabel(verified.getName()).ref(heads).group(REGISTERED_USERS).range(-1, 1)) - .update(); - } - - private void installLabel(LabelType label) throws Exception { - try (ProjectConfigUpdate u = updateProject(project)) { - u.getConfig().upsertLabelType(label); - u.save(); - } - } - - protected ChangeApi forChange(PushOneCommit.Result r) throws RestApiException { - return gApi.changes().id(r.getChangeId()); - } - - private void addOwnerFileWithMatchersToRoot( - boolean inherit, String extension, TestAccount... users) throws Exception { - // Add OWNERS file to root: - // - // inherited: true - // matchers: - // - suffix: extension - // owners: - // - u1.email() - // - ... - // - uN.email() - pushOwnersToMaster( - String.format( - "inherited: %s\nmatchers:\n" + "- suffix: %s\n owners:\n%s", - inherit, - extension, - Stream.of(users) - .map(user -> String.format(" - %s\n", user.email())) - .collect(joining()))); - } - - private void addOwnerFileToRoot(boolean inherit, TestAccount u) throws Exception { - // Add OWNERS file to root: - // - // inherited: true - // owners: - // - u.email() - pushOwnersToMaster(String.format("inherited: %s\nowners:\n- %s\n", inherit, u.email())); - } - - protected void addOwnerFileToRoot(boolean inherit, LabelDefinition label, TestAccount u) - throws Exception { - // Add OWNERS file to root: - // - // inherited: true - // label: label,score # score is optional - // owners: - // - u.email() - pushOwnersToMaster( - 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())); - } - - private void pushOwnersToMaster(String owners) throws Exception { - pushFactory - .create(admin.newIdent(), testRepo, "Add OWNER file", "OWNERS", owners) - .to(RefNames.fullName("master")) - .assertOkStatus(); - } -} +public class OwnersSubmitRequirementIT extends OwnersSubmitRequirementITAbstract {}
diff --git a/owners/src/test/java/com/googlesource/gerrit/owners/OwnersSubmitRequirementITAbstract.java b/owners/src/test/java/com/googlesource/gerrit/owners/OwnersSubmitRequirementITAbstract.java new file mode 100644 index 0000000..d7cea60 --- /dev/null +++ b/owners/src/test/java/com/googlesource/gerrit/owners/OwnersSubmitRequirementITAbstract.java
@@ -0,0 +1,515 @@ +// 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; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; +import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel; +import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; +import static com.google.gerrit.server.project.testing.TestLabels.labelBuilder; +import static com.google.gerrit.server.project.testing.TestLabels.value; +import static java.util.stream.Collectors.joining; + +import com.google.gerrit.acceptance.GitUtil; +import com.google.gerrit.acceptance.LightweightPluginDaemonTest; +import com.google.gerrit.acceptance.PushOneCommit; +import com.google.gerrit.acceptance.TestAccount; +import com.google.gerrit.acceptance.config.GlobalPluginConfig; +import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; +import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; +import com.google.gerrit.entities.LabelFunction; +import com.google.gerrit.entities.LabelId; +import com.google.gerrit.entities.LabelType; +import com.google.gerrit.entities.Project; +import com.google.gerrit.entities.RefNames; +import com.google.gerrit.extensions.api.changes.ChangeApi; +import com.google.gerrit.extensions.api.changes.ReviewInput; +import com.google.gerrit.extensions.client.SubmitType; +import com.google.gerrit.extensions.common.ChangeInfo; +import com.google.gerrit.extensions.common.LegacySubmitRequirementInfo; +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; +import java.util.stream.Stream; +import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; +import org.eclipse.jgit.junit.TestRepository; +import org.junit.Test; + +abstract class OwnersSubmitRequirementITAbstract extends LightweightPluginDaemonTest { + private static final LegacySubmitRequirementInfo NOT_READY = + new LegacySubmitRequirementInfo("NOT_READY", "Owners", "owners"); + private static final LegacySubmitRequirementInfo READY = + new LegacySubmitRequirementInfo("OK", "Owners", "owners"); + + @Inject protected RequestScopeOperations requestScopeOperations; + @Inject private ProjectOperations projectOperations; + + @Test + @GlobalPluginConfig( + pluginName = "owners", + name = "owners.enableSubmitRequirement", + value = "true") + public void shouldRequireAtLeastOneApprovalForMatchingPathFromOwner() throws Exception { + TestAccount admin2 = accountCreator.admin2(); + TestAccount user1 = accountCreator.user1(); + addOwnerFileWithMatchersToRoot(true, ".md", admin2, user1); + + PushOneCommit.Result r = createChange("Add a file", "README.md", "foo"); + 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.approve()); + ChangeInfo changeReady = forChange(r).get(); + assertThat(changeReady.submittable).isTrue(); + assertThat(changeReady.requirements).containsExactly(READY); + } + + @Test + @GlobalPluginConfig( + pluginName = "owners", + name = "owners.enableSubmitRequirement", + value = "true") + public void shouldNotRequireApprovalForNotMatchingPath() throws Exception { + TestAccount admin2 = accountCreator.admin2(); + addOwnerFileWithMatchersToRoot(true, ".md", admin2); + + PushOneCommit.Result r = createChange("Add a file", "README.txt", "foo"); + ChangeApi changeApi = forChange(r); + ChangeInfo changeNotReady = changeApi.get(); + assertThat(changeNotReady.submittable).isFalse(); + assertThat(changeNotReady.requirements).isEmpty(); + + changeApi.current().review(ReviewInput.approve()); + ChangeInfo changeReady = changeApi.get(); + assertThat(changeReady.submittable).isTrue(); + assertThat(changeReady.requirements).isEmpty(); + } + + @Test + @GlobalPluginConfig( + pluginName = "owners", + name = "owners.enableSubmitRequirement", + value = "true") + public void shouldRequireApprovalFromRootOwner() throws Exception { + TestAccount admin2 = accountCreator.admin2(); + addOwnerFileToRoot(true, 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.approve()); + ChangeInfo changeReady = forChange(r).get(); + assertThat(changeReady.submittable).isTrue(); + assertThat(changeReady.requirements).containsExactly(READY); + } + + @Test + @GlobalPluginConfig( + pluginName = "owners", + name = "owners.enableSubmitRequirement", + value = "true") + public void shouldBlockOwnersApprovalForMaxNegativeVote() throws Exception { + TestAccount admin2 = accountCreator.admin2(); + addOwnerFileToRoot(true, 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.approve()); + ChangeInfo changeReady = forChange(r).get(); + assertThat(changeReady.submittable).isTrue(); + assertThat(changeReady.requirements).containsExactly(READY); + + changeApi.current().review(ReviewInput.reject()); + assertThat(forChange(r).get().submittable).isFalse(); + } + + @Test + @GlobalPluginConfig( + pluginName = "owners", + name = "owners.enableSubmitRequirement", + value = "true") + public void shouldRequireVerifiedApprovalEvenIfCodeOwnerApproved() throws Exception { + TestAccount admin2 = accountCreator.admin2(); + addOwnerFileToRoot(true, admin2); + + installVerifiedLabel(); + + PushOneCommit.Result r = createChange("Add a file", "foo", "bar"); + ChangeApi changeApi = forChange(r); + assertThat(changeApi.get().submittable).isFalse(); + assertThat(changeApi.get().requirements).containsExactly(NOT_READY); + + requestScopeOperations.setApiUser(admin2.id()); + forChange(r).current().review(ReviewInput.approve()); + assertThat(forChange(r).get().submittable).isFalse(); + assertThat(forChange(r).get().requirements).containsExactly(READY); + verifyHasSubmitRecord( + forChange(r).get().submitRecords, LabelId.VERIFIED, SubmitRecordInfo.Label.Status.NEED); + + changeApi.current().review(new ReviewInput().label(LabelId.VERIFIED, 1)); + assertThat(changeApi.get().submittable).isTrue(); + verifyHasSubmitRecord( + changeApi.get().submitRecords, LabelId.VERIFIED, SubmitRecordInfo.Label.Status.OK); + } + + @Test + @GlobalPluginConfig( + pluginName = "owners", + name = "owners.enableSubmitRequirement", + value = "true") + public void shouldRequireCodeOwnerApprovalEvenIfVerifiedWasApproved() throws Exception { + TestAccount admin2 = accountCreator.admin2(); + addOwnerFileToRoot(true, admin2); + + installVerifiedLabel(); + + PushOneCommit.Result r = createChange("Add a file", "foo", "bar"); + ChangeApi changeApi = forChange(r); + assertThat(changeApi.get().submittable).isFalse(); + assertThat(changeApi.get().requirements).containsExactly(NOT_READY); + + requestScopeOperations.setApiUser(admin2.id()); + forChange(r).current().review(new ReviewInput().label(LabelId.VERIFIED, 1)); + ChangeInfo changeNotReady = forChange(r).get(); + assertThat(changeNotReady.submittable).isFalse(); + assertThat(changeNotReady.requirements).containsExactly(NOT_READY); + verifyHasSubmitRecord( + changeNotReady.submitRecords, LabelId.VERIFIED, SubmitRecordInfo.Label.Status.OK); + + forChange(r).current().review(ReviewInput.approve()); + ChangeInfo changeReady = forChange(r).get(); + assertThat(changeReady.submittable).isTrue(); + assertThat(changeReady.requirements).containsExactly(READY); + } + + @Test + @GlobalPluginConfig( + pluginName = "owners", + name = "owners.enableSubmitRequirement", + value = "true") + public void shouldRequireConfiguredLabelByCodeOwner() throws Exception { + TestAccount admin2 = accountCreator.admin2(); + String labelId = "Foo"; + addOwnerFileToRoot(true, LabelDefinition.parse(labelId).get(), admin2); + + installLabel(labelId); + + PushOneCommit.Result r = createChange("Add a file", "foo", "bar"); + ChangeApi changeApi = forChange(r); + assertThat(changeApi.get().submittable).isFalse(); + assertThat(changeApi.get().requirements).containsExactly(NOT_READY); + + changeApi.current().review(ReviewInput.approve()); + ChangeInfo changeStillNotReady = changeApi.get(); + assertThat(changeStillNotReady.submittable).isFalse(); + assertThat(changeStillNotReady.requirements).containsExactly(NOT_READY); + + requestScopeOperations.setApiUser(admin2.id()); + forChange(r).current().review(new ReviewInput().label(labelId, 1)); + ChangeInfo changeReady = forChange(r).get(); + assertThat(changeReady.submittable).isTrue(); + assertThat(changeReady.requirements).containsExactly(READY); + verifyHasSubmitRecord(changeReady.submitRecords, labelId, SubmitRecordInfo.Label.Status.OK); + } + + @Test + @GlobalPluginConfig( + pluginName = "owners", + name = "owners.enableSubmitRequirement", + value = "true") + public void shouldRequireConfiguredLabelByCodeOwnerEvenIfItIsNotConfiguredForProject() + throws Exception { + TestAccount admin2 = accountCreator.admin2(); + String notExistinglabelId = "Foo"; + addOwnerFileToRoot(true, LabelDefinition.parse(notExistinglabelId).get(), admin2); + + PushOneCommit.Result r = createChange("Add a file", "foo", "bar"); + ChangeApi changeApi = forChange(r); + assertThat(changeApi.get().submittable).isFalse(); + assertThat(changeApi.get().requirements).containsExactly(NOT_READY); + + changeApi.current().review(ReviewInput.approve()); + ChangeInfo changeStillNotReady = changeApi.get(); + assertThat(changeStillNotReady.submittable).isFalse(); + 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); + } + + @Test + @GlobalPluginConfig( + pluginName = "owners", + name = "owners.enableSubmitRequirement", + value = "true") + public void shouldRequireApprovalFromGrandParentProjectOwner() throws Exception { + Project.NameKey parentProjectName = + createProjectOverAPI("parent", allProjects, true, SubmitType.FAST_FORWARD_ONLY); + Project.NameKey childProjectName = + createProjectOverAPI("child", parentProjectName, true, SubmitType.FAST_FORWARD_ONLY); + TestRepository<InMemoryRepository> childRepo = cloneProject(childProjectName); + + TestAccount admin2 = accountCreator.admin2(); + addOwnerFileToRefsMetaConfig(true, admin2, allProjects); + + PushOneCommit.Result r = + createCommitAndPush(childRepo, "refs/for/master", "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.approve()); + ChangeInfo changeReady = forChange(r).get(); + assertThat(changeReady.submittable).isTrue(); + assertThat(changeReady.requirements).containsExactly(READY); + } + + private void verifyHasSubmitRecord( + Collection<SubmitRecordInfo> records, String label, SubmitRecordInfo.Label.Status status) { + assertThat( + records.stream() + .flatMap(record -> record.labels.stream()) + .filter(l -> l.label.equals(label) && l.status == status) + .findAny()) + .isPresent(); + } + + private void installVerifiedLabel() throws Exception { + installLabel(LabelId.VERIFIED); + } + + 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(); + + installLabel(verified); + + String heads = RefNames.REFS_HEADS + "*"; + projectOperations + .project(project) + .forUpdate() + .add(allowLabel(verified.getName()).ref(heads).group(REGISTERED_USERS).range(-1, 1)) + .update(); + } + + private void installLabel(LabelType label) throws Exception { + try (ProjectConfigUpdate u = updateProject(project)) { + u.getConfig().upsertLabelType(label); + u.save(); + } + } + + protected ChangeApi forChange(PushOneCommit.Result r) throws RestApiException { + return gApi.changes().id(r.getChangeId()); + } + + private void addOwnerFileWithMatchersToRoot( + boolean inherit, String extension, TestAccount... users) throws Exception { + // Add OWNERS file to root: + // + // inherited: true + // matchers: + // - suffix: extension + // owners: + // - u1.email() + // - ... + // - uN.email() + pushOwnersToMaster( + String.format( + "inherited: %s\nmatchers:\n" + "- suffix: %s\n owners:\n%s", + inherit, + extension, + Stream.of(users) + .map(user -> String.format(" - %s\n", user.email())) + .collect(joining()))); + } + + private void addOwnerFileToRoot(boolean inherit, TestAccount u) throws Exception { + // Add OWNERS file to root: + // + // inherited: true + // owners: + // - u.email() + pushOwnersToMaster(String.format("inherited: %s\nowners:\n- %s\n", inherit, u.email())); + } + + private void addOwnerFileToRefsMetaConfig( + boolean inherit, TestAccount u, Project.NameKey projectName) throws Exception { + // Add OWNERS file to root: + // + // inherited: true + // owners: + // - u.email() + pushOwnersToRefsMetaConfig( + String.format("inherited: %s\nowners:\n- %s\n", inherit, u.email()), projectName); + } + + protected void addOwnerFileToRoot(boolean inherit, LabelDefinition label, TestAccount u) + throws Exception { + // Add OWNERS file to root: + // + // inherited: true + // label: label,score # score is optional + // owners: + // - u.email() + pushOwnersToMaster( + 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())); + } + + private void pushOwnersToMaster(String owners) throws Exception { + pushFactory + .create(admin.newIdent(), testRepo, "Add OWNER file", "OWNERS", owners) + .to(RefNames.fullName("master")) + .assertOkStatus(); + } + + private void pushOwnersToRefsMetaConfig(String owners, Project.NameKey projectName) + throws Exception { + TestRepository<InMemoryRepository> project = cloneProject(projectName); + GitUtil.fetch(project, RefNames.REFS_CONFIG + ":" + RefNames.REFS_CONFIG); + project.reset(RefNames.REFS_CONFIG); + pushFactory + .create(admin.newIdent(), project, "Add OWNER file", "OWNERS", owners) + .to(RefNames.REFS_CONFIG) + .assertOkStatus(); + } +}
diff --git a/owners/src/test/java/com/googlesource/gerrit/owners/restapi/GetFilesOwnersIT.java b/owners/src/test/java/com/googlesource/gerrit/owners/restapi/GetFilesOwnersIT.java index ec3e816..76c4962 100644 --- a/owners/src/test/java/com/googlesource/gerrit/owners/restapi/GetFilesOwnersIT.java +++ b/owners/src/test/java/com/googlesource/gerrit/owners/restapi/GetFilesOwnersIT.java
@@ -15,196 +15,9 @@ package com.googlesource.gerrit.owners.restapi; -import static com.google.common.truth.Truth.assertThat; - -import com.google.common.collect.ImmutableMap; -import com.google.gerrit.acceptance.GitUtil; -import com.google.gerrit.acceptance.LightweightPluginDaemonTest; import com.google.gerrit.acceptance.TestPlugin; import com.google.gerrit.acceptance.UseLocalDisk; -import com.google.gerrit.acceptance.config.GlobalPluginConfig; -import com.google.gerrit.entities.Project; -import com.google.gerrit.entities.RefNames; -import com.google.gerrit.extensions.restapi.Response; -import com.googlesource.gerrit.owners.entities.FilesOwnersResponse; -import com.googlesource.gerrit.owners.entities.GroupOwner; -import com.googlesource.gerrit.owners.entities.Owner; -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.junit.Test; @TestPlugin(name = "owners", sysModule = "com.googlesource.gerrit.owners.OwnersModule") @UseLocalDisk -public class GetFilesOwnersIT extends LightweightPluginDaemonTest { - - private GetFilesOwners ownersApi; - private Owner rootOwner; - private Owner projectOwner; - - @Override - public void setUpTestPlugin() throws Exception { - super.setUpTestPlugin(); - - rootOwner = new Owner(admin.fullName(), admin.id().get()); - projectOwner = new Owner(user.fullName(), user.id().get()); - ownersApi = plugin.getSysInjector().getInstance(GetFilesOwners.class); - } - - @Test - public void shouldReturnExactFileOwners() throws Exception { - addOwnerFileToRoot(true); - String changeId = createChange().getChangeId(); - - Response<FilesOwnersResponse> resp = - assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId))); - - assertThat(resp.value().files).containsExactly("a.txt", Sets.newHashSet(rootOwner)); - } - - @Test - public void shouldReturnOwnersLabels() throws Exception { - addOwnerFileToRoot(true); - String changeId = createChange().getChangeId(); - approve(changeId); - - Response<FilesOwnersResponse> resp = - assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId))); - - assertThat(resp.value().ownersLabels) - .containsExactly(admin.id().get(), ImmutableMap.builder().put("Code-Review", 2).build()); - } - - @Test - @GlobalPluginConfig(pluginName = "owners", name = "owners.expandGroups", value = "false") - public void shouldReturnResponseWithUnexpandedFileOwners() throws Exception { - addOwnerFileToRoot(true); - String changeId = createChange().getChangeId(); - - Response<FilesOwnersResponse> resp = - assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId))); - - assertThat(resp.value().files) - .containsExactly("a.txt", Sets.newHashSet(new GroupOwner(admin.username()))); - } - - @Test - @GlobalPluginConfig(pluginName = "owners", name = "owners.expandGroups", value = "false") - public void shouldReturnResponseWithUnexpandedFileMatchersOwners() throws Exception { - addOwnerFileWithMatchersToRoot(true); - String changeId = createChange().getChangeId(); - - Response<FilesOwnersResponse> resp = - assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId))); - - assertThat(resp.value().files) - .containsExactly("a.txt", Sets.newHashSet(new GroupOwner(admin.username()))); - } - - @Test - @UseLocalDisk - public void shouldReturnInheritedOwnersFromProjectsOwners() throws Exception { - assertInheritFromProject(project); - } - - @Test - @UseLocalDisk - public void shouldReturnInheritedOwnersFromParentProjectsOwners() throws Exception { - assertInheritFromProject(allProjects); - } - - @Test - @UseLocalDisk - public void shouldNotReturnInheritedOwnersFromProjectsOwners() throws Exception { - assertNotInheritFromProject(project); - } - - @Test - @UseLocalDisk - public void shouldNotReturnInheritedOwnersFromParentProjectsOwners() throws Exception { - addOwnerFileToProjectConfig(project, false); - assertNotInheritFromProject(allProjects); - } - - private static <T> Response<T> assertResponseOk(Response<T> response) { - assertThat(response.statusCode()).isEqualTo(HttpServletResponse.SC_OK); - return response; - } - - private void assertNotInheritFromProject(Project.NameKey projectNameKey) throws Exception { - addOwnerFileToRoot(false); - addOwnerFileToProjectConfig(projectNameKey, true); - - String changeId = createChange().getChangeId(); - Response<FilesOwnersResponse> resp = - assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId))); - - assertThat(resp.value().files).containsExactly("a.txt", Sets.newHashSet(rootOwner)); - } - - private void assertInheritFromProject(Project.NameKey projectNameKey) throws Exception { - addOwnerFileToRoot(true); - addOwnerFileToProjectConfig(projectNameKey, true); - - String changeId = createChange().getChangeId(); - Response<FilesOwnersResponse> resp = - assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId))); - - assertThat(resp.value().files) - .containsExactly("a.txt", Sets.newHashSet(rootOwner, projectOwner)); - } - - private void addOwnerFileToProjectConfig(Project.NameKey projectNameKey, boolean inherit) - throws Exception { - TestRepository<InMemoryRepository> project = cloneProject(projectNameKey); - GitUtil.fetch(project, RefNames.REFS_CONFIG + ":" + RefNames.REFS_CONFIG); - project.reset(RefNames.REFS_CONFIG); - pushFactory - .create( - admin.newIdent(), - project, - "Add OWNER file", - "OWNERS", - String.format( - "inherited: %s\nmatchers:\n" + "- suffix: .txt\n owners:\n - %s\n", - inherit, user.email())) - .to(RefNames.REFS_CONFIG); - } - - private void addOwnerFileToRoot(boolean inherit) throws Exception { - // Add OWNERS file to root: - // - // inherited: true - // owners: - // - admin - merge( - createChange( - testRepo, - "master", - "Add OWNER file", - "OWNERS", - String.format("inherited: %s\nowners:\n- %s\n", inherit, admin.email()), - "")); - } - - private void addOwnerFileWithMatchersToRoot(boolean inherit) throws Exception { - // Add OWNERS file to root: - // - // inherited: true - // matchers: - // - suffix: .txt - // owners: - // - admin@mail.com - merge( - createChange( - testRepo, - "master", - "Add OWNER file", - "OWNERS", - String.format( - "inherited: %s\nmatchers:\n" + "- suffix: .txt\n owners:\n - %s\n", - inherit, admin.email()), - "")); - } -} +public class GetFilesOwnersIT extends GetFilesOwnersITAbstract {}
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 new file mode 100644 index 0000000..936f3d3 --- /dev/null +++ b/owners/src/test/java/com/googlesource/gerrit/owners/restapi/GetFilesOwnersITAbstract.java
@@ -0,0 +1,361 @@ +// 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.restapi; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.gerrit.testing.GerritJUnit.assertThrows; + +import com.google.gerrit.acceptance.GitUtil; +import com.google.gerrit.acceptance.LightweightPluginDaemonTest; +import com.google.gerrit.acceptance.PushOneCommit.Result; +import com.google.gerrit.acceptance.TestAccount; +import com.google.gerrit.acceptance.UseLocalDisk; +import com.google.gerrit.acceptance.config.GlobalPluginConfig; +import com.google.gerrit.entities.LabelId; +import com.google.gerrit.entities.LabelType; +import com.google.gerrit.entities.Project; +import com.google.gerrit.entities.Project.NameKey; +import com.google.gerrit.entities.RefNames; +import com.google.gerrit.extensions.client.SubmitType; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.ResourceNotFoundException; +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.server.project.testing.TestLabels; +import com.googlesource.gerrit.owners.entities.FilesOwnersResponse; +import com.googlesource.gerrit.owners.entities.GroupOwner; +import com.googlesource.gerrit.owners.entities.Owner; +import com.googlesource.gerrit.owners.restapi.GetFilesOwners.LabelNotFoundException; +import java.util.Arrays; +import javax.servlet.http.HttpServletResponse; +import org.apache.commons.compress.utils.Sets; +import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription; +import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.transport.FetchResult; +import org.eclipse.jgit.util.FS; +import org.junit.Test; + +public abstract class GetFilesOwnersITAbstract extends LightweightPluginDaemonTest { + + private static final String REFS_META_CONFIG = RefNames.REFS_META + "config"; + protected GetFilesOwners ownersApi; + private Owner rootOwner; + private Owner projectOwner; + private NameKey parentProjectName; + private NameKey childProjectName; + private TestRepository<InMemoryRepository> childRepo; + private TestRepository<InMemoryRepository> parentRepo; + private TestRepository<InMemoryRepository> allProjectsRepo; + + @Override + public void setUpTestPlugin() throws Exception { + super.setUpTestPlugin(); + + rootOwner = new Owner(admin.fullName(), admin.id().get()); + projectOwner = new Owner(user.fullName(), user.id().get()); + ownersApi = plugin.getSysInjector().getInstance(GetFilesOwners.class); + + parentProjectName = + createProjectOverAPI("parent", allProjects, true, SubmitType.FAST_FORWARD_ONLY); + parentRepo = cloneProjectWithMetaRefs(parentProjectName); + + childProjectName = + createProjectOverAPI("child", parentProjectName, true, SubmitType.FAST_FORWARD_ONLY); + childRepo = cloneProject(childProjectName); + + allProjectsRepo = cloneProjectWithMetaRefs(allProjects); + } + + @Test + public void shouldReturnExactFileOwners() throws Exception { + addOwnerFileToRoot(true); + assertChangeHasOwners(createChange().getChangeId()); + } + + @Test + public void shouldReturnExactFileOwnersWhenOwnersIsSetToAllProjects() throws Exception { + addOwnerFileWithMatchers(allProjectsRepo, REFS_META_CONFIG, true); + assertChangeHasOwners(createChange(childRepo).getChangeId()); + } + + @Test + public void shouldReturnExactFileOwnersWhenOwnersIsSetToParentProject() throws Exception { + addOwnerFileWithMatchers(parentRepo, REFS_META_CONFIG, true); + assertChangeHasOwners(createChange(childRepo).getChangeId()); + } + + @Test + public void shouldReturnOwnersLabelsWhenNotApprovedByOwners() throws Exception { + addOwnerFileToRoot(true); + String changeId = createChange().getChangeId(); + + Response<FilesOwnersResponse> resp = + assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId))); + + assertThat(resp.value().files) + .containsExactly("a.txt", Sets.newHashSet(new Owner(admin.fullName(), admin.id().get()))); + + assertThat(resp.value().ownersLabels).isEmpty(); + } + + @Test + public void shouldReturnEmptyResponseWhenApprovedByOwners() throws Exception { + addOwnerFileToRoot(true); + String changeId = createChange().getChangeId(); + approve(changeId); + + Response<FilesOwnersResponse> resp = + assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId))); + + assertThat(resp.value().files).isEmpty(); + } + + @Test + @GlobalPluginConfig(pluginName = "owners", name = "owners.expandGroups", value = "false") + public void shouldReturnResponseWithUnexpandedFileOwners() throws Exception { + addOwnerFileToRoot(true); + String changeId = createChange().getChangeId(); + + Response<FilesOwnersResponse> resp = + assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId))); + + assertThat(resp.value().files) + .containsExactly("a.txt", Sets.newHashSet(new GroupOwner(admin.username()))); + } + + @Test + @GlobalPluginConfig(pluginName = "owners", name = "owners.expandGroups", value = "false") + public void shouldReturnEmptyResponseWhenApprovedByOwnersWithUnexpandedFileOwners() + throws Exception { + addOwnerFileToRoot(true); + String changeId = createChange().getChangeId(); + approve(changeId); + + Response<FilesOwnersResponse> resp = + assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId))); + + assertThat(resp.value().files).isEmpty(); + } + + @Test + @GlobalPluginConfig(pluginName = "owners", name = "owners.expandGroups", value = "false") + public void shouldReturnResponseWithUnexpandedFileMatchersOwners() throws Exception { + addOwnerFileWithMatchersToRoot(true); + String changeId = createChange().getChangeId(); + + Response<FilesOwnersResponse> resp = + assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId))); + + assertThat(resp.value().files) + .containsExactly("a.txt", Sets.newHashSet(new GroupOwner(admin.username()))); + } + + @Test + @UseLocalDisk + public void shouldReturnInheritedOwnersFromProjectsOwners() throws Exception { + assertInheritFromProject(project); + } + + @Test + @UseLocalDisk + public void shouldReturnInheritedOwnersFromParentProjectsOwners() throws Exception { + assertInheritFromProject(allProjects); + } + + @Test + @UseLocalDisk + public void shouldReflectChangesInParentProject() throws Exception { + addOwnerFileToProjectConfig(allProjects, true, admin); + + String changeId = createChange().getChangeId(); + Response<FilesOwnersResponse> resp = + assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId))); + assertThat(resp.value().files).containsExactly("a.txt", Sets.newHashSet(rootOwner)); + + addOwnerFileToProjectConfig(allProjects, true, user); + resp = assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId))); + assertThat(resp.value().files).containsExactly("a.txt", Sets.newHashSet(projectOwner)); + } + + @Test + @UseLocalDisk + public void shouldNotReturnInheritedOwnersFromProjectsOwners() throws Exception { + assertNotInheritFromProject(project); + } + + @Test + @UseLocalDisk + public void shouldNotReturnInheritedOwnersFromParentProjectsOwners() throws Exception { + addOwnerFileToProjectConfig(project, false); + assertNotInheritFromProject(allProjects); + } + + @Test + @UseLocalDisk + public void shouldThrowExceptionWhenCodeReviewLabelIsNotConfigured() throws Exception { + addOwnerFileToProjectConfig(project, false); + replaceCodeReviewWithLabel( + TestLabels.label( + "Foo", TestLabels.value(1, "Foo is fine"), TestLabels.value(-1, "Foo is not fine"))); + String changeId = createChange().getChangeId(); + + ResourceNotFoundException thrown = + assertThrows( + ResourceNotFoundException.class, + () -> ownersApi.apply(parseCurrentRevisionResource(changeId))); + assertThat(thrown).hasMessageThat().isEqualTo(GetFilesOwners.MISSING_CODE_REVIEW_LABEL); + assertThat(thrown).hasCauseThat().isInstanceOf(LabelNotFoundException.class); + } + + protected void replaceCodeReviewWithLabel(LabelType label) throws Exception { + try (ProjectConfigUpdate u = updateProject(allProjects)) { + u.getConfig().getLabelSections().remove(LabelId.CODE_REVIEW); + u.getConfig().upsertLabelType(label); + u.save(); + } + } + + protected static <T> Response<T> assertResponseOk(Response<T> response) { + assertThat(response.statusCode()).isEqualTo(HttpServletResponse.SC_OK); + return response; + } + + private void assertNotInheritFromProject(Project.NameKey projectNameKey) throws Exception { + addOwnerFileToRoot(false); + addOwnerFileToProjectConfig(projectNameKey, true); + + String changeId = createChange().getChangeId(); + assertChangeHasOwners(changeId); + } + + private void assertChangeHasOwners(String changeId) + throws AuthException, BadRequestException, ResourceConflictException, Exception { + Response<FilesOwnersResponse> resp = + assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId))); + + assertThat(resp.value().files).containsExactly("a.txt", Sets.newHashSet(rootOwner)); + } + + private void assertInheritFromProject(Project.NameKey projectNameKey) throws Exception { + addOwnerFileToRoot(true); + addOwnerFileToProjectConfig(projectNameKey, true); + + String changeId = createChange().getChangeId(); + Response<FilesOwnersResponse> resp = + assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId))); + + assertThat(resp.value().files) + .containsExactly("a.txt", Sets.newHashSet(rootOwner, projectOwner)); + } + + private void addOwnerFileToProjectConfig(Project.NameKey projectNameKey, boolean inherit) + throws Exception { + addOwnerFileToProjectConfig(projectNameKey, inherit, user); + } + + private void addOwnerFileToProjectConfig( + Project.NameKey projectNameKey, boolean inherit, TestAccount account) throws Exception { + TestRepository<InMemoryRepository> project = cloneProject(projectNameKey); + GitUtil.fetch(project, RefNames.REFS_CONFIG + ":" + RefNames.REFS_CONFIG); + project.reset(RefNames.REFS_CONFIG); + pushFactory + .create( + admin.newIdent(), + project, + "Add OWNER file", + "OWNERS", + String.format( + "inherited: %s\nmatchers:\n" + "- suffix: .txt\n owners:\n - %s\n", + inherit, account.email())) + .to(RefNames.REFS_CONFIG); + } + + private void addOwnerFileToRoot(boolean inherit) throws Exception { + // Add OWNERS file to root: + // + // inherited: true + // owners: + // - admin + merge( + createChange( + testRepo, + "master", + "Add OWNER file", + "OWNERS", + String.format("inherited: %s\nowners:\n- %s\n", inherit, admin.email()), + "")); + } + + private void addOwnerFileWithMatchersToRoot(boolean inherit) throws Exception { + addOwnerFileWithMatchers(testRepo, "master", inherit); + } + + private void addOwnerFileWithMatchers(TestRepository<?> repo, String targetRef, boolean inherit) + throws Exception { + // Add OWNERS file to root: + // + // inherited: true + // matchers: + // - suffix: .txt + // owners: + // - admin@mail.com + Result changeCreated = + createChange( + repo, + targetRef, + "Add OWNER file", + "OWNERS", + String.format( + "inherited: %s\nmatchers:\n" + "- suffix: .txt\n owners:\n - %s\n", + inherit, admin.email()), + ""); + changeCreated.assertOkStatus(); + merge(changeCreated); + } + + public TestRepository<InMemoryRepository> cloneProjectWithMetaRefs(Project.NameKey project) + throws Exception { + String uri = registerRepoConnection(project, admin); + String initialRef = "refs/remotes/origin/config"; + DfsRepositoryDescription desc = new DfsRepositoryDescription("clone of " + project.get()); + + InMemoryRepository.Builder b = new InMemoryRepository.Builder().setRepositoryDescription(desc); + if (uri.startsWith("ssh://")) { + // SshTransport depends on a real FS to read ~/.ssh/config, but InMemoryRepository by default + // uses a null FS. + // Avoid leaking user state into our tests. + b.setFS(FS.detect().setUserHome(null)); + } + InMemoryRepository dest = b.build(); + Config cfg = dest.getConfig(); + cfg.setString("remote", "origin", "url", uri); + cfg.setStringList( + "remote", + "origin", + "fetch", + Arrays.asList( + "+refs/heads/*:refs/remotes/origin/*", "+refs/meta/config:refs/remotes/origin/config")); + TestRepository<InMemoryRepository> testRepo = GitUtil.newTestRepository(dest); + FetchResult result = testRepo.git().fetch().setRemote("origin").call(); + if (result.getTrackingRefUpdate(initialRef) != null) { + testRepo.reset(initialRef); + } + return testRepo; + } +}
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 new file mode 100644 index 0000000..8172611 --- /dev/null +++ b/owners/src/test/java/com/googlesource/gerrit/owners/restapi/GetFilesOwnersSubmitRequirementsIT.java
@@ -0,0 +1,143 @@ +// 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.restapi; + +import static com.google.common.truth.Truth.assertThat; +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.gerrit.acceptance.TestPlugin; +import com.google.gerrit.acceptance.UseLocalDisk; +import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; +import com.google.gerrit.entities.LabelId; +import com.google.gerrit.entities.LabelType; +import com.google.gerrit.entities.RefNames; +import com.google.gerrit.extensions.api.changes.ReviewInput; +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.server.project.testing.TestLabels; +import com.google.inject.Inject; +import com.googlesource.gerrit.owners.common.LabelDefinition; +import com.googlesource.gerrit.owners.entities.FilesOwnersResponse; +import com.googlesource.gerrit.owners.entities.Owner; +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; + +@TestPlugin(name = "owners", sysModule = "com.googlesource.gerrit.owners.OwnersModule") +@UseLocalDisk +public class GetFilesOwnersSubmitRequirementsIT extends GetFilesOwnersITAbstract { + @Inject private ProjectOperations projectOperations; + + @Override + public void setUpTestPlugin() throws Exception { + Config pluginCfg = pluginConfig.getGlobalPluginConfig("owners"); + // enable submit requirements and store them to the file as there is no `ConfigSuite` mechanism + // for plugin config and there is no other way (but adding it to each test case) to enable it + // globally + pluginCfg.setBoolean("owners", null, "enableSubmitRequirement", true); + Files.writeString( + server.getSitePath().resolve("etc").resolve("owners.config"), + pluginCfg.toText(), + StandardOpenOption.CREATE, + StandardOpenOption.APPEND); + super.setUpTestPlugin(); + } + + @Test + public void shouldRequireConfiguredCodeReviewScore() throws Exception { + // configure submit requirement to require CR+1 only + addOwnerFileToRoot(LabelDefinition.parse("Code-Review,1").get(), admin); + + String changeId = createChange("Add a file", "foo", "bar").getChangeId(); + + Response<FilesOwnersResponse> resp = + assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId))); + assertThat(resp.value().files) + .containsExactly("foo", Sets.newHashSet(new Owner(admin.fullName(), admin.id().get()))); + assertThat(resp.value().ownersLabels).isEmpty(); + + // give CR+1 as requested + recommend(changeId); + + resp = assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId))); + assertThat(resp.value().files).isEmpty(); + assertThat(resp.value().ownersLabels) + .containsExactly(admin.id().get(), Map.of(LabelId.CODE_REVIEW, 1)); + } + + @Test + public void shouldRequireConfiguredLabelAndScore() throws Exception { + // configure submit requirement to require LabelFoo+1 + String label = "LabelFoo"; + addOwnerFileToRoot(LabelDefinition.parse(String.format("%s,1", label)).get(), admin); + replaceCodeReviewWithLabel(label); + + String changeId = createChange("Add a file", "foo", "bar").getChangeId(); + + Response<FilesOwnersResponse> resp = + assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId))); + assertThat(resp.value().files) + .containsExactly("foo", Sets.newHashSet(new Owner(admin.fullName(), admin.id().get()))); + assertThat(resp.value().ownersLabels).isEmpty(); + + // give LabelFoo+1 as requested + gApi.changes().id(changeId).current().review(new ReviewInput().label(label, 1)); + + resp = assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId))); + assertThat(resp.value().files).isEmpty(); + assertThat(resp.value().ownersLabels).containsEntry(admin.id().get(), Map.of(label, 1)); + } + + private void addOwnerFileToRoot(LabelDefinition label, TestAccount u) throws Exception { + // Add OWNERS file to root: + // + // inherited: true + // label: label,score # score is optional + // owners: + // - u.email() + String owners = + String.format( + "inherited: true\nlabel: %s\nowners:\n- %s\n", + String.format( + "%s%s", + label.getName(), + label.getScore().map(value -> String.format(",%d", value)).orElse("")), + u.email()); + pushFactory + .create(admin.newIdent(), testRepo, "Add OWNER file", "OWNERS", owners) + .to(RefNames.fullName("master")) + .assertOkStatus(); + } + + private void replaceCodeReviewWithLabel(String labelId) throws Exception { + LabelType label = + TestLabels.label(labelId, TestLabels.value(1, "OK"), TestLabels.value(-1, "Not OK")); + + replaceCodeReviewWithLabel(label); + + // grant label to RegisteredUsers so that it is vote-able + String heads = RefNames.REFS_HEADS + "*"; + projectOperations + .project(project) + .forUpdate() + .add(allowLabel(label.getName()).ref(heads).group(REGISTERED_USERS).range(-1, 1)) + .update(); + } +}