initial revision
Change-Id: I8a62cfe06069af8d8946f579239f84b3484d4788
diff --git a/.buckconfig b/.buckconfig
new file mode 100644
index 0000000..4077128
--- /dev/null
+++ b/.buckconfig
@@ -0,0 +1,11 @@
+[alias]
+ plugin = //:reviewers
+
+[buildfile]
+ includes = //lib/build.defs
+
+[java]
+ src_roots = java, resources
+
+[project]
+ ignore = .git
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..85b5abe
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,10 @@
+/target
+/.classpath
+/.project
+/.settings
+/.buckconfig.local
+/.buckd
+/buck-cache
+/buck-out
+/local.properties
+*.pyc
diff --git a/BUCK b/BUCK
new file mode 100644
index 0000000..572baad
--- /dev/null
+++ b/BUCK
@@ -0,0 +1,18 @@
+API_VERSION = '2.9-SNAPSHOT'
+REPO = MAVEN_LOCAL
+
+gerrit_plugin(
+ name = 'reviewers',
+ srcs = glob(['src/main/java/**/*.java']),
+ resources = glob(['src/main/resources/**/*']),
+ manifest_entries = [
+ 'Gerrit-PluginName: reviewers',
+ 'Gerrit-Module: com.googlesource.gerrit.plugins.reviewers.Module',
+ ]
+)
+
+maven_jar(
+ name = 'plugin-lib',
+ id = 'com.google.gerrit:gerrit-plugin-api:' + API_VERSION,
+ repository = REPO,
+)
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..11069ed
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+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.
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+1.0
diff --git a/lib/build.defs b/lib/build.defs
new file mode 100644
index 0000000..08418d0
--- /dev/null
+++ b/lib/build.defs
@@ -0,0 +1,76 @@
+include_defs('//lib/maven.defs')
+
+def java_library2(
+ name,
+ srcs = [],
+ resources = [],
+ deps = [],
+ compile_deps = [],
+ visibility = []):
+ c = name + '__compile'
+ t = name + '__link'
+ j = 'lib__%s__output/%s.jar' % (c, c)
+ o = 'lib__%s__output/%s.jar' % (name, name)
+ java_library(
+ name = c,
+ srcs = srcs,
+ resources = resources,
+ deps = deps + compile_deps,
+ visibility = visibility,
+ )
+ genrule(
+ name = t,
+ cmd = 'mkdir -p $(dirname $OUT);ln -s $SRCS $OUT',
+ srcs = [genfile(j)],
+ deps = [':' + c],
+ out = o,
+ )
+ prebuilt_jar(
+ name = name,
+ binary_jar = genfile(o),
+ deps = deps + [':' + t],
+ visibility = visibility,
+ )
+
+def gerrit_plugin(
+ name,
+ deps = [],
+ srcs = [],
+ resources = [],
+ manifest_file = None,
+ manifest_entries = [],
+ type = 'plugin',
+ visibility = ['PUBLIC']):
+ mf_cmd = 'v=$(cat VERSION);'
+ if manifest_file:
+ mf_src = [manifest_file]
+ mf_cmd += 'sed "s:@VERSION@:$v:g" $SRCS >$OUT'
+ else:
+ mf_src = []
+ mf_cmd += 'echo "Manifest-Version: 1.0" >$OUT;'
+ mf_cmd += 'echo "Gerrit-ApiType: %s" >>$OUT;' % type
+ mf_cmd += 'echo "Implementation-Version: $v" >>$OUT'
+ for line in manifest_entries:
+ mf_cmd += ';echo "%s" >> $OUT' % line
+ genrule(
+ name = name + '__manifest',
+ cmd = mf_cmd,
+ srcs = mf_src,
+ out = 'MANIFEST.MF',
+ )
+ java_library2(
+ name = name + '__plugin',
+ srcs = srcs,
+ resources = resources,
+ deps = deps,
+ compile_deps = ['//:%s-lib' % type],
+ )
+ java_binary(
+ name = name,
+ manifest_file = genfile('MANIFEST.MF'),
+ deps = [
+ ':%s__plugin' % name,
+ ':%s__manifest' % name,
+ ],
+ visibility = visibility,
+ )
diff --git a/lib/maven.defs b/lib/maven.defs
new file mode 100644
index 0000000..4ec5030
--- /dev/null
+++ b/lib/maven.defs
@@ -0,0 +1,61 @@
+GERRIT = 'GERRIT:'
+MAVEN_LOCAL = 'MAVEN_LOCAL:'
+
+def maven_jar(
+ name,
+ id,
+ deps = [],
+ sha1 = '',
+ bin_sha1 = '',
+ src_sha1 = '',
+ repository = GERRIT,
+ attach_source = True,
+ visibility = ['PUBLIC']):
+ from os import path
+
+ parts = id.split(':')
+ if len(parts) != 3:
+ raise NameError('expected id="groupId:artifactId:version"')
+ group, artifact, version = parts
+
+ file_version = version
+
+ jar = path.join(name, artifact.lower() + '-' + file_version)
+ url = '/'.join([
+ repository,
+ group.replace('.', '/'), artifact, version,
+ artifact + '-' + file_version])
+
+ binjar = jar + '.jar'
+ binurl = url + '.jar'
+
+ srcjar = jar + '-src.jar'
+ srcurl = url + '-sources.jar'
+
+ cmd = ['$(exe //tools:download_file)', '-o', '$OUT', '-u', binurl]
+ if sha1:
+ cmd.extend(['-v', sha1])
+ elif bin_sha1:
+ cmd.extend(['-v', bin_sha1])
+
+ genrule(
+ name = name + '__download_bin',
+ cmd = ' '.join(cmd),
+ deps = ['//tools:download_file'],
+ out = binjar,
+ )
+
+ srcjar = None
+ genrule(
+ name = name + '__download_src',
+ cmd = ':>$OUT',
+ out = '__' + name + '__no_src',
+ )
+
+ prebuilt_jar(
+ name = name,
+ deps = deps + [':' + name + '__download_bin'],
+ binary_jar = genfile(binjar),
+ source_jar = genfile(srcjar) if srcjar else None,
+ visibility = visibility,
+ )
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..bfbfeea
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2013 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <groupId>com.googlesource.gerrit.plugins.reviewers</groupId>
+ <artifactId>reviewers</artifactId>
+ <packaging>jar</packaging>
+ <version>2.9-SNAPSHOT</version>
+ <name>reviewers</name>
+
+ <properties>
+ <Gerrit-ApiType>plugin</Gerrit-ApiType>
+ <Gerrit-ApiVersion>${project.version}</Gerrit-ApiVersion>
+ </properties>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <version>2.4</version>
+ <configuration>
+ <archive>
+ <manifestEntries>
+ <Gerrit-PluginName>reviewers</Gerrit-PluginName>
+ <Gerrit-Module>com.googlesource.gerrit.plugins.reviewers.Module</Gerrit-Module>
+ <Implementation-Vendor>Gerrit Code Review</Implementation-Vendor>
+ <Implementation-URL>http://code.google.com/p/gerrit/</Implementation-URL>
+
+ <Implementation-Title>${Gerrit-ApiType} ${project.artifactId}</Implementation-Title>
+ <Implementation-Version>${project.version}</Implementation-Version>
+
+ <Gerrit-ApiType>${Gerrit-ApiType}</Gerrit-ApiType>
+ <Gerrit-ApiVersion>${Gerrit-ApiVersion}</Gerrit-ApiVersion>
+ </manifestEntries>
+ </archive>
+ </configuration>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <version>2.3.2</version>
+ <configuration>
+ <source>1.6</source>
+ <target>1.6</target>
+ <encoding>UTF-8</encoding>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+ <dependencies>
+ <dependency>
+ <groupId>com.google.gerrit</groupId>
+ <artifactId>gerrit-${Gerrit-ApiType}-api</artifactId>
+ <version>${Gerrit-ApiVersion}</version>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+ <repositories>
+ <repository>
+ <id>gerrit-api-repository</id>
+ <url>https://gerrit-api.commondatastorage.googleapis.com/snapshot/</url>
+ </repository>
+ </repositories>
+ <description>Add default reviewers with different strategies</description>
+</project>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/reviewers/ChangeEventListener.java b/src/main/java/com/googlesource/gerrit/plugins/reviewers/ChangeEventListener.java
new file mode 100644
index 0000000..6345fd3
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/reviewers/ChangeEventListener.java
@@ -0,0 +1,235 @@
+// Copyright (C) 2013 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.plugins.reviewers;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Set;
+
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.ChangeListener;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountByEmailCache;
+import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.account.GroupMembers;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.events.ChangeEvent;
+import com.google.gerrit.server.events.PatchSetCreatedEvent;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.group.GroupsCollection;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
+
+class ChangeEventListener implements ChangeListener {
+
+ private static final Logger log = LoggerFactory
+ .getLogger(ChangeEventListener.class);
+
+ private final AccountResolver accountResolver;
+ private final AccountByEmailCache byEmailCache;
+ private final Provider<GroupsCollection> groupsCollection;
+ private final GroupMembers.Factory groupMembersFactory;
+ private final DefaultReviewers.Factory reviewersFactory;
+ private final GitRepositoryManager repoManager;
+ private final WorkQueue workQueue;
+ private final IdentifiedUser.GenericFactory identifiedUserFactory;
+ private final ThreadLocalRequestContext tl;
+ private final SchemaFactory<ReviewDb> schemaFactory;
+ private final PluginConfigFactory cfg;
+ private final String pluginName;
+ private ReviewDb db;
+
+ @Inject
+ ChangeEventListener(
+ final AccountResolver accountResolver,
+ final AccountByEmailCache byEmailCache,
+ final Provider<GroupsCollection> groupsCollection,
+ final GroupMembers.Factory groupMembersFactory,
+ final DefaultReviewers.Factory reviewersFactory,
+ final GitRepositoryManager repoManager,
+ final WorkQueue workQueue,
+ final IdentifiedUser.GenericFactory identifiedUserFactory,
+ final ThreadLocalRequestContext tl,
+ final SchemaFactory<ReviewDb> schemaFactory,
+ final PluginConfigFactory cfg,
+ final @PluginName String pluginName) {
+ this.accountResolver = accountResolver;
+ this.byEmailCache = byEmailCache;
+ this.groupsCollection = groupsCollection;
+ this.groupMembersFactory = groupMembersFactory;
+ this.reviewersFactory = reviewersFactory;
+ this.repoManager = repoManager;
+ this.workQueue = workQueue;
+ this.identifiedUserFactory = identifiedUserFactory;
+ this.tl = tl;
+ this.schemaFactory = schemaFactory;
+ this.cfg = cfg;
+ this.pluginName = pluginName;
+ }
+
+ @Override
+ public void onChangeEvent(ChangeEvent event) {
+ if (!(event instanceof PatchSetCreatedEvent)) {
+ return;
+ }
+ PatchSetCreatedEvent e = (PatchSetCreatedEvent) event;
+ Project.NameKey projectName = new Project.NameKey(e.change.project);
+ Set<Account> reviewers;
+ try {
+ reviewers = reviewers(cfg
+ .getFromProjectConfigWithInheritance(projectName, pluginName)
+ .getStringList("reviewer"), projectName, e.uploader.email);
+ } catch (NoSuchProjectException x) {
+ log.error(x.getMessage(), x);
+ return;
+ }
+
+ if (reviewers.isEmpty()) {
+ return;
+ }
+
+ Repository git;
+ try {
+ git = repoManager.openRepository(projectName);
+ } catch (RepositoryNotFoundException x) {
+ log.error(x.getMessage(), x);
+ return;
+ } catch (IOException x) {
+ log.error(x.getMessage(), x);
+ return;
+ }
+
+ final ReviewDb reviewDb;
+ final RevWalk rw = new RevWalk(git);
+
+ try {
+ reviewDb = schemaFactory.open();
+ try {
+ Change.Id changeId = new Change.Id(Integer.parseInt(e.change.number));
+ PatchSet.Id psId = new PatchSet.Id(changeId,
+ Integer.parseInt(e.patchSet.number));
+ PatchSet ps = reviewDb.patchSets().get(psId);
+ if (ps == null) {
+ log.warn("Patch set " + psId.get() + " not found.");
+ return;
+ }
+
+ final Change change = reviewDb.changes().get(psId.getParentKey());
+ if (change == null) {
+ log.warn("Change " + changeId.get() + " not found.");
+ return;
+ }
+
+ final Runnable task = reviewersFactory.create(change, reviewers);
+
+ workQueue.getDefaultQueue().submit(new Runnable() {
+ public void run() {
+ RequestContext old = tl.setContext(new RequestContext() {
+
+ @Override
+ public CurrentUser getCurrentUser() {
+ return identifiedUserFactory.create(change.getOwner());
+ }
+
+ @Override
+ public Provider<ReviewDb> getReviewDbProvider() {
+ return new Provider<ReviewDb>() {
+ @Override
+ public ReviewDb get() {
+ if (db == null) {
+ try {
+ db = schemaFactory.open();
+ } catch (OrmException e) {
+ throw new ProvisionException("Cannot open ReviewDb", e);
+ }
+ }
+ return db;
+ }
+ };
+ }
+ });
+ try {
+ task.run();
+ } finally {
+ tl.setContext(old);
+ if (db != null) {
+ db.close();
+ db = null;
+ }
+ }
+ }
+ });
+ } catch (OrmException x) {
+ log.error(x.getMessage(), x);
+ } finally {
+ reviewDb.close();
+ }
+ } catch (OrmException x) {
+ log.error(x.getMessage(), x);
+ } finally {
+ rw.release();
+ git.close();
+ }
+ }
+
+ private Set<Account> reviewers(String[] list, Project.NameKey p,
+ String uploaderEMail) {
+ if (list == null || list.length == 0) {
+ return Collections.emptySet();
+ }
+ Set<Account> reviewers = Sets.newHashSetWithExpectedSize(list.length);
+ GroupMembers groupMembers = null;
+ for (String r : list) {
+ try {
+ Account account = accountResolver.find(r);
+ if (account != null) {
+ reviewers.add(account);
+ continue;
+ }
+ if (groupMembers == null) {
+ groupMembers =
+ groupMembersFactory.create(identifiedUserFactory.create(Iterables
+ .getOnlyElement(byEmailCache.get(uploaderEMail))));
+ }
+ reviewers.addAll(groupMembers.listAccounts(groupsCollection.get()
+ .parseInternal(r).getGroupUUID(), p));
+ } catch (Exception e) {
+ log.warn("Cannot resolve reviewer: " + r, e);
+ }
+ }
+ return reviewers;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/reviewers/DefaultReviewers.java b/src/main/java/com/googlesource/gerrit/plugins/reviewers/DefaultReviewers.java
new file mode 100644
index 0000000..1968a62
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/reviewers/DefaultReviewers.java
@@ -0,0 +1,87 @@
+// Copyright (C) 2013 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.plugins.reviewers;
+
+import java.util.Set;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.PostReviewers;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.assistedinject.Assisted;
+
+public class DefaultReviewers implements Runnable {
+ private static final Logger log = LoggerFactory
+ .getLogger(DefaultReviewers.class);
+
+ private final Change change;
+ private final Set<Account> reviewers;
+ private final Provider<PostReviewers> reviewersProvider;
+ private final IdentifiedUser.GenericFactory identifiedUserFactory;
+ private final ChangeControl.GenericFactory changeControlFactory;
+
+ public interface Factory {
+ DefaultReviewers create(Change change,
+ Set<Account> reviewers);
+ }
+
+ @Inject
+ public DefaultReviewers(
+ ChangeControl.GenericFactory changeControlFactory,
+ Provider<PostReviewers> reviewersProvider,
+ IdentifiedUser.GenericFactory identifiedUserFactory,
+ @Assisted Change change, @Assisted Set<Account> reviewers) {
+ this.change = change;
+ this.reviewers = reviewers;
+ this.reviewersProvider = reviewersProvider;
+ this.identifiedUserFactory = identifiedUserFactory;
+ this.changeControlFactory = changeControlFactory;
+ }
+
+ @Override
+ public void run() {
+ addReviewers(reviewers, change);
+ }
+
+ /**
+ * Append the reviewers to change#{@link Change}
+ *
+ * @param topReviewers Set of reviewers proposed
+ * @param change {@link Change} to add the reviewers to
+ */
+ private void addReviewers(Set<Account> reviewers, Change change) {
+ try {
+ ChangeControl changeControl =
+ changeControlFactory.controlFor(change,
+ identifiedUserFactory.create(change.getOwner()));
+ ChangeResource changeResource = new ChangeResource(changeControl);
+ PostReviewers post = reviewersProvider.get();
+ for (Account account : reviewers) {
+ PostReviewers.Input input = new PostReviewers.Input();
+ input.reviewer = account.getId().toString();
+ post.apply(changeResource, input);
+ }
+ } catch (Exception ex) {
+ log.error("Couldn't add reviewers to the change", ex);
+ }
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/reviewers/Module.java b/src/main/java/com/googlesource/gerrit/plugins/reviewers/Module.java
new file mode 100644
index 0000000..3b4b803
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/reviewers/Module.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2013 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.plugins.reviewers;
+
+import com.google.gerrit.common.ChangeListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.config.FactoryModule;
+
+public class Module extends FactoryModule {
+ @Override
+ protected void configure() {
+ DynamicSet.bind(binder(), ChangeListener.class).to(
+ ChangeEventListener.class);
+ factory(DefaultReviewers.Factory.class);
+ }
+}
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
new file mode 100644
index 0000000..96d54b5
--- /dev/null
+++ b/src/main/resources/Documentation/about.md
@@ -0,0 +1,9 @@
+A plugin that allows adding default reviewers to a change.
+
+The configuration for adding reviewers to submitted changes can be
+[configured per project](config.html).
+
+SEE ALSO
+--------
+
+* [reviewers-by-blame plugin](https://gerrit-review.googlesource.com/#/admin/projects/plugins/reviewers-by-blame)
diff --git a/src/main/resources/Documentation/build.md b/src/main/resources/Documentation/build.md
new file mode 100644
index 0000000..997d702
--- /dev/null
+++ b/src/main/resources/Documentation/build.md
@@ -0,0 +1,43 @@
+Build
+=====
+
+This plugin can be built with Buck or Maven.
+
+Buck
+----
+
+Two modes of operations of building with Buck are supported: build in Gerrit
+tree and outside of the Gerrit tree.
+
+To build in Gerrit tree clone the plugin under plugins directory and run
+
+```
+ $>buck build plugins/reviewers:reviewers
+```
+
+from the Gerrit base directory. To build the plugin standalone (outside of
+the Gerrit tree), run
+
+```
+ $>buck build plugin
+```
+
+Maven
+-----
+
+To build with Maven, run
+
+```
+mvn clean package
+```
+
+Prerequisites
+-------------
+
+Only Gerrit in tree mode doesn't need gerrit-plugin-api dependency. For
+other build modes gerrit-plugin-api must be fetched from remote or local
+Maven repository.
+
+How to obtain the Gerrit Plugin API is described in the [Gerrit
+documentation](../../../Documentation/dev-buck.html#_extension_and_plugin_api_jar_files).
+
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
new file mode 100644
index 0000000..152f88d
--- /dev/null
+++ b/src/main/resources/Documentation/config.md
@@ -0,0 +1,20 @@
+Configuration
+=============
+
+The configuration of the @PLUGIN@ plugin is done on project level in
+the `project.config` file of the project. Missing values are inherited
+from the parent projects. This means a global default configuration can
+be done in the `project.config` file of the `All-Projects` root project.
+Other projects can then override the configuration in their own
+`project.config` file.
+
+```
+ [plugin "reviewers"]
+ reviewer = john.doe@example.com
+ reviewer = jane.doe@example.com
+ reviewer = QAGroup
+```
+
+plugin.reviewers.reviewer
+: An account (email or full user name) or a group name. Multiple
+ `reviewer` occurrences are allowed.
diff --git a/tools/BUCK b/tools/BUCK
new file mode 100644
index 0000000..1b1175c
--- /dev/null
+++ b/tools/BUCK
@@ -0,0 +1,5 @@
+python_binary(
+ name = 'download_file',
+ main = 'download_file.py',
+ visibility = ['PUBLIC'],
+)
diff --git a/tools/download_file.py b/tools/download_file.py
new file mode 100755
index 0000000..8d76a40
--- /dev/null
+++ b/tools/download_file.py
@@ -0,0 +1,208 @@
+#!/usr/bin/python
+# Copyright (C) 2013 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.
+
+from __future__ import print_function
+
+from hashlib import sha1
+from optparse import OptionParser
+from os import link, makedirs, path, remove
+import shutil
+from subprocess import check_call, CalledProcessError
+from sys import stderr
+from zipfile import ZipFile, BadZipfile, LargeZipFile
+
+REPO_ROOTS = {
+ 'GERRIT': 'http://gerrit-maven.storage.googleapis.com',
+ 'ECLIPSE': 'https://repo.eclipse.org/content/groups/releases',
+ 'MAVEN_CENTRAL': 'http://repo1.maven.org/maven2',
+ 'MAVEN_LOCAL': 'file://' + path.expanduser('~/.m2/repository'),
+}
+
+GERRIT_HOME = path.expanduser('~/.gerritcodereview')
+CACHE_DIR = path.join(GERRIT_HOME, 'buck-cache')
+LOCAL_PROPERTIES = 'local.properties'
+
+
+def hashfile(p):
+ d = sha1()
+ with open(p, 'rb') as f:
+ while True:
+ b = f.read(8192)
+ if not b:
+ break
+ d.update(b)
+ return d.hexdigest()
+
+def safe_mkdirs(d):
+ if path.isdir(d):
+ return
+ try:
+ makedirs(d)
+ except OSError as err:
+ if not path.isdir(d):
+ raise err
+
+def download_properties(root_dir):
+ """ Get the download properties.
+
+ First tries to find the properties file in the given root directory,
+ and if not found there, tries in the Gerrit settings folder in the
+ user's home directory.
+
+ Returns a set of download properties, which may be empty.
+
+ """
+ p = {}
+ local_prop = path.join(root_dir, LOCAL_PROPERTIES)
+ if not path.isfile(local_prop):
+ local_prop = path.join(GERRIT_HOME, LOCAL_PROPERTIES)
+ if path.isfile(local_prop):
+ try:
+ with open(local_prop) as fd:
+ for line in fd:
+ if line.startswith('download.'):
+ d = [e.strip() for e in line.split('=', 1)]
+ name, url = d[0], d[1]
+ p[name[len('download.'):]] = url
+ except OSError:
+ pass
+ return p
+
+def cache_entry(args):
+ if args.v:
+ h = args.v
+ else:
+ h = sha1(args.u).hexdigest()
+ name = '%s-%s' % (path.basename(args.o), h)
+ return path.join(CACHE_DIR, name)
+
+def resolve_url(url, redirects):
+ s = url.find(':')
+ if s < 0:
+ return url
+ scheme, rest = url[:s], url[s+1:]
+ if scheme not in REPO_ROOTS:
+ return url
+ if scheme in redirects:
+ root = redirects[scheme]
+ else:
+ root = REPO_ROOTS[scheme]
+ root = root.rstrip('/')
+ rest = rest.lstrip('/')
+ return '/'.join([root, rest])
+
+opts = OptionParser()
+opts.add_option('-o', help='local output file')
+opts.add_option('-u', help='URL to download')
+opts.add_option('-v', help='expected content SHA-1')
+opts.add_option('-x', action='append', help='file to delete from ZIP')
+opts.add_option('--exclude_java_sources', action='store_true')
+opts.add_option('--unsign', action='store_true')
+args, _ = opts.parse_args()
+
+root_dir = args.o
+while root_dir:
+ root_dir, n = path.split(root_dir)
+ if n == 'buck-out':
+ break
+
+redirects = download_properties(root_dir)
+cache_ent = cache_entry(args)
+src_url = resolve_url(args.u, redirects)
+
+if not path.exists(cache_ent):
+ try:
+ safe_mkdirs(path.dirname(cache_ent))
+ except OSError as err:
+ print('error creating directory %s: %s' %
+ (path.dirname(cache_ent), err), file=stderr)
+ exit(1)
+
+ print('Download %s' % src_url, file=stderr)
+ try:
+ check_call(['curl', '--proxy-anyauth', '-sfo', cache_ent, src_url])
+ except OSError as err:
+ print('could not invoke curl: %s\nis curl installed?' % err, file=stderr)
+ exit(1)
+ except CalledProcessError as err:
+ print('error using curl: %s' % err, file=stderr)
+ exit(1)
+
+if args.v:
+ have = hashfile(cache_ent)
+ if args.v != have:
+ print((
+ '%s:\n' +
+ 'expected %s\n' +
+ 'received %s\n') % (src_url, args.v, have), file=stderr)
+ try:
+ remove(cache_ent)
+ except OSError as err:
+ if path.exists(cache_ent):
+ print('error removing %s: %s' % (cache_ent, err), file=stderr)
+ exit(1)
+
+exclude = []
+if args.x:
+ exclude += args.x
+if args.exclude_java_sources:
+ try:
+ zf = ZipFile(cache_ent, 'r')
+ try:
+ for n in zf.namelist():
+ if n.endswith('.java'):
+ exclude.append(n)
+ finally:
+ zf.close()
+ except (BadZipfile, LargeZipFile) as err:
+ print('error opening %s: %s' % (cache_ent, err), file=stderr)
+ exit(1)
+
+if args.unsign:
+ try:
+ zf = ZipFile(cache_ent, 'r')
+ try:
+ for n in zf.namelist():
+ if (n.endswith('.RSA')
+ or n.endswith('.SF')
+ or n.endswith('.LIST')):
+ exclude.append(n)
+ finally:
+ zf.close()
+ except (BadZipfile, LargeZipFile) as err:
+ print('error opening %s: %s' % (cache_ent, err), file=stderr)
+ exit(1)
+
+safe_mkdirs(path.dirname(args.o))
+if exclude:
+ try:
+ shutil.copyfile(cache_ent, args.o)
+ except (shutil.Error, IOError) as err:
+ print('error copying to %s: %s' % (args.o, err), file=stderr)
+ exit(1)
+ try:
+ check_call(['zip', '-d', args.o] + exclude)
+ except CalledProcessError as err:
+ print('error removing files from zip: %s' % err, file=stderr)
+ exit(1)
+else:
+ try:
+ link(cache_ent, args.o)
+ except OSError as err:
+ try:
+ shutil.copyfile(cache_ent, args.o)
+ except (shutil.Error, IOError) as err:
+ print('error copying to %s: %s' % (args.o, err), file=stderr)
+ exit(1)