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