Merge branch 'stable-2.15' into stable-2.16

* stable-2.15:
  Bump bazel version to 1.0.0
  Upgrade bazlets to latest stable-2.15 to build with 2.15.17 API

Change-Id: I8779416563e221b0302d9ceaeb9cb8735af906a3
diff --git a/BUILD b/BUILD
index 2699f2b..0d3e09b 100644
--- a/BUILD
+++ b/BUILD
@@ -6,6 +6,8 @@
     "PLUGIN_TEST_DEPS",
     "gerrit_plugin",
 )
+load("//tools/bzl:genrule2.bzl", "genrule2")
+load("//tools/bzl:js.bzl", "polygerrit_plugin")
 
 SRC = "src/main/java/com/googlesource/gerrit/plugins/reviewers/"
 
@@ -38,6 +40,28 @@
         "Gerrit-Module: com.googlesource.gerrit.plugins.reviewers.Module",
     ],
     resources = glob(["src/main/**/*"]),
+    resource_jars = [":rv-reviewers-static"],
+)
+
+genrule2(
+    name = "rv-reviewers-static",
+    srcs = [":rv-reviewers"],
+    outs = ["rv-reviewers-static.jar"],
+    cmd = " && ".join([
+        "mkdir $$TMP/static",
+        "cp -r $(locations :rv-reviewers) $$TMP/static",
+        "cd $$TMP",
+        "zip -Drq $$ROOT/$@ -g .",
+    ]),
+)
+
+polygerrit_plugin(
+    name = "rv-reviewers",
+    srcs = glob([
+        "rv-reviewers/*.html",
+        "rv-reviewers/*.js",
+    ]),
+    app = "plugin.html",
 )
 
 junit_tests(
diff --git a/WORKSPACE b/WORKSPACE
index 7f84954..2a95e3e 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -3,10 +3,36 @@
 load("//:bazlets.bzl", "load_bazlets")
 
 load_bazlets(
-    commit = "ae1cd231b0262b2738e6c0593eb3c504209ad4f5",
+    commit = "ec989bb514e39447764057c60d3f9959bff8e153",
     #local_path = "/home/<user>/projects/bazlets",
 )
 
+# Polymer dependencies
+load(
+    "@com_googlesource_gerrit_bazlets//:gerrit_polymer.bzl",
+    "gerrit_polymer",
+)
+
+gerrit_polymer()
+
+# Load closure compiler with transitive dependencies
+load("@io_bazel_rules_closure//closure:defs.bzl", "closure_repositories")
+
+closure_repositories()
+
+# Load Gerrit npm_binary toolchain
+load("@com_googlesource_gerrit_bazlets//tools:js.bzl", "GERRIT", "npm_binary")
+
+npm_binary(
+    name = "polymer-bundler",
+    repository = GERRIT,
+)
+
+npm_binary(
+    name = "crisper",
+    repository = GERRIT,
+)
+
 # Release Plugin API
 load(
     "@com_googlesource_gerrit_bazlets//:gerrit_api.bzl",
diff --git a/plugin.html b/plugin.html
new file mode 100644
index 0000000..29c10cb
--- /dev/null
+++ b/plugin.html
@@ -0,0 +1,27 @@
+<!--
+@license
+Copyright (C) 2019 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.
+-->
+<link rel="import" href="./rv-reviewers/rv-reviewers.html">
+
+<dom-module id="reviewers">
+  <script>
+    if (window.Polymer) {
+      Gerrit.install(plugin => {
+        plugin.registerCustomComponent('repo-command', 'rv-reviewers');
+      });
+    }
+  </script>
+</dom-module>
diff --git a/rv-reviewers/rv-edit-screen.html b/rv-reviewers/rv-edit-screen.html
new file mode 100644
index 0000000..3291e3c
--- /dev/null
+++ b/rv-reviewers/rv-edit-screen.html
@@ -0,0 +1,77 @@
+<!--
+@license
+Copyright (C) 2019 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.
+-->
+<link rel="import" href="./rv-filter-section.html">
+
+<dom-module id="rv-edit-screen">
+  <template>
+    <style include="shared-styles"></style>
+    <style include="gr-menu-page-styles"></style>
+    <style include="gr-subpage-styles">
+      .bottomButtons {
+        display: flex;
+      }
+      #closeButton {
+        float: right;
+      }
+      #filterSections {
+          width: 100%;
+      }
+      header {
+        border-bottom: 1px solid var(--border-colo);
+        flex-shrink: 0;
+        font-weight: var(--font-weight-bold)
+      }
+    </style>
+    <div>
+      <header>Reviewers Config</header>
+      <table id="filterSections">
+        <tr>
+          <th>Filter Sections</th>
+        </tr>
+        <tr id="loading" class$="loadingMsg [[_computeLoadingClass(loading)]]">
+          <td>Loading...</td>
+        </tr>
+        <tbody class$="[[_computeLoadingClass(loading)]]">
+          <tr>
+            <template
+                is="dom-repeat"
+                items="[[_filterSections]]"
+                as="section">
+              <rv-filter-section
+                  filter="[[section.filter]]"
+                  reviewers="[[section.reviewers]]"
+                  editing="[[section.editing]]"
+                  reviewers-url="[[_getReviewersUrl(repoName)]]"
+                  repo-name="[[repoName]]"
+                  plugin-rest-api="[[pluginRestApi]]"
+                  can-modify-config="[[canModifyConfig]]"
+                  on-reviewer-changed="_handleReviewerChanged"></rv-filter-section>
+            </template>
+          </tr>
+        </tbody>
+      </table>
+      <div class="bottomButtons">
+        <gr-button id="closeButton" on-tap="_handleCloseTap">Close</gr-button>
+        <gr-button
+            id="addFilterBtn"
+            on-tap="_handleCreateSection"
+            hidden="[[_computeAddFilterBtnHidden(canModifyConfig, _editingFilter)]]">Add New Filter</gr-button>
+      </div>
+    </div>
+  </template>
+  <script src="./rv-edit-screen.js"></script>
+</dom-module>
diff --git a/rv-reviewers/rv-edit-screen.js b/rv-reviewers/rv-edit-screen.js
new file mode 100644
index 0000000..05e0df8
--- /dev/null
+++ b/rv-reviewers/rv-edit-screen.js
@@ -0,0 +1,68 @@
+// Copyright (C) 2019 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.
+(function() {
+  Polymer({
+    is: 'rv-edit-screen',
+
+    properties: {
+      pluginRestApi: {
+        type: Object,
+        observer: '_loadFilterSections'
+      },
+      repoName: String,
+      loading: Boolean,
+      canModifyConfig: Boolean,
+      _editingFilter: {
+        type: Boolean,
+        value: false,
+      },
+      _filterSections: Array,
+    },
+
+    _loadFilterSections() {
+      this.pluginRestApi.get(this._getReviewersUrl(this.repoName))
+          .then(filterSections => {
+            this._filterSections = filterSections;
+          });
+    },
+
+    _computeAddFilterBtnHidden(canModifyConfig, editingFilter) {
+      return !canModifyConfig || editingFilter;
+    },
+
+    _computeLoadingClass(loading) {
+      return loading ? 'loading' : '';
+    },
+
+    _getReviewersUrl(repoName) {
+      return `/projects/${encodeURIComponent(repoName)}/reviewers`;
+    },
+
+    _handleCreateSection() {
+      const section = {filter: '', reviewers: [], editing: true};
+      this._editingFilter = true;
+      this.push('_filterSections', section);
+    },
+
+    _handleCloseTap(e) {
+      e.preventDefault();
+      this.fire('close', null, {bubbles: false});
+    },
+
+    _handleReviewerChanged(e) {
+      this._filterSections = e.detail.result;
+      this._editingFilter = false;
+    },
+  });
+})();
diff --git a/rv-reviewers/rv-filter-section.html b/rv-reviewers/rv-filter-section.html
new file mode 100644
index 0000000..c657bda
--- /dev/null
+++ b/rv-reviewers/rv-filter-section.html
@@ -0,0 +1,98 @@
+<!--
+@license
+Copyright (C) 2019 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.
+-->
+<link rel="import" href="./rv-reviewer.html">
+
+<dom-module id="rv-filter-section">
+  <template>
+    <style include="shared-styles">
+      :host {
+        display: block;
+        margin-bottom: 1em;
+      }
+      fieldset {
+        border: 1px solid var(--border-color);
+      }
+      .name {
+        align-items: center;
+        display: flex;
+      }
+      .header {
+        align-items: center;
+        background: var(--table-header-background-color);
+        border-bottom: 1px dotted var(--border-color);
+        display: flex;
+        justify-content: space-between;
+        min-height: 3em;
+        padding: 0 .7em;
+      }
+      #addReviewer {
+        display: flex;
+      }
+      #editFilterInput {
+        width: 30vw;
+        max-width: 500px;
+        margin-left: 3px;
+      }
+      #mainContainer {
+        display: block;
+      }
+    </style>
+    <style include="gr-form-styles"></style>
+    <fieldset id="section"
+        class$="gr-form-styles">
+      <div id="mainContainer">
+        <div class="header">
+          <div class="name">
+            <h3>Filter:</h3>
+            <input
+                id="editFilterInput"
+                bind-value="{{filter}}"
+                is="iron-input"
+                type="text"
+                disabled="[[_computeFilterInputDisabled(canModifyConfig, _originalFilter)]]">
+            <gr-button
+                id="cancelBtn"
+                on-tap="_handleCancel"
+                hidden$="[[_computeCancelHidden(filter, _originalFilter)]]">Cancel</gr-button>
+          </div><!-- name -->
+        </div><!-- header -->
+        <div class="reviewers">
+          <template
+              is="dom-repeat"
+              items="{{reviewers}}">
+            <rv-reviewer
+                reviewer="{{item}}"
+                can-modify-config="[[canModifyConfig]]"
+                plugin-rest-api="[[pluginRestApi]]"
+                repo-name="[[repoName]]"
+                on-reviewer-deleted="_handleReviewerDeleted"
+                on-reviewer-added="_handleReviewerAdded">
+            </rv-reviewer>
+          </template>
+          <div id="addReviewer">
+            <gr-button
+                link
+                id="addBtn"
+                on-tap="_handleAddReviewer"
+                hidden="[[_computeAddBtnHidden(canModifyConfig, _editingReviewer)]]">Add Reviewer</gr-button>
+          </div><!-- addReviewer -->
+        </div><!-- reviewers -->
+      </div>
+    </fieldset>
+  </template>
+  <script src="./rv-filter-section.js"></script>
+</dom-module>
diff --git a/rv-reviewers/rv-filter-section.js b/rv-reviewers/rv-filter-section.js
new file mode 100644
index 0000000..110c27d
--- /dev/null
+++ b/rv-reviewers/rv-filter-section.js
@@ -0,0 +1,99 @@
+// Copyright (C) 2019 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.
+(function() {
+  Polymer({
+    is: 'rv-filter-section',
+
+    properties: {
+      pluginRestApi: Object,
+      repoName: String,
+      reviewers: Array,
+      filter: String,
+      canModifyConfig: Boolean,
+      _originalFilter: String,
+      _editingReviewer: {
+        type: Boolean,
+        value: false,
+      },
+      reviewersUrl: String,
+    },
+
+    attached() {
+      this._updateSection();
+    },
+
+    _updateSection() {
+      this._originalFilter = this.filter;
+    },
+
+    _computeEditing(filter, _originalFilter) {
+      if (_originalFilter === '') {
+        return true;
+      }
+      return filter === '';
+    },
+
+    _computeCancelHidden(filter, _originalFilter) {
+      return !this._computeEditing(filter, _originalFilter);
+    },
+
+    _computeAddBtnHidden(canModifyConfig, editingReviewer) {
+      return !(canModifyConfig && !editingReviewer);
+    },
+
+    _computeFilterInputDisabled(canModifyConfig, originalFilter) {
+      return !canModifyConfig || originalFilter !== '';
+    },
+
+    _handleCancel() {
+      this.remove();
+    },
+
+    _handleReviewerDeleted(e) {
+      if (e.detail.editing) {
+        this.reviewers.pop();
+        this._editingReviewer = false;
+      } else {
+        const index = e.model.index;
+        const deleted = this.reviewers[index];
+        this._putReviewer(deleted, 'DELETE');
+      }
+    },
+
+    _handleReviewerAdded(e) {
+      this._editingReviewer = false;
+      this._putReviewer(e.detail.reviewer, 'ADD').catch(err => {
+        this.fire('show-alert', {message: err});
+        throw err;
+      });
+    },
+
+    _putReviewer(reviewer, action) {
+      return this.pluginRestApi.put(this.reviewersUrl, {
+        action,
+        reviewer,
+        filter: this.filter,
+      }).then(result => {
+        const detail = {result};
+        this.dispatchEvent(
+            new CustomEvent('reviewer-changed', {detail, bubbles: true}));
+      });
+    },
+
+    _handleAddReviewer() {
+      this.push('reviewers', '');
+      this._editingReviewer = true;
+    },
+  });
+})();
diff --git a/rv-reviewers/rv-reviewer.html b/rv-reviewers/rv-reviewer.html
new file mode 100644
index 0000000..339eff7
--- /dev/null
+++ b/rv-reviewers/rv-reviewer.html
@@ -0,0 +1,80 @@
+<!--
+@license
+Copyright (C) 2019 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.
+-->
+<dom-module id="rv-reviewer">
+  <template>
+    <style include="shared-styles">
+      #editReviewerInput {
+        display: block;
+        width: 250px;
+      }
+      .reviewerRow {
+        align-items: center;
+        display: flex;
+      }
+      #reviewerHeader,
+      #editReviewerInput,
+      #deleteCancelBtn,
+      #addBtn,
+      #reviewerField {
+          margin-left: 3px;
+      }
+      #reviewerField {
+        width: 250px;
+        text-indent: 1px;
+        border: 1px solid var(--border-color);
+      }
+    </style>
+    <style include="gr-form-styles"></style>
+    <div class="reviewerRow">
+      <h4 id="reviewerHeader">Reviewer:</h4>
+      <template is="dom-if" if="[[_computeEditing(reviewer, _originalReviewer)]]">
+        <span class="value">
+            <!--
+              TODO:
+              Investigate wether we could reuse gr-account-list.
+              If the REST API returns AccountInfo instead of an account
+              identifier String we should be able to use gr-account-list(size=1)
+              for all reviewers, including those who are non-editable
+              (#reviewerField below) and allign the plugin with how accounts
+              are displayed in core Gerrit's UI.
+            -->
+            <gr-autocomplete
+                id="editReviewerInput"
+                text="{{reviewer}}"
+                value="{{_reviewerSearchId}}"
+                query="[[_queryReviewers]]"
+                placeholder="Name Or Email"
+                disabled="[[_computeReviewerDisabled(reviewer, _originalReviewer)]]">
+            </gr-autocomplete>
+        </span>
+      </template>
+      <template is="dom-if" if="[[!_computeEditing(reviewer, _originalReviewer)]]">
+        <td id="reviewerField">[[reviewer]]</td>
+      </template>
+      <gr-button
+          id="deleteCancelBtn"
+          on-tap="_handleDeleteCancel"
+          hidden$="[[_computeHideDeleteButton(canModifyConfig)]]"
+          >[[_computeDeleteCancel(reviewer, _originalReviewer)]]</gr-button>
+      <gr-button
+          id="addBtn"
+          on-tap="_handleAddReviewer"
+          hidden$="[[_computeHideAddButton(reviewer, _originalReviewer)]]">Add</gr-button>
+    </div> <!-- reviewerRow -->
+  </template>
+  <script src="./rv-reviewer.js"></script>
+</dom-module>
diff --git a/rv-reviewers/rv-reviewer.js b/rv-reviewers/rv-reviewer.js
new file mode 100644
index 0000000..0c8adde
--- /dev/null
+++ b/rv-reviewers/rv-reviewer.js
@@ -0,0 +1,140 @@
+// Copyright (C) 2019 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.
+(function() {
+  Polymer({
+    is: 'rv-reviewer',
+
+    properties: {
+      canModifyConfig: Boolean,
+      pluginRestAPi: Object,
+      repoName: String,
+      reviewer: String,
+      _reviewerSearchId: String,
+      _queryReviewers: {
+        type: Function,
+        value() {
+          return this._getReviewerSuggestions.bind(this);
+        },
+      },
+      _originalReviewer: String,
+      _deleted: Boolean,
+      _editing: {
+        type: Boolean,
+        computed: '_computeEditing(reviewer, _originalReviewer)',
+      },
+    },
+
+    attached() {
+      this._originalReviewer = this.reviewer;
+    },
+
+    _computeReviewerDisabled(reviewer, _originalReviewer) {
+      return !this._computeEditing(reviewer, _originalReviewer);
+    },
+
+    _computeEditing(reviewer, _originalReviewer) {
+      if (_originalReviewer === '') {
+        return true;
+      }
+      return reviewer === '';
+    },
+
+    _computeDeleteCancel(reviewer, _originalReviewer) {
+      return this._computeEditing(reviewer, _originalReviewer) ?
+      'Cancel' : 'Delete';
+    },
+
+    _computeHideAddButton(reviewer, _originalReviewer) {
+      return !(this._computeEditing(reviewer, _originalReviewer)
+      && this._reviewerSearchId);
+    },
+
+    _computeHideDeleteButton(canModifyConfig) {
+      return !canModifyConfig;
+    },
+
+    _getReviewerSuggestions(input) {
+      if (input.length === 0) { return Promise.resolve([]); }
+      const promises = [];
+      promises.push(this._getSuggestedGroups(input));
+      promises.push(this._getSuggestedAccounts(input));
+      return Promise.all(promises).then(result => {
+        return result.flat();
+      });
+    },
+
+    _getSuggestedGroups(input) {
+      const suggestUrl = `/groups/?suggest=${input}&p=${this.repoName}`;
+      return this.pluginRestApi.get(suggestUrl).then(groups => {
+        if (!groups) { return []; }
+        const groupSuggestions = [];
+        for (const key in groups) {
+          if (!groups.hasOwnProperty(key)) { continue; }
+          groupSuggestions.push({
+            name: key,
+            value: key,
+          });
+        }
+        return groupSuggestions;
+      });
+    },
+
+    _getSuggestedAccounts(input) {
+      const suggestUrl = `/accounts/?suggest&q=${input}`;
+      return this.pluginRestApi.get(suggestUrl).then(accounts => {
+        const accountSuggestions = [];
+        let nameAndEmail;
+        let value;
+        if (!accounts) { return []; }
+        for (const key in accounts) {
+          if (!accounts.hasOwnProperty(key)) { continue; }
+          if (accounts[key].email) {
+            nameAndEmail = accounts[key].name +
+              ' <' + accounts[key].email + '>';
+          } else {
+            nameAndEmail = accounts[key].name;
+          }
+          if (accounts[key].username) {
+            value = accounts[key].username;
+          } else if (accounts[key].email) {
+            value = accounts[key].email;
+          } else {
+            value = accounts[key]._account_id;
+          }
+          accountSuggestions.push({
+            name: nameAndEmail,
+            value,
+          });
+        }
+        return accountSuggestions;
+      });
+    },
+
+    _handleDeleteCancel() {
+      const detail = {editing: this._editing};
+      if (this._editing) {
+        this.remove();
+      }
+      this.dispatchEvent(
+          new CustomEvent('reviewer-deleted', {detail, bubbles: true}));
+    },
+
+    _handleAddReviewer() {
+      const detail = {reviewer: this._reviewerSearchId};
+      this._originalReviewer = this.reviewer;
+      this.dispatchEvent(
+          new CustomEvent('reviewer-added', {detail, bubbles: true}));
+    },
+  });
+})();
diff --git a/rv-reviewers/rv-reviewers.html b/rv-reviewers/rv-reviewers.html
new file mode 100644
index 0000000..f44aa6f
--- /dev/null
+++ b/rv-reviewers/rv-reviewers.html
@@ -0,0 +1,41 @@
+<!--
+@license
+Copyright (C) 2019 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.
+-->
+<link rel="import" href="./rv-edit-screen.html">
+
+<dom-module id="rv-reviewers">
+  <template>
+    <style include="shared-styles">
+      #rvScreenOverlay {
+        width: 50em;
+        overflow: auto;
+      }
+    </style>
+    <gr-repo-command
+        title="Reviewers Config"
+        on-command-tap="_handleCommandTap">
+    </gr-repo-command>
+    <gr-overlay id="rvScreenOverlay" with-backdrop>
+      <rv-edit-screen
+          plugin-rest-api="[[pluginRestApi]]"
+          repo-name="[[repoName]]"
+          loading="[[_loading]]"
+          can-modify-config="[[_canModifyConfig]]"
+          on-close="_handleRvEditScreenClose"></rv-edit-screen>
+    </gr-overlay>
+  </template>
+  <script src="./rv-reviewers.js"></script>
+</dom-module>
diff --git a/rv-reviewers/rv-reviewers.js b/rv-reviewers/rv-reviewers.js
new file mode 100644
index 0000000..e40e03a
--- /dev/null
+++ b/rv-reviewers/rv-reviewers.js
@@ -0,0 +1,82 @@
+// Copyright (C) 2019 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.
+(function() {
+  Polymer({
+    is: 'rv-reviewers',
+
+    properties: {
+      pluginRestApi: Object,
+      repoName: String,
+      _canModifyConfig: {
+        type: Boolean,
+        computed: '_computeCanModifyConfig(_isOwner, _hasModifyCapability)',
+      },
+      _loading: {
+        type: Boolean,
+        value: true,
+      },
+      _isOwner: {
+        type: Boolean,
+        value: false,
+      },
+      _hasModifyCapability: {
+        type: Boolean,
+        value: false,
+      },
+    },
+
+    attached() {
+      this.pluginRestApi = this.plugin.restApi();
+      this._setCanModifyConfig();
+    },
+
+    _handleCommandTap() {
+      this.$.rvScreenOverlay.open();
+    },
+
+    _handleRvEditScreenClose() {
+      this.$.rvScreenOverlay.close();
+    },
+
+    _setCanModifyConfig() {
+      const promises = [];
+      promises.push(this._getRepoAccess(this.repoName).then( access => {
+        if (access && access[this.repoName] && access[this.repoName].is_owner) {
+          this._isOwner = true;
+        }
+      }));
+      promises.push(this._getCapabilities().then(capabilities => {
+        if (capabilities['reviewers-modifyReviewersConfig']) {
+          this._hasModifyCapability = true;
+        }
+      }));
+      Promise.all(promises).then(() => {
+        this._loading = false;
+      });
+    },
+
+    _computeCanModifyConfig(isOwner, hasModifyCapability) {
+      return isOwner || hasModifyCapability;
+    },
+
+    _getRepoAccess(repoName) {
+      return this.pluginRestApi.get(
+          '/access/?project=' + encodeURIComponent(repoName));
+    },
+
+    _getCapabilities() {
+      return this.pluginRestApi.get('/accounts/self/capabilities');
+    },
+  });
+})();
diff --git a/src/main/java/com/googlesource/gerrit/plugins/reviewers/ClientModule.java b/src/main/java/com/googlesource/gerrit/plugins/reviewers/ClientModule.java
index 4aac38f..bf6acbb 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/reviewers/ClientModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/reviewers/ClientModule.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.webui.GwtPlugin;
+import com.google.gerrit.extensions.webui.JavaScriptPlugin;
 import com.google.gerrit.extensions.webui.TopMenu;
 import com.google.gerrit.extensions.webui.WebUiPlugin;
 import com.google.inject.AbstractModule;
