Merge branch 'stable-3.2' into stable-3.3
* stable-3.2:
Add new documentation for the owners-autoassign plugin
Allow async assignment of reviewers
Change-Id: Icd345499536d5a81f099bc454a63a9105c551a0d
diff --git a/README.md b/README.md
index 4c9519f..4893600 100644
--- a/README.md
+++ b/README.md
@@ -57,6 +57,7 @@
```
bazel-bin/owners/owners.jar
bazel-bin/owners-autoassign/owners-autoassign.jar
+ bazel-bin/owners-api/owners-api.jar
```
@@ -91,6 +92,7 @@
$ cd gerrit/plugins
$ ln -s ../../owners/owners .
$ ln -s ../../owners/owners-autoassign .
+ $ ln -s ../../owners/owners-api .
$ ln -sf ../../owners/external_plugin_deps.bzl .
$ cd ..
$ ln -s ../owners/owners-common .
diff --git a/WORKSPACE b/WORKSPACE
index 67f7e9e..273e2eb 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -3,7 +3,7 @@
load("//:bazlets.bzl", "load_bazlets")
load_bazlets(
- commit = "4a27255dff75eadc98b86391806be13e030e6ff3",
+ commit = "10e78cc706760ff24cbc67ba527f9a8e4134d66f",
#local_path = "/home/<user>/projects/bazlets",
)
diff --git a/owners-api/BUILD b/owners-api/BUILD
new file mode 100644
index 0000000..06416d2
--- /dev/null
+++ b/owners-api/BUILD
@@ -0,0 +1,36 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+load("//tools/bzl:plugin.bzl", "PLUGIN_DEPS", "PLUGIN_DEPS_NEVERLINK", "PLUGIN_TEST_DEPS", "gerrit_plugin")
+
+gerrit_plugin(
+ name = "owners-api",
+ srcs = glob([
+ "src/main/java/**/*.java",
+ ]),
+ dir_name = "owners",
+ manifest_entries = [
+ "Implementation-Title: Gerrit OWNERS api plugin",
+ "Implementation-URL: https://gerrit.googlesource.com/plugins/owners",
+ "Gerrit-PluginName: owners-api",
+ "Gerrit-Module: com.googlesource.gerrit.owners.api.OwnersApiModule",
+ ],
+ resources = glob(["src/main/**/*"]),
+ deps = [],
+)
+
+java_library(
+ name = "owners-api_deps",
+ srcs = glob([
+ "src/main/java/**/*.java",
+ ]),
+ visibility = ["//visibility:public"],
+ deps = PLUGIN_DEPS_NEVERLINK,
+)
+
+junit_tests(
+ name = "owners_api_tests",
+ testonly = 1,
+ srcs = glob(["src/test/java/**/*.java"]),
+ deps = PLUGIN_DEPS + PLUGIN_TEST_DEPS + [
+ ":owners-api_deps",
+ ],
+)
diff --git a/owners-api/src/main/java/com/googlesource/gerrit/owners/api/OwnersApiModule.java b/owners-api/src/main/java/com/googlesource/gerrit/owners/api/OwnersApiModule.java
new file mode 100644
index 0000000..54ee51d
--- /dev/null
+++ b/owners-api/src/main/java/com/googlesource/gerrit/owners/api/OwnersApiModule.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2021 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.api;
+
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.inject.AbstractModule;
+
+public class OwnersApiModule extends AbstractModule {
+
+ @Override
+ protected void configure() {
+ DynamicItem.itemOf(binder(), OwnersAttentionSet.class);
+ }
+}
diff --git a/owners-api/src/main/java/com/googlesource/gerrit/owners/api/OwnersAttentionSet.java b/owners-api/src/main/java/com/googlesource/gerrit/owners/api/OwnersAttentionSet.java
new file mode 100644
index 0000000..31cd2a4
--- /dev/null
+++ b/owners-api/src/main/java/com/googlesource/gerrit/owners/api/OwnersAttentionSet.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2021 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.api;
+
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import java.util.Collection;
+
+/** API to expose a mechanism to selectively add owners to the attention-set. */
+public interface OwnersAttentionSet {
+
+ /**
+ * Select the owners that should be added to the attention-set.
+ *
+ * @param changeInfo change under review
+ * @param owners set of owners associated with a change.
+ * @return subset of owners that need to be added to the attention-set.
+ */
+ Collection<Account.Id> addToAttentionSet(ChangeInfo changeInfo, Collection<Account.Id> owners);
+}
diff --git a/owners-api/src/test/java/com/googlesource/gerrit/owners/api/OwnersAttentionSetIT.java b/owners-api/src/test/java/com/googlesource/gerrit/owners/api/OwnersAttentionSetIT.java
new file mode 100644
index 0000000..8a3ec18
--- /dev/null
+++ b/owners-api/src/test/java/com/googlesource/gerrit/owners/api/OwnersAttentionSetIT.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2021 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.api;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.entities.Account.Id;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Scopes;
+import java.util.Collection;
+import org.junit.Test;
+
+@TestPlugin(
+ name = "owners-api",
+ sysModule = "com.googlesource.gerrit.owners.api.OwnersAttentionSetIT$TestModule")
+public class OwnersAttentionSetIT extends LightweightPluginDaemonTest {
+
+ @Inject private DynamicItem<OwnersAttentionSet> ownerAttentionSetItem;
+
+ @Override
+ public Module createModule() {
+ return new OwnersApiModule();
+ }
+
+ public static class TestModule extends AbstractModule {
+ @Override
+ protected void configure() {
+ DynamicItem.bind(binder(), OwnersAttentionSet.class)
+ .to(SelectFirstOwnerForAttentionSet.class)
+ .in(Scopes.SINGLETON);
+ }
+ }
+
+ public static class SelectFirstOwnerForAttentionSet implements OwnersAttentionSet {
+ @Override
+ public Collection<Id> addToAttentionSet(ChangeInfo changeInfo, Collection<Id> owners) {
+ return null;
+ }
+ }
+
+ @Test
+ public void shouldAllowOwnersAttentionSetOverride() {
+ OwnersAttentionSet attentionSetSelector = ownerAttentionSetItem.get();
+
+ assertThat(attentionSetSelector).isNotNull();
+ assertThat(attentionSetSelector.getClass()).isEqualTo(SelectFirstOwnerForAttentionSet.class);
+ }
+}
diff --git a/owners-autoassign/BUILD b/owners-autoassign/BUILD
index 463afcb..024be42 100644
--- a/owners-autoassign/BUILD
+++ b/owners-autoassign/BUILD
@@ -12,11 +12,11 @@
"Implementation-URL: https://gerrit.googlesource.com/plugins/owners",
"Gerrit-PluginName: owners-autoassign",
"Gerrit-Module: com.googlesource.gerrit.owners.common.AutoassignModule",
- "Gerrit-ApiVersion: 2.16",
],
resources = glob(["src/main/**/*"]),
deps = [
"//owners-common",
+ "//plugins/owners-api",
],
)
@@ -28,6 +28,7 @@
visibility = ["//visibility:public"],
deps = PLUGIN_DEPS_NEVERLINK + [
"//owners-common",
+ "//plugins/owners-api",
],
)
@@ -37,6 +38,7 @@
srcs = glob(["src/test/java/**/*.java"]),
deps = PLUGIN_DEPS + PLUGIN_TEST_DEPS + [
"//owners-common",
+ "//plugins/owners-api",
":owners-autoassign_deps",
],
)
diff --git a/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AsyncReviewerManager.java b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AsyncReviewerManager.java
index 64607e6..6ea35e4 100644
--- a/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AsyncReviewerManager.java
+++ b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AsyncReviewerManager.java
@@ -17,6 +17,8 @@
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Account.Id;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
import com.google.gerrit.extensions.api.changes.ChangeApi;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.git.WorkQueue;
@@ -41,13 +43,16 @@
private final OneOffRequestContext requestContext;
class AddReviewersTask implements Runnable {
+ private final NameKey projectName;
private final ChangeApi cApi;
private final Collection<Id> reviewers;
private final int changeNum;
private int retryNum;
- public AddReviewersTask(ChangeApi cApi, Collection<Account.Id> reviewers)
+ public AddReviewersTask(
+ Project.NameKey projectName, ChangeApi cApi, Collection<Account.Id> reviewers)
throws RestApiException {
+ this.projectName = projectName;
this.cApi = cApi;
this.changeNum = cApi.get()._number;
this.reviewers = reviewers;
@@ -63,7 +68,7 @@
@Override
public void run() {
try (ManualRequestContext ctx = requestContext.open()) {
- syncReviewerManager.addReviewers(cApi, reviewers);
+ syncReviewerManager.addReviewers(projectName, cApi, reviewers);
} catch (Exception e) {
retryNum++;
@@ -91,11 +96,14 @@
}
@Override
- public void addReviewers(ChangeApi cApi, Collection<Account.Id> reviewers)
+ public void addReviewers(
+ Project.NameKey projectName, ChangeApi cApi, Collection<Account.Id> reviewers)
throws ReviewerManagerException {
try {
executor.schedule(
- new AddReviewersTask(cApi, reviewers), config.asyncDelay(), TimeUnit.MILLISECONDS);
+ new AddReviewersTask(projectName, cApi, reviewers),
+ config.asyncDelay(),
+ TimeUnit.MILLISECONDS);
} catch (RestApiException e) {
throw new ReviewerManagerException(e);
}
diff --git a/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AutoAssignConfig.java b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AutoAssignConfig.java
index 32bdedc..634d524 100644
--- a/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AutoAssignConfig.java
+++ b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AutoAssignConfig.java
@@ -15,11 +15,19 @@
package com.googlesource.gerrit.owners.common;
+import static com.google.gerrit.extensions.client.InheritableBoolean.TRUE;
+import static com.googlesource.gerrit.owners.common.AutoassignConfigModule.PROJECT_CONFIG_AUTOASSIGN_FIELD;
+import static com.googlesource.gerrit.owners.common.AutoassignConfigModule.PROJECT_CONFIG_AUTOASSIGN_WIP_CHANGES;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.PluginConfig;
import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.eclipse.jgit.lib.Config;
@@ -44,8 +52,13 @@
private final long retryInterval;
private final long asyncDelay;
+ private final PluginConfigFactory cfgFactory;
+ private final String pluginName;
+
@Inject
AutoAssignConfig(PluginConfigFactory configFactory, @PluginName String pluginName) {
+ this.pluginName = pluginName;
+ this.cfgFactory = configFactory;
Config config = configFactory.getGlobalPluginConfig(pluginName);
asyncReviewers = config.getBoolean(REVIEWERS_SECTION, ASYNC, ASYNC_DEF);
asyncThreads = config.getInt(REVIEWERS_SECTION, THREADS, THREADS_DEF);
@@ -59,6 +72,23 @@
config, REVIEWERS_SECTION, null, RETRY_INTERVAL, asyncDelay, MILLISECONDS);
}
+ @VisibleForTesting
+ public AutoAssignConfig() {
+ this.pluginName = "owners-autoassign";
+ cfgFactory = null;
+ Config config = new Config();
+ asyncReviewers = config.getBoolean(REVIEWERS_SECTION, ASYNC, ASYNC_DEF);
+ asyncThreads = config.getInt(REVIEWERS_SECTION, THREADS, THREADS_DEF);
+ asyncDelay =
+ ConfigUtil.getTimeUnit(
+ config, REVIEWERS_SECTION, null, DELAY, DELAY_MSEC_DEF, MILLISECONDS);
+
+ retryCount = config.getInt(REVIEWERS_SECTION, RETRY_COUNT, RETRY_COUNT_DEF);
+ retryInterval =
+ ConfigUtil.getTimeUnit(
+ config, REVIEWERS_SECTION, null, RETRY_INTERVAL, asyncDelay, MILLISECONDS);
+ }
+
public boolean isAsyncReviewers() {
return asyncReviewers;
}
@@ -78,4 +108,17 @@
public long asyncDelay() {
return asyncDelay;
}
+
+ public boolean autoAssignWip(Project.NameKey projectKey) throws NoSuchProjectException {
+ return cfg(projectKey).getEnum(PROJECT_CONFIG_AUTOASSIGN_WIP_CHANGES, TRUE).equals(TRUE);
+ }
+
+ public ReviewerState autoassignedReviewerState(Project.NameKey projectKey)
+ throws NoSuchProjectException {
+ return cfg(projectKey).getEnum(PROJECT_CONFIG_AUTOASSIGN_FIELD, ReviewerState.REVIEWER);
+ }
+
+ private PluginConfig cfg(Project.NameKey projectKey) throws NoSuchProjectException {
+ return cfgFactory.getFromProjectConfigWithInheritance(projectKey, pluginName);
+ }
}
diff --git a/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AutoassignConfigModule.java b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AutoassignConfigModule.java
new file mode 100644
index 0000000..dfa812d
--- /dev/null
+++ b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AutoassignConfigModule.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+// implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.owners.common;
+
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.extensions.api.projects.ProjectConfigEntryType;
+import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.client.ReviewerState;
+import com.google.gerrit.server.config.ProjectConfigEntry;
+import com.google.inject.AbstractModule;
+import java.util.Arrays;
+
+public class AutoassignConfigModule extends AbstractModule {
+ public static final String PROJECT_CONFIG_AUTOASSIGN_WIP_CHANGES = "autoAssignWip";
+ public static final String PROJECT_CONFIG_AUTOASSIGN_FIELD = "autoAssignField";
+
+ @Override
+ protected void configure() {
+ bind(ProjectConfigEntry.class)
+ .annotatedWith(Exports.named(PROJECT_CONFIG_AUTOASSIGN_WIP_CHANGES))
+ .toInstance(
+ new ProjectConfigEntry(
+ "Auto-assign WIP changes", InheritableBoolean.TRUE, InheritableBoolean.class));
+ bind(ProjectConfigEntry.class)
+ .annotatedWith(Exports.named(PROJECT_CONFIG_AUTOASSIGN_FIELD))
+ .toInstance(
+ new ProjectConfigEntry(
+ "Auto-assign field",
+ ReviewerState.REVIEWER.name(),
+ ProjectConfigEntryType.LIST,
+ Arrays.asList(ReviewerState.CC.name(), ReviewerState.REVIEWER.name()),
+ true,
+ "Change field to use for the assigned accounts"));
+ }
+}
diff --git a/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AutoassignModule.java b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AutoassignModule.java
index 0278c88..ae3127b 100644
--- a/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AutoassignModule.java
+++ b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AutoassignModule.java
@@ -16,6 +16,7 @@
package com.googlesource.gerrit.owners.common;
+import com.google.common.annotations.VisibleForTesting;
import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.inject.AbstractModule;
@@ -29,10 +30,16 @@
this.config = config;
}
+ @VisibleForTesting
+ AutoassignModule() {
+ this.config = new AutoAssignConfig();
+ }
+
@Override
protected void configure() {
bind(ReviewerManager.class)
.to(config.isAsyncReviewers() ? AsyncReviewerManager.class : SyncReviewerManager.class);
DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(GitRefListener.class);
+ install(new AutoassignConfigModule());
}
}
diff --git a/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/GitRefListener.java b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/GitRefListener.java
index 6f53d04..cc8d0ed 100644
--- a/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/GitRefListener.java
+++ b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/GitRefListener.java
@@ -38,18 +38,25 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.ChangeNotesCommit;
+import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
import com.google.gerrit.server.patch.PatchList;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.patch.PatchListKey;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.util.ManualRequestContext;
import com.google.gerrit.server.util.OneOffRequestContext;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.io.IOException;
+import java.util.List;
import java.util.Set;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.FooterKey;
import org.eclipse.jgit.revwalk.RevCommit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -58,6 +65,8 @@
public class GitRefListener implements GitReferenceUpdatedListener {
private static final Logger logger = LoggerFactory.getLogger(GitRefListener.class);
+ private static final FooterKey FOOTER_WORK_IN_PROGRESS = new FooterKey("Work-in-progress");
+
private final GerritApi api;
private final PatchListCache patchListCache;
@@ -71,6 +80,8 @@
private ChangeNotes.Factory notesFactory;
+ private final AutoAssignConfig cfg;
+
@Inject
public GitRefListener(
GerritApi api,
@@ -80,7 +91,8 @@
ReviewerManager reviewerManager,
OneOffRequestContext oneOffReqCtx,
Provider<CurrentUser> currentUserProvider,
- ChangeNotes.Factory notesFactory) {
+ ChangeNotes.Factory notesFactory,
+ AutoAssignConfig cfg) {
this.api = api;
this.patchListCache = patchListCache;
this.repositoryManager = repositoryManager;
@@ -89,6 +101,7 @@
this.oneOffReqCtx = oneOffReqCtx;
this.currentUserProvider = currentUserProvider;
this.notesFactory = notesFactory;
+ this.cfg = cfg;
}
@Override
@@ -98,46 +111,51 @@
return;
}
- AccountInfo updaterAccountInfo = event.getUpdater();
- CurrentUser currentUser = currentUserProvider.get();
- if (currentUser.isIdentifiedUser()) {
- handleGitReferenceUpdated(event);
- } else if (updaterAccountInfo != null) {
- handleGitReferenceUpdatedAsUser(event, Account.id(updaterAccountInfo._accountId));
- } else {
- handleGitReferenceUpdatedAsServer(event);
+ try {
+ AccountInfo updaterAccountInfo = event.getUpdater();
+ CurrentUser currentUser = currentUserProvider.get();
+ if (currentUser.isIdentifiedUser()) {
+ handleGitReferenceUpdated(event);
+ } else if (updaterAccountInfo != null) {
+ handleGitReferenceUpdatedAsUser(event, Account.id(updaterAccountInfo._accountId));
+ } else {
+ handleGitReferenceUpdatedAsServer(event);
+ }
+ } catch (StorageException | NoSuchProjectException e) {
+ logger.warn("Unable to process event {} on project {}", event, event.getProjectName(), e);
}
}
- private void handleGitReferenceUpdatedAsUser(Event event, Account.Id updaterAccountId) {
+ private void handleGitReferenceUpdatedAsUser(Event event, Account.Id updaterAccountId)
+ throws NoSuchProjectException {
try (ManualRequestContext ctx = oneOffReqCtx.openAs(updaterAccountId)) {
handleGitReferenceUpdated(event);
- } catch (StorageException e) {
- logger.warn("Unable to process event {} on project {}", event, event.getProjectName(), e);
}
}
- private void handleGitReferenceUpdatedAsServer(Event event) {
+ private void handleGitReferenceUpdatedAsServer(Event event) throws NoSuchProjectException {
try (ManualRequestContext ctx = oneOffReqCtx.open()) {
handleGitReferenceUpdated(event);
- } catch (StorageException e) {
- logger.warn("Unable to process event {} on project {}", event, event.getProjectName(), e);
}
}
- private void handleGitReferenceUpdated(Event event) {
+ private void handleGitReferenceUpdated(Event event) throws NoSuchProjectException {
String projectName = event.getProjectName();
Repository repository;
try {
NameKey projectNameKey = Project.NameKey.parse(projectName);
+ boolean autoAssignWip = cfg.autoAssignWip(projectNameKey);
repository = repositoryManager.openRepository(projectNameKey);
try {
String refName = event.getRefName();
Change.Id changeId = Change.Id.fromRef(refName);
- if (changeId != null
- && (!RefNames.isNoteDbMetaRef(refName)
- || isChangeSetReadyForReview(projectNameKey, changeId, event.getNewObjectId()))) {
- processEvent(repository, event, changeId);
+ if (changeId != null) {
+ ChangeNotes changeNotes = notesFactory.createChecked(projectNameKey, changeId);
+ if ((!RefNames.isNoteDbMetaRef(refName)
+ && isChangeToBeProcessed(changeNotes.getChange(), autoAssignWip))
+ || isChangeSetReadyForReview(repository, changeNotes, event.getNewObjectId())) {
+ processEvent(projectNameKey, repository, event, changeId);
+ }
}
} finally {
repository.close();
@@ -147,18 +165,41 @@
}
}
- private boolean isChangeSetReadyForReview(
- NameKey project, Change.Id changeId, String metaObjectId) {
- ChangeNotes changeNotes = notesFactory.createChecked(project, changeId);
- return !changeNotes.getChange().isWorkInProgress()
- && changeNotes.getChangeMessages().stream()
- .filter(message -> message.getKey().uuid().equals(metaObjectId))
- .map(message -> message.getTag())
- .filter(Predicates.notNull())
- .anyMatch(tag -> tag.contains(ChangeMessagesUtil.TAG_SET_READY));
+ private boolean isChangeToBeProcessed(Change change, boolean autoAssignWip) {
+ return !change.isWorkInProgress() || autoAssignWip;
}
- public void processEvent(Repository repository, Event event, Change.Id cId) {
+ private boolean isChangeSetReadyForReview(
+ Repository repository, ChangeNotes changeNotes, String metaObjectId)
+ throws MissingObjectException, IncorrectObjectTypeException, IOException {
+ if (changeNotes.getChange().isWorkInProgress()) {
+ return false;
+ }
+
+ if (changeNotes.getChangeMessages().stream()
+ .filter(message -> message.getKey().uuid().equals(metaObjectId))
+ .map(message -> message.getTag())
+ .filter(Predicates.notNull())
+ .anyMatch(tag -> tag.contains(ChangeMessagesUtil.TAG_SET_READY))) {
+ return true;
+ }
+
+ try (ChangeNotesRevWalk revWalk = ChangeNotesCommit.newRevWalk(repository)) {
+ ChangeNotesCommit metaCommit = revWalk.parseCommit(ObjectId.fromString(metaObjectId));
+ if (metaCommit.getParentCount() == 0) {
+ // The first commit cannot be a 'Set ready' operation
+ return false;
+ }
+ List<String> wipFooterLines = metaCommit.getFooterLines(FOOTER_WORK_IN_PROGRESS);
+ return wipFooterLines != null
+ && !wipFooterLines.isEmpty()
+ && Boolean.FALSE.toString().equalsIgnoreCase(wipFooterLines.get(0));
+ }
+ }
+
+ public void processEvent(
+ Project.NameKey projectNameKey, Repository repository, Event event, Change.Id cId)
+ throws NoSuchProjectException {
Changes changes = api.changes();
// The provider injected by Gerrit is shared with other workers on the
// same local thread and thus cannot be closed in this event listener.
@@ -170,11 +211,13 @@
PathOwners owners = new PathOwners(accounts, repository, change.branch, patchList);
Set<Account.Id> allReviewers = Sets.newHashSet();
allReviewers.addAll(owners.get().values());
+ allReviewers.addAll(owners.getReviewers().values());
for (Matcher matcher : owners.getMatchers().values()) {
allReviewers.addAll(matcher.getOwners());
+ allReviewers.addAll(matcher.getReviewers());
}
logger.debug("Autoassigned reviewers are: {}", allReviewers.toString());
- reviewerManager.addReviewers(cApi, allReviewers);
+ reviewerManager.addReviewers(projectNameKey, cApi, allReviewers);
}
} catch (RestApiException e) {
logger.warn("Could not open change: {}", cId, e);
diff --git a/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/ReviewerManager.java b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/ReviewerManager.java
index 1347d33..64ba615 100644
--- a/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/ReviewerManager.java
+++ b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/ReviewerManager.java
@@ -16,11 +16,15 @@
package com.googlesource.gerrit.owners.common;
import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Project.NameKey;
import com.google.gerrit.extensions.api.changes.ChangeApi;
+import com.google.gerrit.server.project.NoSuchProjectException;
+
import java.util.Collection;
public interface ReviewerManager {
- void addReviewers(ChangeApi cApi, Collection<Account.Id> reviewers)
- throws ReviewerManagerException;
+ public void addReviewers(
+ NameKey projectNameKey, ChangeApi cApi, Collection<Account.Id> accountsIds)
+ throws ReviewerManagerException, NoSuchProjectException;
}
diff --git a/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/SyncReviewerManager.java b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/SyncReviewerManager.java
index 71c1ac2..65e0bd1 100644
--- a/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/SyncReviewerManager.java
+++ b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/SyncReviewerManager.java
@@ -20,22 +20,33 @@
import com.google.gerrit.entities.Account.Id;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.api.changes.AddReviewerInput;
+import com.google.gerrit.extensions.api.changes.AttentionSetInput;
import com.google.gerrit.extensions.api.changes.ChangeApi;
import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.util.ManualRequestContext;
import com.google.gerrit.server.util.OneOffRequestContext;
import com.google.inject.Inject;
import com.google.inject.Singleton;
+import com.googlesource.gerrit.owners.api.OwnersAttentionSet;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -49,36 +60,63 @@
private final ChangeData.Factory changeDataFactory;
private final PermissionBackend permissionBackend;
+ /**
+ * TODO: The optional injection here is needed for keeping backward compatibility with existing
+ * setups that do not have the owners-api.jar configured as Gerrit libModule.
+ *
+ * <p>Once merged to master, the optional injection can go and this can be moved as extra argument
+ * in the constructor.
+ */
+ @Inject(optional = true)
+ private DynamicItem<OwnersAttentionSet> ownersForAttentionSet;
+
+ private final AutoAssignConfig cfg;
+
@Inject
public SyncReviewerManager(
OneOffRequestContext requestContext,
GerritApi gApi,
IdentifiedUser.GenericFactory userFactory,
ChangeData.Factory changeDataFactory,
- PermissionBackend permissionBackend) {
+ PermissionBackend permissionBackend,
+ AutoAssignConfig cfg) {
this.requestContext = requestContext;
this.gApi = gApi;
this.userFactory = userFactory;
this.changeDataFactory = changeDataFactory;
this.permissionBackend = permissionBackend;
+ this.cfg = cfg;
}
@Override
- public void addReviewers(ChangeApi cApi, Collection<Account.Id> reviewers)
- throws ReviewerManagerException {
+ public void addReviewers(
+ NameKey projectNameKey, ChangeApi cApi, Collection<Account.Id> accountsIds)
+ throws ReviewerManagerException, NoSuchProjectException {
try {
ChangeInfo changeInfo = cApi.get();
+ Set<Integer> currentReviewers =
+ changeInfo.reviewers.values().stream()
+ .flatMap(Collection::stream)
+ .map(ri -> ri._accountId)
+ .collect(Collectors.toSet());
+ ReviewerState reviewerState = cfg.autoassignedReviewerState(projectNameKey);
try (ManualRequestContext ctx =
requestContext.openAs(Account.id(changeInfo.owner._accountId))) {
// TODO(davido): Switch back to using changes API again,
// when it supports batch mode for adding reviewers
ReviewInput in = new ReviewInput();
- in.reviewers = new ArrayList<>(reviewers.size());
- for (Account.Id account : reviewers) {
- if (isVisibleTo(changeInfo, account)) {
+ in.reviewers = new ArrayList<>(accountsIds.size());
+ Collection<Account.Id> validOwnersForAttentionSet = new ArrayList<>(accountsIds.size());
+ for (Account.Id account : accountsIds) {
+ if (!currentReviewers.contains(account.get()) && isVisibleTo(changeInfo, account)) {
AddReviewerInput addReviewerInput = new AddReviewerInput();
addReviewerInput.reviewer = account.toString();
+ addReviewerInput.state = reviewerState;
in.reviewers.add(addReviewerInput);
+
+ if (reviewerState == ReviewerState.REVIEWER) {
+ validOwnersForAttentionSet.add(account);
+ }
} else {
log.warn(
"Not adding account {} as reviewer to change {} because the associated ref is not visible",
@@ -86,6 +124,28 @@
changeInfo._number);
}
}
+
+ Collection<Account.Id> reviewersAccounts;
+ if (validOwnersForAttentionSet.isEmpty()) {
+ reviewersAccounts = Collections.emptyList();
+ } else {
+ reviewersAccounts =
+ Optional.ofNullable(ownersForAttentionSet)
+ .map(DynamicItem::get)
+ .filter(Objects::nonNull)
+ .map(owners -> owners.addToAttentionSet(changeInfo, validOwnersForAttentionSet))
+ .orElse(validOwnersForAttentionSet);
+ }
+
+ in.ignoreAutomaticAttentionSetRules = true;
+ in.addToAttentionSet =
+ reviewersAccounts.stream()
+ .map(
+ (reviewer) ->
+ new AttentionSetInput(
+ reviewer.toString(), "Selected as member of the OWNERS file"))
+ .collect(Collectors.toList());
+
gApi.changes().id(changeInfo.id).current().review(in);
}
} catch (RestApiException e) {
diff --git a/owners-autoassign/src/main/resources/Documentation/attention-set.md b/owners-autoassign/src/main/resources/Documentation/attention-set.md
new file mode 100644
index 0000000..7ad9a55
--- /dev/null
+++ b/owners-autoassign/src/main/resources/Documentation/attention-set.md
@@ -0,0 +1,88 @@
+## Attention-Set
+
+The owners-autoassign plugin allows to customize the selection of owners
+that need to be added to the attention-set.
+By default, Gerrit adds all reviewers to the attention-set, which could
+not be ideal when the list of owners automatically assigned could be
+quite long, due to the hierarchy of the OWNERS files in the parent
+directories.
+
+The `owners-api.jar` libModule included in the owners' plugin project contains
+a generic interface that can be used to customize Gerrit's default
+attention-set behaviour.
+
+## owner-api setup
+
+Copy the `owners-api.jar` libModule into the $GERRIT_SITE/lib directory
+and add the following entry to `gerrit.config`:
+
+```
+[gerrit]
+ installModule = com.googlesource.gerrit.owners.api.OwnersApiModule
+```
+
+## Customization of the attention-set selection
+
+The OwnersAttentionSet API, contained in the owners-api.jar libModule,
+provides the following interface:
+
+```
+public interface OwnersAttentionSet {
+
+ Collection<Account.Id> addToAttentionSet(ChangeInfo changeInfo, Collection<Account.Id> owners);
+}
+```
+
+Any other plugin, or script, can implement the interface and provide
+an alternative implementation of the Gerrit's default mechanism.
+
+Example: select two random owners and add to the attention set by adding the
+following script as $GERRIT_SITE/plugins/owners-attentionset-1.0.groovy.
+
+```
+import com.google.inject.*
+import com.google.gerrit.common.*
+import com.google.gerrit.entities.*
+import com.google.gerrit.extensions.common.*
+import com.google.gerrit.extensions.registration.*
+import com.googlesource.gerrit.owners.api.*
+import java.util.*
+
+@Singleton
+class MyAttentionSet implements OwnersAttentionSet {
+ def desiredAttentionSet = 3
+
+ Collection<Account.Id> addToAttentionSet(ChangeInfo changeInfo, Collection<Account.Id> owners) {
+ def currentAttentionSet = changeInfo.attentionSet.size()
+
+ // There is already the desired number of attention-set
+ if (currentAttentionSet >= desiredAttentionSet) {
+ return Collections.emptyList()
+ }
+
+ // All owners are within the attention-set limits
+ if (owners.size() <= desiredAttentionSet) {
+ return owners
+ }
+
+ // Select randomly some owners for the attention-set
+ def shuffledOwners = owners.asType(List)
+ Collections.shuffle shuffledOwners
+ return shuffledOwners.subList(0,desiredAttentionSet)
+ }
+}
+
+class MyAttentionSetModule extends AbstractModule {
+
+ protected void configure() {
+ DynamicItem.bind(binder(), OwnersAttentionSet.class)
+ .to(MyAttentionSet.class)
+ .in(Scopes.SINGLETON)
+ }
+}
+
+modules = [ MyAttentionSetModule.class ]
+```
+
+**NOTE**: Install the [groovy-provider plugin](https://gerrit.googlesource.com/plugins/scripting/groovy-provider/)
+for enabling Gerrit to load Groovy scripts as plugins.
\ No newline at end of file
diff --git a/owners-autoassign/src/main/resources/Documentation/config.md b/owners-autoassign/src/main/resources/Documentation/config.md
index b294ae8..afb534c 100644
--- a/owners-autoassign/src/main/resources/Documentation/config.md
+++ b/owners-autoassign/src/main/resources/Documentation/config.md
@@ -1,4 +1,15 @@
-## Configuration
+## Project configuration
+
+The project configuration `autoAssignWip` controls the automatic
+assignment of reviewers based on the OWNERS file on WIP changes.
+
+The setting can be inherited from the parent project by setting the value
+to `INHERIT`.
+
+By default, all changes are subject to auto-assignment, unless the project
+or one of its parent projects has the `autoAssignWip` set to `FALSE`.
+
+## OWNERS configuration
Owner approval is determined based on OWNERS files located in the same
repository on the target branch of the changes uploaded for review.
@@ -84,6 +95,27 @@
That means that in the absence of any OWNERS file in the target branch, the refs/meta/config
OWNERS is used as global default.
+## Additional non-owners added as reviewers
+
+The OWNERS file can also contain a section called `reviewers` that allows
+to add extra people as reviewers to a change without having to make them
+owners and therefore without having any impact on the underlying validation
+rules.
+
+See for instance the example below, where `john@example.com` is added as an additional
+reviewer in addition to the owners.
+
+```yaml
+inherited: true
+owners:
+- some.email@example.com
+- User Name
+reviewers:
+- john@example.com
+```
+
+The `reviewers` optional section can be added in any place where `owners` is specified
+and can be also associated with matchers exactly in the same way that `owners` do.
## Example 1 - OWNERS file without matchers and default Gerrit submit rules
diff --git a/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/AbstractAutoassignIT.java b/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/AbstractAutoassignIT.java
new file mode 100644
index 0000000..b77cdab
--- /dev/null
+++ b/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/AbstractAutoassignIT.java
@@ -0,0 +1,266 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+// implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.owners.common;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.googlesource.gerrit.owners.common.AutoassignConfigModule.PROJECT_CONFIG_AUTOASSIGN_FIELD;
+
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.common.RawInputUtil;
+import com.google.gerrit.extensions.api.changes.AddReviewerInput;
+import com.google.gerrit.extensions.api.changes.ChangeApi;
+import com.google.gerrit.extensions.api.changes.ChangeEditApi;
+import com.google.gerrit.extensions.client.ReviewerState;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.project.ProjectConfig;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.junit.Ignore;
+import org.junit.Test;
+
+@Ignore
+public abstract class AbstractAutoassignIT extends LightweightPluginDaemonTest {
+ private static final String PLUGIN_NAME = "owners-api";
+
+ private final String section;
+ private final boolean INHERITED = true;
+ private final boolean NOT_INHERITED = false;
+ private final ReviewerState assignedUserState;
+
+ @SuppressWarnings("hiding")
+ @Inject
+ private ProjectConfig.Factory projectConfigFactory;
+
+ AbstractAutoassignIT(String section, ReviewerState assignedUserState) {
+ this.section = section;
+ this.assignedUserState = assignedUserState;
+ }
+
+ public static class TestModule extends AbstractModule {
+ @Override
+ protected void configure() {
+ install(new AutoassignModule());
+ }
+ }
+
+ @Override
+ public void setUpTestPlugin() throws Exception {
+ super.setUpTestPlugin();
+
+ try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
+ ProjectConfig projectConfig = projectConfigFactory.create(project);
+ projectConfig.load(md);
+ projectConfig.updatePluginConfig(
+ PLUGIN_NAME,
+ cfg -> cfg.setString(PROJECT_CONFIG_AUTOASSIGN_FIELD, assignedUserState.name()));
+ projectConfig.commit(md);
+ projectCache.evict(project);
+ }
+ }
+
+ @Test
+ public void shouldAutoassignUserInPath() throws Exception {
+ String ownerEmail = user.email();
+
+ addOwnersToRepo("", ownerEmail, NOT_INHERITED);
+
+ Collection<AccountInfo> reviewers = getAutoassignedAccounts(change(createChange()).get());
+
+ assertThat(reviewers).isNotNull();
+ assertThat(reviewers).hasSize(1);
+ assertThat(reviewersEmail(reviewers).get(0)).isEqualTo(ownerEmail);
+ }
+
+ @Test
+ public void shouldNotReAutoassignUserInPath() throws Exception {
+ String ownerEmail = user.email();
+
+ addOwnersToRepo("", ownerEmail, NOT_INHERITED);
+
+ ChangeApi changeApi = change(createChange());
+ ChangeInfo changeInfo = changeApi.get();
+ Collection<AccountInfo> reviewers = getAutoassignedAccounts(changeInfo);
+ assertThat(reviewers).hasSize(1);
+
+ // Switch user from CC to Reviewer or the other way around
+ AddReviewerInput switchReviewerInput = new AddReviewerInput();
+ switchReviewerInput.reviewer = ownerEmail;
+ switchReviewerInput.state =
+ assignedUserState == ReviewerState.REVIEWER ? ReviewerState.CC : ReviewerState.REVIEWER;
+ changeApi.addReviewer(switchReviewerInput);
+
+ ChangeEditApi changeEdit = changeApi.edit();
+ changeEdit.create();
+ changeEdit.modifyFile("foo", RawInputUtil.create("foo content"));
+ changeEdit.publish();
+
+ // It should not re-assign any user
+ assertThat(getAutoassignedAccounts(changeInfo)).isNull();
+ }
+
+ @Test
+ public void shouldAutoassignUserInPathWithInheritance() throws Exception {
+ String childOwnersEmail = accountCreator.user2().email();
+ String parentOwnersEmail = user.email();
+ String childpath = "childpath/";
+
+ addOwnersToRepo("", parentOwnersEmail, NOT_INHERITED);
+ addOwnersToRepo(childpath, childOwnersEmail, INHERITED);
+
+ Collection<AccountInfo> reviewers =
+ getAutoassignedAccounts(
+ change(createChange("test change", childpath + "foo.txt", "foo")).get());
+
+ assertThat(reviewers).isNotNull();
+ assertThat(reviewersEmail(reviewers)).containsExactly(parentOwnersEmail, childOwnersEmail);
+ }
+
+ @Test
+ public void shouldAutoassignUserInPathWithoutInheritance() throws Exception {
+ String childOwnersEmail = accountCreator.user2().email();
+ String parentOwnersEmail = user.email();
+ String childpath = "childpath/";
+
+ addOwnersToRepo("", parentOwnersEmail, NOT_INHERITED);
+ addOwnersToRepo(childpath, childOwnersEmail, NOT_INHERITED);
+
+ Collection<AccountInfo> reviewers =
+ getAutoassignedAccounts(
+ change(createChange("test change", childpath + "foo.txt", "foo")).get());
+
+ assertThat(reviewers).isNotNull();
+ assertThat(reviewersEmail(reviewers)).containsExactly(childOwnersEmail);
+ }
+
+ @Test
+ public void shouldAutoassignUserMatchingPath() throws Exception {
+ String ownerEmail = user.email();
+
+ addOwnersToRepo("", "suffix", ".java", ownerEmail, NOT_INHERITED);
+
+ Collection<AccountInfo> reviewers =
+ getAutoassignedAccounts(change(createChange("test change", "foo.java", "foo")).get());
+
+ assertThat(reviewers).isNotNull();
+ assertThat(reviewersEmail(reviewers)).containsExactly(ownerEmail);
+ }
+
+ @Test
+ public void shouldNotAutoassignUserNotMatchingPath() throws Exception {
+ String ownerEmail = user.email();
+
+ addOwnersToRepo("", "suffix", ".java", ownerEmail, NOT_INHERITED);
+
+ ChangeApi changeApi = change(createChange("test change", "foo.bar", "foo"));
+ Collection<AccountInfo> reviewers = getAutoassignedAccounts(changeApi.get());
+
+ assertThat(reviewers).isNull();
+ }
+
+ @Test
+ public void shouldAutoassignUserMatchingPathWithInheritance() throws Exception {
+ String childOwnersEmail = accountCreator.user2().email();
+ String parentOwnersEmail = user.email();
+ String childpath = "childpath/";
+
+ addOwnersToRepo("", "suffix", ".java", parentOwnersEmail, NOT_INHERITED);
+ addOwnersToRepo(childpath, "suffix", ".java", childOwnersEmail, INHERITED);
+
+ ChangeApi changeApi = change(createChange("test change", childpath + "foo.java", "foo"));
+ Collection<AccountInfo> reviewers = getAutoassignedAccounts(changeApi.get());
+
+ assertThat(reviewers).isNotNull();
+ assertThat(reviewersEmail(reviewers)).containsExactly(parentOwnersEmail, childOwnersEmail);
+ }
+
+ @Test
+ public void shouldAutoassignUserMatchingPathWithoutInheritance() throws Exception {
+ String childOwnersEmail = accountCreator.user2().email();
+ String parentOwnersEmail = user.email();
+ String childpath = "childpath/";
+
+ addOwnersToRepo("", parentOwnersEmail, NOT_INHERITED);
+ addOwnersToRepo(childpath, "suffix", ".java", childOwnersEmail, NOT_INHERITED);
+
+ ChangeApi changeApi = change(createChange("test change", childpath + "foo.java", "foo"));
+ Collection<AccountInfo> reviewers = getAutoassignedAccounts(changeApi.get());
+
+ assertThat(reviewers).isNotNull();
+ assertThat(reviewersEmail(reviewers)).containsExactly(childOwnersEmail);
+ }
+
+ private Collection<AccountInfo> getAutoassignedAccounts(ChangeInfo changeInfo)
+ throws RestApiException {
+ Collection<AccountInfo> reviewers =
+ gApi.changes().id(changeInfo._number).get().reviewers.get(assignedUserState);
+ return reviewers;
+ }
+
+ private List<String> reviewersEmail(Collection<AccountInfo> reviewers) {
+ List<String> reviewersEmail = reviewers.stream().map(a -> a.email).collect(Collectors.toList());
+ return reviewersEmail;
+ }
+
+ private void addOwnersToRepo(String parentPath, String ownerEmail, boolean inherited)
+ throws Exception {
+ pushFactory
+ .create(
+ admin.newIdent(),
+ testRepo,
+ "Set OWNERS",
+ parentPath + "OWNERS",
+ "inherited: " + inherited + "\n" + section + ":\n" + "- " + ownerEmail)
+ .to("refs/heads/master")
+ .assertOkStatus();
+ }
+
+ private void addOwnersToRepo(
+ String parentPath,
+ String matchingType,
+ String patternMatch,
+ String ownerEmail,
+ boolean inherited)
+ throws Exception {
+ pushFactory
+ .create(
+ admin.newIdent(),
+ testRepo,
+ "Set OWNERS",
+ parentPath + "OWNERS",
+ "inherited: "
+ + inherited
+ + "\n"
+ + "matchers:\n"
+ + "- "
+ + matchingType
+ + ": "
+ + patternMatch
+ + "\n"
+ + " "
+ + section
+ + ":\n"
+ + " - "
+ + ownerEmail)
+ .to("refs/heads/master")
+ .assertOkStatus();
+ }
+}
diff --git a/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/GitRefListenerIT.java b/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/GitRefListenerIT.java
index 18dea29..43aa41a 100644
--- a/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/GitRefListenerIT.java
+++ b/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/GitRefListenerIT.java
@@ -15,19 +15,29 @@
package com.googlesource.gerrit.owners.common;
+import static com.googlesource.gerrit.owners.common.AutoassignConfigModule.PROJECT_CONFIG_AUTOASSIGN_WIP_CHANGES;
import static org.junit.Assert.assertEquals;
import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.acceptance.TestPlugin;
-import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.projects.ConfigInput;
+import com.google.gerrit.extensions.api.projects.ConfigValue;
+import com.google.gerrit.extensions.client.InheritableBoolean;
import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.registration.Extension;
+import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.util.ManualRequestContext;
import com.google.gerrit.server.util.ThreadLocalRequestContext;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
+import com.googlesource.gerrit.owners.common.AutoassignConfigModule;
+import java.util.HashMap;
+import java.util.Map;
import java.util.stream.StreamSupport;
import com.googlesource.gerrit.owners.common.ReviewerManager;
@@ -38,6 +48,7 @@
name = "owners-autoassign",
sysModule = "com.googlesource.gerrit.owners.common.GitRefListenerIT$TestModule")
public class GitRefListenerIT extends LightweightPluginDaemonTest {
+ private static final String PLUGIN_NAME = "owners-autoassign";
@Inject DynamicSet<GitReferenceUpdatedListener> allRefUpdateListeners;
@Inject ThreadLocalRequestContext requestContext;
@@ -49,24 +60,16 @@
@Override
protected void configure() {
DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(GitRefListenerTest.class);
+ install(new AutoassignConfigModule());
}
}
@Test
public void shouldNotProcessNoteDbOnlyRefs() throws Exception {
- String changeRefPrefix = createChange().getChange().getId().toRefPrefix();
+ int changeNum = createChange().getChange().getId().get();
int baselineProcessedEvents = gitRefListener().getProcessedEvents();
- ReferenceUpdatedEventTest refUpdatedEvent =
- new ReferenceUpdatedEventTest(
- project,
- changeRefPrefix + RefNames.META_SUFFIX.substring(1),
- anOldObjectId,
- aNewObjectId,
- Type.CREATE,
- admin.id());
-
- gitRefListener().onGitReferenceUpdated(refUpdatedEvent);
+ gApi.changes().id(changeNum).current().review(new ReviewInput().message("Foo comment"));
assertEquals(baselineProcessedEvents, gitRefListener().getProcessedEvents());
}
@@ -82,6 +85,69 @@
}
@Test
+ public void shouldProcessSendAndStartReviewOnNoteDb() throws Exception {
+ int wipChangeNum = createChange().getChange().getId().get();
+ gApi.changes().id(wipChangeNum).setWorkInProgress();
+
+ int baselineProcessedEvents = gitRefListener().getProcessedEvents();
+
+ ReviewInput input = new ReviewInput().message("Let's start the review");
+ input.setWorkInProgress(false);
+
+ RestResponse resp =
+ adminRestSession.post("/changes/" + wipChangeNum + "/revisions/1/review", input);
+ resp.assertOK();
+
+ assertEquals(1, gitRefListener().getProcessedEvents() - baselineProcessedEvents);
+ }
+
+ @Test
+ public void shouldProcessWipChangesByDefault() throws Exception {
+ int baselineProcessedEvents = gitRefListener().getProcessedEvents();
+
+ createChange("refs/for/master%wip");
+
+ assertEquals(1, gitRefListener().getProcessedEvents() - baselineProcessedEvents);
+ }
+
+ @Test
+ public void shouldNotProcessWipChanges() throws Exception {
+ setProjectPluginConfig(
+ project, PROJECT_CONFIG_AUTOASSIGN_WIP_CHANGES, InheritableBoolean.FALSE.name());
+ int baselineProcessedEvents = gitRefListener().getProcessedEvents();
+
+ createChange("refs/for/master%wip");
+
+ assertEquals(baselineProcessedEvents, gitRefListener().getProcessedEvents());
+ }
+
+ @Test
+ public void shouldNotProcessWipChangesWhenAutoAssignWipChangesIsInherited() throws Exception {
+ setProjectPluginConfig(
+ allProjects, PROJECT_CONFIG_AUTOASSIGN_WIP_CHANGES, InheritableBoolean.FALSE.name());
+ setProjectPluginConfig(
+ project, PROJECT_CONFIG_AUTOASSIGN_WIP_CHANGES, InheritableBoolean.INHERIT.name());
+ int baselineProcessedEvents = gitRefListener().getProcessedEvents();
+
+ createChange("refs/for/master%wip");
+
+ assertEquals(baselineProcessedEvents, gitRefListener().getProcessedEvents());
+ }
+
+ @Test
+ public void shouldProcessWipChangesWhenAutoAssignWipChangesIsOverridden() throws Exception {
+ setProjectPluginConfig(
+ allProjects, PROJECT_CONFIG_AUTOASSIGN_WIP_CHANGES, InheritableBoolean.FALSE.name());
+ setProjectPluginConfig(
+ project, PROJECT_CONFIG_AUTOASSIGN_WIP_CHANGES, InheritableBoolean.TRUE.name());
+ int baselineProcessedEvents = gitRefListener().getProcessedEvents();
+
+ createChange("refs/for/master%wip");
+
+ assertEquals(1, gitRefListener().getProcessedEvents() - baselineProcessedEvents);
+ }
+
+ @Test
public void shoulNotProcessEmptyMetaRefUpdatesAfterSetReadyForReviewOnNoteDb() throws Exception {
int wipChangeNum = createChange().getChange().getId().get();
gApi.changes().id(wipChangeNum).setWorkInProgress();
@@ -118,6 +184,19 @@
}
}
+ private void setProjectPluginConfig(Project.NameKey projectName, String configKey, String value)
+ throws RestApiException {
+ ConfigInput projectConfig = new ConfigInput();
+ Map<String, ConfigValue> ownerAutoassignConfig = new HashMap<>();
+ ConfigValue configValue = new ConfigValue();
+ configValue.value = value;
+ ownerAutoassignConfig.put(configKey, configValue);
+ projectConfig.pluginConfigValues = new HashMap<>();
+ projectConfig.pluginConfigValues.put(PLUGIN_NAME, ownerAutoassignConfig);
+
+ gApi.projects().name(projectName.get()).config(projectConfig);
+ }
+
private GitRefListenerTest gitRefListener() {
return (GitRefListenerTest)
StreamSupport.stream(allRefUpdateListeners.entries().spliterator(), false)
diff --git a/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/GitRefListenerTest.java b/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/GitRefListenerTest.java
index fef3f29..7178120 100644
--- a/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/GitRefListenerTest.java
+++ b/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/GitRefListenerTest.java
@@ -16,8 +16,8 @@
package com.googlesource.gerrit.owners.common;
import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.api.GerritApi;
-import com.google.gerrit.extensions.events.GitReferenceUpdatedListener.Event;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.notedb.ChangeNotes;
@@ -27,6 +27,7 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.googlesource.gerrit.owners.common.Accounts;
+import com.googlesource.gerrit.owners.common.AutoAssignConfig;
import com.googlesource.gerrit.owners.common.GitRefListener;
import com.googlesource.gerrit.owners.common.ReviewerManager;
import org.eclipse.jgit.lib.Repository;
@@ -46,7 +47,8 @@
SyncReviewerManager reviewerManager,
OneOffRequestContext oneOffReqCtx,
Provider<CurrentUser> currentUserProvider,
- ChangeNotes.Factory notesFactory) {
+ ChangeNotes.Factory notesFactory,
+ AutoAssignConfig cfg) {
super(
api,
patchListCache,
@@ -55,11 +57,13 @@
reviewerManager,
oneOffReqCtx,
currentUserProvider,
- notesFactory);
+ notesFactory,
+ cfg);
}
@Override
- public void processEvent(Repository repository, Event event, Change.Id cId) {
+ public void processEvent(
+ Project.NameKey projectKey, Repository repository, Event event, Change.Id cId) {
processedEvents++;
}
diff --git a/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/OwnersAutoassignAsCcIT.java b/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/OwnersAutoassignAsCcIT.java
new file mode 100644
index 0000000..ff71c87
--- /dev/null
+++ b/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/OwnersAutoassignAsCcIT.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+// implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.owners.common;
+
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.extensions.client.ReviewerState;
+
+@TestPlugin(
+ name = "owners-api",
+ sysModule = "com.googlesource.gerrit.owners.common.AbstractAutoassignIT$TestModule")
+public class OwnersAutoassignAsCcIT extends AbstractAutoassignIT {
+
+ public OwnersAutoassignAsCcIT() {
+ super("owners", ReviewerState.CC);
+ }
+}
diff --git a/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/OwnersAutoassignAsReviewerIT.java b/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/OwnersAutoassignAsReviewerIT.java
new file mode 100644
index 0000000..06db78d
--- /dev/null
+++ b/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/OwnersAutoassignAsReviewerIT.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+// implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.owners.common;
+
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.extensions.client.ReviewerState;
+
+@TestPlugin(
+ name = "owners-api",
+ sysModule = "com.googlesource.gerrit.owners.common.AbstractAutoassignIT$TestModule")
+public class OwnersAutoassignAsReviewerIT extends AbstractAutoassignIT {
+
+ public OwnersAutoassignAsReviewerIT() {
+ super("owners", ReviewerState.REVIEWER);
+ }
+}
diff --git a/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/OwnersAutoassignWithAttentionSetIT.java b/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/OwnersAutoassignWithAttentionSetIT.java
new file mode 100644
index 0000000..36795a6
--- /dev/null
+++ b/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/OwnersAutoassignWithAttentionSetIT.java
@@ -0,0 +1,135 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+// implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.owners.common;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static java.util.stream.Collectors.toList;
+
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.entities.AccessSection;
+import com.google.gerrit.entities.Account.Id;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Permission;
+import com.google.gerrit.extensions.api.groups.GroupInput;
+import com.google.gerrit.extensions.client.ReviewerState;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Scopes;
+import com.googlesource.gerrit.owners.api.OwnersApiModule;
+import com.googlesource.gerrit.owners.api.OwnersAttentionSet;
+import java.util.Collection;
+import java.util.Collections;
+import org.junit.Test;
+
+@TestPlugin(
+ name = "owners-autoassign",
+ sysModule =
+ "com.googlesource.gerrit.owners.common.OwnersAutoassignWithAttentionSetIT$TestModule")
+public class OwnersAutoassignWithAttentionSetIT extends LightweightPluginDaemonTest {
+
+ @Inject private ProjectOperations projectOperations;
+
+ @Override
+ public Module createModule() {
+ return new OwnersApiModule();
+ }
+
+ public static class TestModule extends AbstractModule {
+ @Override
+ protected void configure() {
+ install(new AutoassignModule());
+
+ DynamicItem.bind(binder(), OwnersAttentionSet.class)
+ .to(SelectFirstOwnerForAttentionSet.class)
+ .in(Scopes.SINGLETON);
+ }
+ }
+
+ public static class SelectFirstOwnerForAttentionSet implements OwnersAttentionSet {
+
+ @Override
+ public Collection<Id> addToAttentionSet(ChangeInfo changeInfo, Collection<Id> owners) {
+ return owners.stream().limit(1).collect(toList());
+ }
+ }
+
+ @Test
+ public void shouldAutoassignTwoOwnersWithOneAttentionSet() throws Exception {
+ String ownerEmail1 = user.email();
+ String ownerEmail2 = accountCreator.user2().email();
+
+ setOwners(ownerEmail1, ownerEmail2);
+
+ ChangeInfo change = change(createChange()).get();
+ assertThat(change.reviewers.get(ReviewerState.REVIEWER)).hasSize(2);
+ assertThat(change.attentionSet).hasSize(1);
+ }
+
+ @Test
+ public void shouldAddToAttentionSetOneUserIfAnotherUserHasNoPermission() throws Exception {
+ TestAccount userWithAccessToProject = accountCreator.user();
+ TestAccount userWithNoAccessToProject = accountCreator.user2();
+
+ AccountGroup.UUID groupWithNoAccessToProject =
+ createGroup("groupWithNoAccessToProject", userWithNoAccessToProject);
+
+ setOwners(userWithAccessToProject.email(), userWithNoAccessToProject.email());
+
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(block(Permission.READ).ref(AccessSection.ALL).group(groupWithNoAccessToProject))
+ .update();
+
+ ChangeInfo change = change(createChange()).get();
+ assertThat(change.attentionSet).isNotNull();
+ assertThat(change.attentionSet.keySet()).containsExactly(userWithAccessToProject.id().get());
+ }
+
+ private void setOwners(String ownerEmail1, String ownerEmail2) throws Exception {
+ pushFactory
+ .create(
+ admin.newIdent(),
+ testRepo,
+ "Set OWNERS",
+ "OWNERS",
+ "inherited: false\n"
+ + "owners:\n"
+ + "- "
+ + ownerEmail1
+ + "\n"
+ + "- "
+ + ownerEmail2
+ + "\n")
+ .to("refs/heads/master")
+ .assertOkStatus();
+ }
+
+ private AccountGroup.UUID createGroup(String name, TestAccount member) throws RestApiException {
+ GroupInput groupInput = new GroupInput();
+ groupInput.name = name(name);
+ groupInput.members = Collections.singletonList(String.valueOf(member.id().get()));
+ return AccountGroup.uuid(gApi.groups().create(groupInput).get().id);
+ }
+}
diff --git a/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/ReviewersAutoassignAsCcIT.java b/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/ReviewersAutoassignAsCcIT.java
new file mode 100644
index 0000000..ce9cc02
--- /dev/null
+++ b/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/ReviewersAutoassignAsCcIT.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+// implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.owners.common;
+
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.extensions.client.ReviewerState;
+
+@TestPlugin(
+ name = "owners-api",
+ sysModule = "com.googlesource.gerrit.owners.common.AbstractAutoassignIT$TestModule")
+public class ReviewersAutoassignAsCcIT extends AbstractAutoassignIT {
+
+ public ReviewersAutoassignAsCcIT() {
+ super("reviewers", ReviewerState.CC);
+ }
+}
diff --git a/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/ReviewersAutoassignAsReviewerIT.java b/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/ReviewersAutoassignAsReviewerIT.java
new file mode 100644
index 0000000..8e2e45b
--- /dev/null
+++ b/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/ReviewersAutoassignAsReviewerIT.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+// implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.owners.common;
+
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.extensions.client.ReviewerState;
+
+@TestPlugin(
+ name = "owners-api",
+ sysModule = "com.googlesource.gerrit.owners.common.AbstractAutoassignIT$TestModule")
+public class ReviewersAutoassignAsReviewerIT extends AbstractAutoassignIT {
+
+ public ReviewersAutoassignAsReviewerIT() {
+ super("reviewers", ReviewerState.REVIEWER);
+ }
+}
diff --git a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/ConfigurationParser.java b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/ConfigurationParser.java
index 5b1049d..2fa2b16 100644
--- a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/ConfigurationParser.java
+++ b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/ConfigurationParser.java
@@ -30,7 +30,6 @@
import org.slf4j.LoggerFactory;
public class ConfigurationParser {
-
private static final Logger log = LoggerFactory.getLogger(OwnersConfig.class);
private Accounts accounts;
@@ -55,21 +54,14 @@
}
private void addClassicMatcher(JsonNode jsonNode, OwnersConfig ret) {
- Optional<Stream<String>> owners =
- Optional.ofNullable(jsonNode.get("owners")).map(ConfigurationParser::extractOwners);
- ret.setOwners(flattenSet(owners));
- }
-
- private static <T> Set<T> flattenSet(Optional<Stream<T>> optionalStream) {
- return flatten(optionalStream).collect(Collectors.toSet());
- }
-
- private static <T> Stream<T> flatten(Optional<Stream<T>> optionalStream) {
- return optionalStream.orElse(Stream.empty());
+ ret.setOwners(toClassicOwnersList(jsonNode, "owners").collect(Collectors.toSet()));
+ ret.setReviewers(toClassicOwnersList(jsonNode, "reviewers").collect(Collectors.toSet()));
}
private void addMatchers(JsonNode jsonNode, OwnersConfig ret) {
- getNode(jsonNode, "matchers").map(this::getMatchers).ifPresent(m -> m.forEach(ret::addMatcher));
+ getNode(jsonNode, "matchers")
+ .map(m -> getMatchers(m))
+ .ifPresent(m -> m.forEach(ret::addMatcher));
}
private Stream<Matcher> getMatchers(JsonNode node) {
@@ -79,29 +71,43 @@
.map(m -> m.get());
}
- private static Stream<String> extractOwners(JsonNode node) {
+ private static Stream<String> extractAsText(JsonNode node) {
if (node.isTextual()) {
return Stream.of(node.asText());
}
return iteratorStream(node.iterator()).map(JsonNode::asText);
}
+ private Stream<String> toClassicOwnersList(JsonNode jsonNode, String sectionName) {
+ Stream<String> ownersStream =
+ Optional.ofNullable(jsonNode.get(sectionName))
+ .map(ConfigurationParser::extractAsText)
+ .orElse(Stream.empty());
+ return ownersStream;
+ }
+
private Optional<Matcher> toMatcher(JsonNode node) {
Set<Id> owners =
- flatten(getNode(node, "owners").map(ConfigurationParser::extractOwners))
+ getNode(node, "owners")
+ .map(ConfigurationParser::extractAsText)
+ .orElse(Stream.empty())
.flatMap(o -> accounts.find(o).stream())
.collect(Collectors.toSet());
- if (owners.isEmpty()) {
- log.warn("Matchers must contain a list of owners");
- return Optional.empty();
- }
+ Set<Id> reviewers =
+ getNode(node, "reviewers")
+ .map(ConfigurationParser::extractAsText)
+ .orElse(Stream.empty())
+ .flatMap(o -> accounts.find(o).stream())
+ .collect(Collectors.toSet());
Optional<Matcher> suffixMatcher =
- getText(node, "suffix").map(el -> new SuffixMatcher(el, owners));
- Optional<Matcher> regexMatcher = getText(node, "regex").map(el -> new RegExMatcher(el, owners));
+ getText(node, "suffix").map(el -> new SuffixMatcher(el, owners, reviewers));
+ Optional<Matcher> regexMatcher =
+ getText(node, "regex").map(el -> new RegExMatcher(el, owners, reviewers));
Optional<Matcher> partialRegexMatcher =
- getText(node, "partial_regex").map(el -> new PartialRegExMatcher(el, owners));
- Optional<Matcher> exactMatcher = getText(node, "exact").map(el -> new ExactMatcher(el, owners));
+ getText(node, "partial_regex").map(el -> new PartialRegExMatcher(el, owners, reviewers));
+ Optional<Matcher> exactMatcher =
+ getText(node, "exact").map(el -> new ExactMatcher(el, owners, reviewers));
return Optional.ofNullable(
suffixMatcher.orElseGet(
diff --git a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/ExactMatcher.java b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/ExactMatcher.java
index 07dadbe..d97b01e 100644
--- a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/ExactMatcher.java
+++ b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/ExactMatcher.java
@@ -16,15 +16,21 @@
package com.googlesource.gerrit.owners.common;
import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Account.Id;
import java.util.Set;
public class ExactMatcher extends Matcher {
- public ExactMatcher(String path, Set<Account.Id> owners) {
- super(path, owners);
+ public ExactMatcher(String path, Set<Account.Id> owners, Set<Account.Id> reviewers) {
+ super(path, owners, reviewers);
}
@Override
public boolean matches(String pathToMatch) {
return pathToMatch.equals(path);
}
+
+ @Override
+ protected Matcher clone(Set<Id> owners, Set<Id> reviewers) {
+ return new ExactMatcher(path, owners, reviewers);
+ }
}
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 c1a7368..0f0db10 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
@@ -40,8 +40,7 @@
}
try (final TreeWalk w =
- TreeWalk.forPath(
- repository, path, parseCommit(repository, objectId).getTree())) {
+ TreeWalk.forPath(repository, path, parseCommit(repository, objectId).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/Matcher.java b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/Matcher.java
index 412fa79..7c1565f 100644
--- a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/Matcher.java
+++ b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/Matcher.java
@@ -14,21 +14,25 @@
package com.googlesource.gerrit.owners.common;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Account.Id;
import java.util.Set;
public abstract class Matcher {
private Set<Account.Id> owners;
+ private Set<Account.Id> reviewers;
protected String path;
- public Matcher(String key, Set<Account.Id> owners) {
+ public Matcher(String key, Set<Account.Id> owners, Set<Account.Id> reviewers) {
this.path = key;
this.owners = owners;
+ this.reviewers = reviewers;
}
@Override
public String toString() {
- return "Matcher [path=" + path + ", owners=" + owners + "]";
+ return "Matcher [path=" + path + ", owners=" + owners + ", reviewers=" + reviewers + "]";
}
public Set<Account.Id> getOwners() {
@@ -39,6 +43,14 @@
this.owners = owners;
}
+ public Set<Account.Id> getReviewers() {
+ return reviewers;
+ }
+
+ public void setReviewers(Set<Account.Id> reviewers) {
+ this.reviewers = reviewers;
+ }
+
public void setPath(String path) {
this.path = path;
}
@@ -48,4 +60,19 @@
}
public abstract boolean matches(String pathToMatch);
+
+ public Matcher merge(Matcher other) {
+ if (other == null) {
+ return this;
+ }
+
+ return clone(mergeSet(owners, other.owners), mergeSet(reviewers, other.reviewers));
+ }
+
+ protected abstract Matcher clone(Set<Id> owners, Set<Id> reviewers);
+
+ private Set<Id> mergeSet(Set<Id> set1, Set<Id> set2) {
+ ImmutableSet.Builder<Id> setBuilder = ImmutableSet.builder();
+ return setBuilder.addAll(set1).addAll(set2).build();
+ }
}
diff --git a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/OwnersConfig.java b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/OwnersConfig.java
index 0c29516..d6e40eb 100644
--- a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/OwnersConfig.java
+++ b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/OwnersConfig.java
@@ -30,8 +30,8 @@
/** Flag for marking that this OWNERS file inherits from the parent OWNERS. */
private boolean inherited = true;
- /** Set of OWNER email addresses. */
private Set<String> owners = Sets.newHashSet();
+ private Set<String> reviewers = Sets.newHashSet();
/** Map name of matcher and Matcher (value + Set Owners) */
private Map<String, Matcher> matchers = Maps.newHashMap();
@@ -63,6 +63,14 @@
this.owners = owners;
}
+ public Set<String> getReviewers() {
+ return reviewers;
+ }
+
+ public void setReviewers(Set<String> reviewers) {
+ this.reviewers = reviewers;
+ }
+
public Map<String, Matcher> getMatchers() {
return matchers;
}
diff --git a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/OwnersMap.java b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/OwnersMap.java
index 06d9d12..111b88e 100644
--- a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/OwnersMap.java
+++ b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/OwnersMap.java
@@ -25,8 +25,10 @@
public class OwnersMap {
private SetMultimap<String, Account.Id> pathOwners = HashMultimap.create();
+ private SetMultimap<String, Account.Id> pathReviewers = HashMultimap.create();
private Map<String, Matcher> matchers = Maps.newHashMap();
private Map<String, Set<Account.Id>> fileOwners = Maps.newHashMap();
+ private Map<String, Set<Account.Id>> fileReviewers = Maps.newHashMap();
@Override
public String toString() {
@@ -45,6 +47,14 @@
this.pathOwners = pathOwners;
}
+ public SetMultimap<String, Account.Id> getPathReviewers() {
+ return pathReviewers;
+ }
+
+ public void setPathReviewers(SetMultimap<String, Account.Id> pathReviewers) {
+ this.pathReviewers = pathReviewers;
+ }
+
public Map<String, Matcher> getMatchers() {
return matchers;
}
@@ -57,10 +67,18 @@
pathOwners.putAll(ownersPath, owners);
}
+ public void addPathReviewers(String ownersPath, Set<Id> reviewers) {
+ pathOwners.putAll(ownersPath, reviewers);
+ }
+
public Map<String, Set<Id>> getFileOwners() {
return fileOwners;
}
+ public Map<String, Set<Id>> getFileReviewers() {
+ return fileReviewers;
+ }
+
public void addFileOwners(String file, Set<Id> owners) {
if (owners.isEmpty()) {
return;
@@ -74,4 +92,18 @@
fileOwners.put(file, Sets.newHashSet(owners));
}
}
+
+ public void addFileReviewers(String file, Set<Id> reviewers) {
+ if (reviewers.isEmpty()) {
+ return;
+ }
+
+ Set<Id> set = fileReviewers.get(file);
+ if (set != null) {
+ // add new owners removing duplicates
+ set.addAll(reviewers);
+ } else {
+ fileReviewers.put(file, Sets.newHashSet(reviewers));
+ }
+ }
}
diff --git a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PartialRegExMatcher.java b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PartialRegExMatcher.java
index c046c13..18f04f5 100644
--- a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PartialRegExMatcher.java
+++ b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PartialRegExMatcher.java
@@ -16,6 +16,7 @@
package com.googlesource.gerrit.owners.common;
import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Account.Id;
import java.util.Set;
import java.util.regex.Pattern;
@@ -23,8 +24,8 @@
public class PartialRegExMatcher extends Matcher {
Pattern pattern;
- public PartialRegExMatcher(String path, Set<Account.Id> owners) {
- super(path, owners);
+ public PartialRegExMatcher(String path, Set<Account.Id> owners, Set<Account.Id> reviewers) {
+ super(path, owners, reviewers);
pattern = Pattern.compile(".*" + path + ".*");
}
@@ -32,4 +33,9 @@
public boolean matches(String pathToMatch) {
return pattern.matcher(pathToMatch).matches();
}
+
+ @Override
+ protected Matcher clone(Set<Id> owners, Set<Id> reviewers) {
+ return new PartialRegExMatcher(path, owners, reviewers);
+ }
}
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 77289a3..6bf288b 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
@@ -31,6 +31,7 @@
import com.google.gerrit.server.patch.PatchList;
import com.google.gerrit.server.patch.PatchListEntry;
import java.io.IOException;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
@@ -49,6 +50,8 @@
private final SetMultimap<String, Account.Id> owners;
+ private final SetMultimap<String, Account.Id> reviewers;
+
private final Repository repository;
private final PatchList patchList;
@@ -69,6 +72,7 @@
OwnersMap map = fetchOwners(branch);
owners = Multimaps.unmodifiableSetMultimap(map.getPathOwners());
+ reviewers = Multimaps.unmodifiableSetMultimap(map.getPathReviewers());
matchers = map.getMatchers();
fileOwners = map.getFileOwners();
}
@@ -82,6 +86,15 @@
return owners;
}
+ /**
+ * Returns a read only view of the paths to reviewers mapping.
+ *
+ * @return multimap of paths to reviewers
+ */
+ public SetMultimap<String, Account.Id> getReviewers() {
+ return reviewers;
+ }
+
public Map<String, Matcher> getMatchers() {
return matchers;
}
@@ -102,12 +115,28 @@
PathOwnersEntry projectEntry =
getOwnersConfig(rootPath, RefNames.REFS_CONFIG)
- .map(conf -> new PathOwnersEntry(rootPath, conf, accounts, Collections.emptySet()))
+ .map(
+ conf ->
+ new PathOwnersEntry(
+ rootPath,
+ conf,
+ accounts,
+ Collections.emptySet(),
+ Collections.emptySet(),
+ Collections.emptySet()))
.orElse(new PathOwnersEntry());
PathOwnersEntry rootEntry =
getOwnersConfig(rootPath, branch)
- .map(conf -> new PathOwnersEntry(rootPath, conf, accounts, Collections.emptySet()))
+ .map(
+ conf ->
+ new PathOwnersEntry(
+ rootPath,
+ conf,
+ accounts,
+ Collections.emptySet(),
+ Collections.emptySet(),
+ Collections.emptySet()))
.orElse(new PathOwnersEntry());
Set<String> modifiedPaths = getModifiedPaths();
@@ -116,13 +145,15 @@
for (String path : modifiedPaths) {
currentEntry = resolvePathEntry(path, branch, projectEntry, rootEntry, entries);
- // add owners to file for matcher predicates
+ // add owners and reviewers to file for matcher predicates
ownersMap.addFileOwners(path, currentEntry.getOwners());
+ ownersMap.addFileReviewers(path, currentEntry.getReviewers());
// Only add the path to the OWNERS file to reduce the number of
// entries in the result
if (currentEntry.getOwnersPath() != null) {
ownersMap.addPathOwners(currentEntry.getOwnersPath(), currentEntry.getOwners());
+ ownersMap.addPathReviewers(currentEntry.getOwnersPath(), currentEntry.getReviewers());
}
ownersMap.addMatchers(currentEntry.getMatchers());
}
@@ -157,6 +188,7 @@
if (matcher.matches(path)) {
newMatchers.put(matcher.getPath(), matcher);
ownersMap.addFileOwners(path, matcher.getOwners());
+ ownersMap.addFileReviewers(path, matcher.getReviewers());
}
}
}
@@ -200,14 +232,14 @@
String ownersPath = partial + "OWNERS";
Optional<OwnersConfig> conf = getOwnersConfig(ownersPath, branch);
final Set<Id> owners = currentEntry.getOwners();
+ final Set<Id> reviewers = currentEntry.getReviewers();
+ Collection<Matcher> inheritedMatchers = currentEntry.getMatchers().values();
currentEntry =
- conf.map(c -> new PathOwnersEntry(ownersPath, c, accounts, owners))
+ conf.map(
+ c ->
+ new PathOwnersEntry(
+ ownersPath, c, accounts, owners, reviewers, inheritedMatchers))
.orElse(currentEntry);
- if (conf.map(OwnersConfig::isInherited).orElse(false)) {
- for (Matcher m : currentEntry.getMatchers().values()) {
- currentEntry.addMatcher(m);
- }
- }
entries.put(partial, currentEntry);
}
}
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 d2c73ec..6c498d6 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,22 +31,40 @@
*/
class PathOwnersEntry {
private final boolean inherited;
+ private Set<Account.Id> owners = Sets.newHashSet();
+ private Set<Account.Id> reviewers = Sets.newHashSet();
+ private String ownersPath;
+ private Map<String, Matcher> matchers = Maps.newHashMap();
public PathOwnersEntry() {
inherited = true;
}
public PathOwnersEntry(
- String path, OwnersConfig config, Accounts accounts, Set<Account.Id> inheritedOwners) {
+ String path,
+ OwnersConfig config,
+ Accounts accounts,
+ Set<Account.Id> inheritedOwners,
+ Set<Account.Id> inheritedReviewers,
+ Collection<Matcher> inheritedMatchers) {
this.ownersPath = path;
this.owners =
config.getOwners().stream()
.flatMap(o -> accounts.find(o).stream())
.collect(Collectors.toSet());
+ this.reviewers =
+ config.getReviewers().stream()
+ .flatMap(o -> accounts.find(o).stream())
+ .collect(Collectors.toSet());
+ this.matchers = config.getMatchers();
+
if (config.isInherited()) {
this.owners.addAll(inheritedOwners);
+ this.reviewers.addAll(inheritedReviewers);
+ for (Matcher matcher : inheritedMatchers) {
+ addMatcher(matcher);
+ }
}
- this.matchers = config.getMatchers();
this.inherited = config.isInherited();
}
@@ -61,13 +79,9 @@
+ "]";
}
- private String ownersPath;
- private Set<Account.Id> owners = Sets.newHashSet();
-
- private Map<String, Matcher> matchers = Maps.newHashMap();
-
public void addMatcher(Matcher matcher) {
- this.matchers.put(matcher.getPath(), matcher);
+ Matcher currMatchers = this.matchers.get(matcher.getPath());
+ this.matchers.put(matcher.getPath(), matcher.merge(currMatchers));
}
public Map<String, Matcher> getMatchers() {
@@ -82,6 +96,14 @@
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;
}
diff --git a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/RegExMatcher.java b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/RegExMatcher.java
index 45fd615..13f3636 100644
--- a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/RegExMatcher.java
+++ b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/RegExMatcher.java
@@ -16,14 +16,15 @@
package com.googlesource.gerrit.owners.common;
import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Account.Id;
import java.util.Set;
import java.util.regex.Pattern;
public class RegExMatcher extends Matcher {
Pattern pattern;
- public RegExMatcher(String path, Set<Account.Id> owners) {
- super(path, owners);
+ public RegExMatcher(String path, Set<Account.Id> owners, Set<Account.Id> reviewers) {
+ super(path, owners, reviewers);
pattern = Pattern.compile(path);
}
@@ -31,4 +32,9 @@
public boolean matches(String pathToMatch) {
return pattern.matcher(pathToMatch).matches();
}
+
+ @Override
+ protected Matcher clone(Set<Id> owners, Set<Id> reviewers) {
+ return new RegExMatcher(path, owners, reviewers);
+ }
}
diff --git a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/SuffixMatcher.java b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/SuffixMatcher.java
index 39e8b32..6b56cc4 100644
--- a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/SuffixMatcher.java
+++ b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/SuffixMatcher.java
@@ -16,15 +16,21 @@
package com.googlesource.gerrit.owners.common;
import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Account.Id;
import java.util.Set;
public class SuffixMatcher extends Matcher {
- public SuffixMatcher(String path, Set<Account.Id> owners) {
- super(path, owners);
+ public SuffixMatcher(String path, Set<Account.Id> owners, Set<Account.Id> reviewers) {
+ super(path, owners, reviewers);
}
@Override
public boolean matches(String pathToMatch) {
return pathToMatch.endsWith(path);
}
+
+ @Override
+ protected Matcher clone(Set<Id> owners, Set<Id> reviewers) {
+ return new SuffixMatcher(path, owners, reviewers);
+ }
}
diff --git a/owners-common/src/test/java/com/googlesource/gerrit/owners/common/RegexMatcherTest.java b/owners-common/src/test/java/com/googlesource/gerrit/owners/common/RegexMatcherTest.java
index 1fd557b..c61827e 100644
--- a/owners-common/src/test/java/com/googlesource/gerrit/owners/common/RegexMatcherTest.java
+++ b/owners-common/src/test/java/com/googlesource/gerrit/owners/common/RegexMatcherTest.java
@@ -23,18 +23,18 @@
public class RegexMatcherTest {
@Test
public void testRegex() {
- RegExMatcher matcher = new RegExMatcher(".*/a.*", null);
+ RegExMatcher matcher = new RegExMatcher(".*/a.*", null, null);
assertTrue(matcher.matches("xxxxxx/axxxx"));
assertFalse(matcher.matches("axxxx"));
assertFalse(matcher.matches("xxxxx/bxxxx"));
- RegExMatcher matcher2 = new RegExMatcher("a.*.sql", null);
+ RegExMatcher matcher2 = new RegExMatcher("a.*.sql", null, null);
assertFalse(matcher2.matches("xxxxxx/alfa.sql"));
}
@Test
public void testFloatingRegex() {
- PartialRegExMatcher matcher = new PartialRegExMatcher("a.*.sql", null);
+ PartialRegExMatcher matcher = new PartialRegExMatcher("a.*.sql", null, null);
assertTrue(matcher.matches("xxxxxxx/alfa.sql"));
assertTrue(matcher.matches("alfa.sqlxxxxx"));
assertFalse(matcher.matches("alfa.bar"));
diff --git a/owners/BUILD b/owners/BUILD
index 3acc05b..ab6705e 100644
--- a/owners/BUILD
+++ b/owners/BUILD
@@ -36,7 +36,6 @@
"Implementation-URL: https://gerrit.googlesource.com/plugins/owners",
"Gerrit-PluginName: owners",
"Gerrit-Module: com.googlesource.gerrit.owners.OwnersModule",
- "Gerrit-ApiVersion: 2.16",
],
resources = glob(["src/main/resources/**/*"]),
deps = [
diff --git a/plugins/owners-api b/plugins/owners-api
new file mode 120000
index 0000000..d90303f
--- /dev/null
+++ b/plugins/owners-api
@@ -0,0 +1 @@
+../owners-api
\ No newline at end of file