@@ -25,5 +26,7 @@
   protected void configure() {
     DynamicSet.bind(binder(), TopMenu.class).to(ReviewersTopMenu.class);
     DynamicSet.bind(binder(), WebUiPlugin.class).toInstance(new GwtPlugin("reviewers"));
+    DynamicSet.bind(binder(), WebUiPlugin.class)
+        .toInstance(new JavaScriptPlugin("rv-reviewers.html"));
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/reviewers/server/AddReviewers.java b/src/main/java/com/googlesource/gerrit/plugins/reviewers/server/AddReviewers.java
index be04ea4..54d1552 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/reviewers/server/AddReviewers.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/reviewers/server/AddReviewers.java
@@ -14,6 +14,7 @@
 
 package com.googlesource.gerrit.plugins.reviewers.server;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -31,11 +32,9 @@
 import com.google.inject.ProvisionException;
 import java.util.ArrayList;
 import java.util.Set;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 abstract class AddReviewers implements Runnable {
-  private static final Logger log = LoggerFactory.getLogger(AddReviewers.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final ThreadLocalRequestContext tl;
   protected final GerritApi gApi;
@@ -112,7 +111,7 @@
       }
       gApi.changes().id(changeInfo._number).current().review(in);
     } catch (RestApiException e) {
-      log.error("Couldn't add reviewers to the change", e);
+      logger.atSevere().withCause(e).log("Couldn't add reviewers to the change");
     }
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/reviewers/server/PutReviewers.java b/src/main/java/com/googlesource/gerrit/plugins/reviewers/server/PutReviewers.java
index b90b350..cc985df 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/reviewers/server/PutReviewers.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/reviewers/server/PutReviewers.java
@@ -16,6 +16,7 @@
 
 import static com.googlesource.gerrit.plugins.reviewers.server.ModifyReviewersConfigCapability.MODIFY_REVIEWERS_CONFIG;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.api.access.PluginPermission;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -27,9 +28,11 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.account.AccountResolver;
-import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.group.GroupsCollection;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.group.GroupResolver;
 import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gwtorm.server.OrmException;
@@ -42,12 +45,10 @@
 import java.util.List;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 class PutReviewers implements RestModifyView<ProjectResource, Input> {
-  private static final Logger log = LoggerFactory.getLogger(PutReviewers.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static class Input {
     public Action action;
@@ -60,7 +61,7 @@
   private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
   private final ProjectCache projectCache;
   private final AccountResolver accountResolver;
-  private final Provider<GroupsCollection> groupsCollection;
+  private final Provider<GroupResolver> groupResolver;
   private final PermissionBackend permissionBackend;
 
   @Inject
@@ -70,20 +71,20 @@
       Provider<MetaDataUpdate.User> metaDataUpdateFactory,
       ProjectCache projectCache,
       AccountResolver accountResolver,
-      Provider<GroupsCollection> groupsCollection,
+      Provider<GroupResolver> groupResolver,
       PermissionBackend permissionBackend) {
     this.pluginName = pluginName;
     this.config = config;
     this.metaDataUpdateFactory = metaDataUpdateFactory;
     this.projectCache = projectCache;
     this.accountResolver = accountResolver;
-    this.groupsCollection = groupsCollection;
+    this.groupResolver = groupResolver;
     this.permissionBackend = permissionBackend;
   }
 
   @Override
   public List<ReviewerFilterSection> apply(ProjectResource rsrc, Input input)
-      throws RestApiException {
+      throws RestApiException, PermissionBackendException {
     Project.NameKey projectName = rsrc.getNameKey();
     ReviewersConfig.ForProject cfg = config.forProject(projectName);
     if (cfg == null) {
@@ -91,7 +92,7 @@
     }
 
     PermissionBackend.WithUser userPermission = permissionBackend.user(rsrc.getUser());
-    if (!rsrc.getControl().isOwner()
+    if (!userPermission.project(rsrc.getNameKey()).testOrFalse(ProjectPermission.WRITE_CONFIG)
         && !userPermission.testOrFalse(new PluginPermission(pluginName, MODIFY_REVIEWERS_CONFIG))) {
       throw new AuthException("not allowed to modify reviewers config");
     }
@@ -150,13 +151,13 @@
       Account account = accountResolver.find(reviewer);
       if (account == null) {
         try {
-          groupsCollection.get().parse(reviewer);
+          groupResolver.get().parse(reviewer);
         } catch (UnprocessableEntityException e) {
           throw new ResourceNotFoundException("Account or group " + reviewer + " not found");
         }
       }
     } catch (OrmException | IOException | ConfigInvalidException e) {
-      log.error("Failed to resolve account " + reviewer);
+      logger.atSevere().log("Failed to resolve account %s", reviewer);
     }
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/reviewers/server/Reviewers.java b/src/main/java/com/googlesource/gerrit/plugins/reviewers/server/Reviewers.java
index d21c538..07a7d18 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/reviewers/server/Reviewers.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/reviewers/server/Reviewers.java
@@ -20,6 +20,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -43,8 +44,6 @@
 import com.google.inject.Singleton;
 import java.util.List;
 import java.util.Set;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 class Reviewers
@@ -52,7 +51,7 @@
         PrivateStateChangedListener,
         WorkInProgressStateChangedListener,
         ReviewerSuggestion {
-  private static final Logger log = LoggerFactory.getLogger(Reviewers.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final ReviewersResolver resolver;
   private final AddReviewersByConfiguration.Factory byConfigFactory;
@@ -115,7 +114,7 @@
             .collect(toSet());
       }
     } catch (OrmException | QueryParseException x) {
-      log.error(x.getMessage(), x);
+      logger.atSevere().withCause(x).log(x.getMessage());
     }
     return ImmutableSet.of();
   }
@@ -161,13 +160,11 @@
 
       workQueue.getDefaultQueue().submit(task);
     } catch (QueryParseException e) {
-      log.warn(
-          "Could not add default reviewers for change {} of project {}, filter is invalid: {}",
-          changeNumber,
-          projectName.get(),
-          e.getMessage());
+      logger.atWarning().log(
+          "Could not add default reviewers for change %d of project %s, filter is invalid: %s",
+          changeNumber, projectName.get(), e.getMessage());
     } catch (OrmException x) {
-      log.error(x.getMessage(), x);
+      logger.atSevere().withCause(x).log(x.getMessage());
     }
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/reviewers/server/ReviewersConfig.java b/src/main/java/com/googlesource/gerrit/plugins/reviewers/server/ReviewersConfig.java
index b3e2ed2..fe06aa3 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/reviewers/server/ReviewersConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/reviewers/server/ReviewersConfig.java
@@ -17,11 +17,12 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.config.PluginConfigFactory;
-import com.google.gerrit.server.git.VersionedMetaData;
+import com.google.gerrit.server.git.meta.VersionedMetaData;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -32,12 +33,10 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class ReviewersConfig {
-  private static final Logger log = LoggerFactory.getLogger(ReviewersConfig.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   static final String FILENAME = "reviewers.config";
   static final String SECTION_FILTER = "filter";
@@ -71,7 +70,7 @@
     try {
       cfg = cfgFactory.getProjectPluginConfigWithMergedInheritance(projectName, pluginName);
     } catch (NoSuchProjectException e) {
-      log.error("Unable to get config for project {}", projectName.get());
+      logger.atSevere().log("Unable to get config for project %s", projectName.get());
       cfg = new Config();
     }
     return new ForProject(cfg);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/reviewers/server/ReviewersResolver.java b/src/main/java/com/googlesource/gerrit/plugins/reviewers/server/ReviewersResolver.java
index f7fe4f3..10785f2 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/reviewers/server/ReviewersResolver.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/reviewers/server/ReviewersResolver.java
@@ -18,16 +18,15 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.account.GroupMembers;
-import com.google.gerrit.server.group.GroupsCollection;
+import com.google.gerrit.server.group.GroupResolver;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -36,29 +35,24 @@
 import java.io.IOException;
 import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /* Resolve account and group names to account ids */
 @Singleton
 class ReviewersResolver {
-  private static final Logger log = LoggerFactory.getLogger(ReviewersResolver.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final AccountResolver accountResolver;
-  private final Provider<GroupsCollection> groupsCollection;
-  private final GroupMembers.Factory groupMembersFactory;
-  private final IdentifiedUser.GenericFactory identifiedUserFactory;
+  private final Provider<GroupResolver> groupResolver;
+  private final GroupMembers groupMembers;
 
   @Inject
   ReviewersResolver(
       AccountResolver accountResolver,
-      Provider<GroupsCollection> groupsCollection,
-      GroupMembers.Factory groupMembersFactory,
-      IdentifiedUser.GenericFactory identifiedUserFactory) {
+      Provider<GroupResolver> groupResolver,
+      GroupMembers groupMembers) {
     this.accountResolver = accountResolver;
-    this.groupsCollection = groupsCollection;
-    this.groupMembersFactory = groupMembersFactory;
-    this.identifiedUserFactory = identifiedUserFactory;
+    this.groupResolver = groupResolver;
+    this.groupMembers = groupMembers;
   }
 
   /**
@@ -78,26 +72,12 @@
       int changeNumber,
       @Nullable AccountInfo uploader) {
     Set<Account.Id> reviewers = Sets.newHashSetWithExpectedSize(names.size());
-    GroupMembers groupMembers = null;
     for (String name : names) {
       if (resolveAccount(project, changeNumber, uploader, reviewers, name)) {
         continue;
       }
 
-      if (groupMembers == null && uploader != null) {
-        groupMembers = createGroupMembers(project, changeNumber, uploader, name);
-      }
-
-      if (groupMembers != null) {
-        resolveGroup(project, changeNumber, reviewers, groupMembers, name);
-      } else {
-        log.warn(
-            "For the change {} of project {}: failed to list accounts for group {}; cannot retrieve uploader account for {}.",
-            changeNumber,
-            project,
-            name,
-            uploader.email);
-      }
+      resolveGroup(project, changeNumber, reviewers, groupMembers, name);
     }
     return reviewers;
   }
@@ -117,21 +97,16 @@
           }
           return true;
         }
-        log.warn(
-            "For the change {} of project {}: account {} is inactive.",
-            changeNumber,
-            project,
-            accountName);
+        logger.atWarning().log(
+            "For the change %d of project %s: account %s is inactive.",
+            changeNumber, project, accountName);
       }
     } catch (OrmException | IOException | ConfigInvalidException e) {
       // If the account doesn't exist, find() will return null.  We only
       // get here if something went wrong accessing the database
-      log.error(
-          "For the change {} of project {}: failed to resolve account {}.",
-          changeNumber,
-          project,
-          accountName,
-          e);
+      logger.atSevere().withCause(e).log(
+          "For the change %d of project %s: failed to resolve account %s.",
+          changeNumber, project, accountName);
       return true;
     }
     return false;
@@ -145,47 +120,20 @@
       String group) {
     try {
       Set<Account.Id> accounts =
-          groupMembers.listAccounts(groupsCollection.get().parse(group).getGroupUUID(), project)
+          groupMembers.listAccounts(groupResolver.get().parse(group).getGroupUUID(), project)
               .stream()
               .filter(Account::isActive)
               .map(Account::getId)
               .collect(toSet());
       reviewers.addAll(accounts);
-    } catch (UnprocessableEntityException | NoSuchGroupException e) {
-      log.warn(
-          "For the change {} of project {}: reviewer {} is neither an account nor a group.",
-          changeNumber,
-          project,
-          group);
-    } catch (NoSuchProjectException | IOException | OrmException e) {
-      log.error(
-          "For the change {} of project {}: failed to list accounts for group {}.",
-          changeNumber,
-          project,
-          group,
-          e);
+    } catch (UnprocessableEntityException e) {
+      logger.atWarning().log(
+          "For the change %d of project %s: reviewer %s is neither an account nor a group.",
+          changeNumber, project, group);
+    } catch (NoSuchProjectException | IOException e) {
+      logger.atSevere().withCause(e).log(
+          "For the change %d of project %s: failed to list accounts for group %s.",
+          changeNumber, project, group);
     }
   }
-
-  private GroupMembers createGroupMembers(
-      Project.NameKey project, int changeNumber, AccountInfo uploader, String group) {
-    // email is not unique to one account, try to locate the account using
-    // "Full name <email>" to increase chance of finding only one.
-    String uploaderNameEmail = String.format("%s <%s>", uploader.name, uploader.email);
-    try {
-      Account uploaderAccount = accountResolver.findByNameOrEmail(uploaderNameEmail);
-      if (uploaderAccount != null) {
-        return groupMembersFactory.create(identifiedUserFactory.create(uploaderAccount.getId()));
-      }
-    } catch (OrmException | IOException e) {
-      log.warn(
-          "For the change {} of project {}: failed to list accounts for group {}, cannot retrieve uploader account {}.",
-          changeNumber,
-          project,
-          group,
-          uploaderNameEmail,
-          e);
-    }
-    return null;
-  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/reviewers/server/SuggestProjectReviewers.java b/src/main/java/com/googlesource/gerrit/plugins/reviewers/server/SuggestProjectReviewers.java
index b8b076e..35619c1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/reviewers/server/SuggestProjectReviewers.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/reviewers/server/SuggestProjectReviewers.java
@@ -20,14 +20,14 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.ReviewersUtil;
-import com.google.gerrit.server.ReviewersUtil.VisibilityControl;
-import com.google.gerrit.server.change.SuggestReviewers;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.restapi.change.ReviewersUtil;
+import com.google.gerrit.server.restapi.change.ReviewersUtil.VisibilityControl;
+import com.google.gerrit.server.restapi.change.SuggestReviewers;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -43,18 +43,18 @@
   @Inject
   SuggestProjectReviewers(
       AccountVisibility av,
-      IdentifiedUser.GenericFactory identifiedUserFactory,
       Provider<ReviewDb> dbProvider,
       @GerritServerConfig Config cfg,
       ReviewersUtil reviewersUtil,
       PermissionBackend permissionBackend) {
-    super(av, identifiedUserFactory, dbProvider, cfg, reviewersUtil);
+    super(av, dbProvider, cfg, reviewersUtil);
     this.permissionBackend = permissionBackend;
   }
 
   @Override
   public List<SuggestedReviewerInfo> apply(ProjectResource rsrc)
-      throws BadRequestException, OrmException, IOException, ConfigInvalidException {
+      throws BadRequestException, OrmException, IOException, ConfigInvalidException,
+          PermissionBackendException {
     return reviewersUtil.suggestReviewers(
         null, this, rsrc.getProjectState(), getVisibility(rsrc), true);
   }
@@ -62,9 +62,9 @@
   private VisibilityControl getVisibility(final ProjectResource rsrc) {
     return new VisibilityControl() {
       @Override
-      public boolean isVisibleTo(Account.Id account) throws OrmException {
+      public boolean isVisibleTo(Account.Id account) {
         return permissionBackend
-            .user(identifiedUserFactory.create(account))
+            .absentUser(account)
             .project(rsrc.getNameKey())
             .testOrFalse(ProjectPermission.ACCESS);
       }
diff --git a/src/main/resources/Documentation/build.md b/src/main/resources/Documentation/build.md
index 1c80901..864485c 100644
--- a/src/main/resources/Documentation/build.md
+++ b/src/main/resources/Documentation/build.md
@@ -1,7 +1,7 @@
 Build
 =====
 
-This plugin can be built with Bazel.
+This plugin is built with Bazel.
 
 Two build modes are supported: Standalone and in Gerrit tree.
 The standalone build mode is recommended, as this mode doesn't require
diff --git a/src/test/java/com/googlesource/gerrit/plugins/reviewers/server/ReviewersConfigIT.java b/src/test/java/com/googlesource/gerrit/plugins/reviewers/server/ReviewersConfigIT.java
index c966a6f..32b2243 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/reviewers/server/ReviewersConfigIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/reviewers/server/ReviewersConfigIT.java
@@ -41,9 +41,7 @@
   private static final String JOHN_DOE = "john.doe@example.com";
 
   @Before
-  @Override
   public void setUp() throws Exception {
-    super.setUp();
     fetch(testRepo, RefNames.REFS_CONFIG + ":refs/heads/config");
     testRepo.reset("refs/heads/config");
   }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/reviewers/server/ReviewersIT.java b/src/test/java/com/googlesource/gerrit/plugins/reviewers/server/ReviewersIT.java
index 6036865..5c5f478 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/reviewers/server/ReviewersIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/reviewers/server/ReviewersIT.java
@@ -42,9 +42,7 @@
 @TestPlugin(name = "reviewers", sysModule = "com.googlesource.gerrit.plugins.reviewers.Module")
 public class ReviewersIT extends LightweightPluginDaemonTest {
   @Before
-  @Override
   public void setUp() throws Exception {
-    super.setUp();
     fetch(testRepo, RefNames.REFS_CONFIG + ":refs/heads/config");
     testRepo.reset("refs/heads/config");
   }
@@ -85,6 +83,42 @@
   }
 
   @Test
+  public void addReviewersMatchMultipleSections() throws Exception {
+    RevCommit oldHead = getRemoteHead();
+    TestAccount user2 = accountCreator.user2();
+
+    Config cfg = new Config();
+    cfg.setStringList(SECTION_FILTER, "*", KEY_REVIEWER, ImmutableList.of(user.email));
+    cfg.setStringList(SECTION_FILTER, "^a.txt", KEY_REVIEWER, ImmutableList.of(user2.email));
+
+    pushFactory
+        .create(db, admin.getIdent(), testRepo, "Add reviewers", FILENAME, cfg.toText())
+        .to(RefNames.REFS_CONFIG)
+        .assertOkStatus();
+
+    testRepo.reset(oldHead);
+    String changeId = createChange().getChangeId();
+
+    Collection<AccountInfo> reviewers;
+    // Wait for 100 ms until the create patch set event
+    // is processed by the reviewers plugin
+    long wait = 0;
+    do {
+      reviewers = gApi.changes().id(changeId).get().reviewers.get(REVIEWER);
+      if (reviewers == null) {
+        Thread.sleep(10);
+        wait += 10;
+        if (wait > 100) {
+          assert_().fail("Timeout of 100 ms exceeded");
+        }
+      }
+    } while (reviewers == null);
+
+    assertThat(reviewers.stream().map(a -> a._accountId).collect(toSet()))
+        .containsExactlyElementsIn(ImmutableSet.of(admin.id.get(), user.id.get(), user2.id.get()));
+  }
+
+  @Test
   public void doNotAddReviewersFromNonMatchingFilters() throws Exception {
     RevCommit oldHead = getRemoteHead();
 
diff --git a/tools/bzl/genrule2.bzl b/tools/bzl/genrule2.bzl
new file mode 100644
index 0000000..61c4e18
--- /dev/null
+++ b/tools/bzl/genrule2.bzl
@@ -0,0 +1,3 @@
+load("@com_googlesource_gerrit_bazlets//tools:genrule2.bzl", _genrule2 = "genrule2")
+
+genrule2 = _genrule2
diff --git a/tools/bzl/js.bzl b/tools/bzl/js.bzl
new file mode 100644
index 0000000..0f9e367
--- /dev/null
+++ b/tools/bzl/js.bzl
@@ -0,0 +1,3 @@
+load("@com_googlesource_gerrit_bazlets//tools:js.bzl", _polygerrit_plugin = "polygerrit_plugin")
+
+polygerrit_plugin = _polygerrit_plugin