CopyrightValidator to scan uploaded files.
Copyright plugin part 3 of 3: CopyrightValidator scans each new
revision looking for copyright author/owner or license declarations.
Whenever it finds a copyright declaration that might not be allowed, it
requires special review by voting down a configured label and by adding
a configured reviewer.
e.g. If it finds a third-party license or owner outside of the projects
where third-party code is allowed, it requires special review.
If it finds a forbidden or unknown license anywhere, it requires
special review.
If it finds a first-party license--including first-party licensed code
from a third-party, it notes the findings but requires no special
review.
etc.
Change-Id: Ia272fd02b3f38c5649dfc9fd8cbe3521cdfa4efc
diff --git a/src/main/java/com/googlesource/gerrit/plugins/copyright/CheckConfig.java b/src/main/java/com/googlesource/gerrit/plugins/copyright/CheckConfig.java
index e6654c7..d9dd534 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/copyright/CheckConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/copyright/CheckConfig.java
@@ -368,7 +368,7 @@
sb.setLength(0);
for (int i = 255; i >= 0; i--) {
- sb.append(String.format("%2x%2x", 255-i, i));
+ sb.append(String.format("%2x%2x", 255 - i, i));
}
sb.append('\n');
String alnum1k = sb.toString();
@@ -382,7 +382,7 @@
sb.append('\n');
String alpha1k = sb.toString();
- for (int i = 0; i < 16; i++) { // 16k + 48k = 64k
+ for (int i = 0; i < 16; i++) { // 16k + 48k = 64k
sb.append(space1k);
}
for (int i = 0; i < 48; i++) {
@@ -507,16 +507,13 @@
elapsedMicros / 1000000);
System.err.println(
"might cause problems on your server. Please compare wtih the current");
- System.err.println(
- "configuration, and if this configuration is significantly slower,");
+ System.err.println("configuration, and if this configuration is significantly slower,");
System.err.println("consider changing whatever pattern might cause the problem.");
} else { // between 1s and 2s might be needed but could at least try to do better
System.err.printf(
"\nAt %dms, the scan took just longer than 1 second. This\n", elapsedMicros / 1000);
- System.err.println(
- "configuration might work okay, but takes longer than ideal. Please");
- System.err.println(
- "investigate whether an added pattern is more costly than needed.");
+ System.err.println("configuration might work okay, but takes longer than ideal. Please");
+ System.err.println("investigate whether an added pattern is more costly than needed.");
}
} else if (elapsedMicros > 1000) {
System.err.printf("\nScanned the test load in %dms.\n", elapsedMicros / 1000);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/copyright/CopyrightConfig.java b/src/main/java/com/googlesource/gerrit/plugins/copyright/CopyrightConfig.java
index ac76aa4..11b3a3a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/copyright/CopyrightConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/copyright/CopyrightConfig.java
@@ -333,6 +333,7 @@
} finally {
if (trialConfig != null
&& trialConfig.scannerConfig != null
+ && !trialConfig.scannerConfig.messages.isEmpty()
&& !trialConfig.scannerConfig.hasErrors()) {
try {
ReviewResult result =
diff --git a/src/main/java/com/googlesource/gerrit/plugins/copyright/CopyrightReviewApi.java b/src/main/java/com/googlesource/gerrit/plugins/copyright/CopyrightReviewApi.java
index 1463db3..15a8364 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/copyright/CopyrightReviewApi.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/copyright/CopyrightReviewApi.java
@@ -196,9 +196,7 @@
oldConfig == null
|| oldConfig.scanner == null
|| !oldConfig.scanner.equals(newConfig.scanner));
- Preconditions.checkArgument(
- findings.size() != 1
- || !findings.get(0).isValid());
+ Preconditions.checkArgument(findings.size() != 1 || !findings.get(0).isValid());
Preconditions.checkArgument(maxElapsedSeconds > 0);
long startNanos = System.nanoTime();
@@ -267,8 +265,8 @@
String pluginName, ImmutableList<CommitMessageFinding> findings, long maxElapsedSeconds) {
Preconditions.checkArgument(
findings.size() != 1
- || !findings.get(0).isValid()
- || findings.get(0).elapsedMicros > maxElapsedSeconds * 1000000);
+ || !findings.get(0).isValid()
+ || findings.get(0).elapsedMicros > maxElapsedSeconds * 1000000);
StringBuilder sb = new StringBuilder();
sb.append(getCommitMessageMessage(pluginName, findings, maxElapsedSeconds));
sb.append("\n\n");
diff --git a/src/main/java/com/googlesource/gerrit/plugins/copyright/CopyrightValidator.java b/src/main/java/com/googlesource/gerrit/plugins/copyright/CopyrightValidator.java
new file mode 100644
index 0000000..cda88ce
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/copyright/CopyrightValidator.java
@@ -0,0 +1,298 @@
+// 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.
+
+package com.googlesource.gerrit.plugins.copyright;
+
+import static com.googlesource.gerrit.plugins.copyright.CopyrightReviewApi.ALWAYS_REVIEW;
+import static com.googlesource.gerrit.plugins.copyright.ScannerConfig.KEY_ENABLE;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.api.changes.AddReviewerResult;
+import com.google.gerrit.extensions.api.changes.ReviewResult;
+import com.google.gerrit.extensions.events.RevisionCreatedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.metrics.Counter0;
+import com.google.gerrit.metrics.Counter1;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Description.Units;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.metrics.Timer0;
+import com.google.gerrit.metrics.Timer1;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.AbstractModule;
+import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.copyright.lib.CopyrightScanner.Match;
+import com.googlesource.gerrit.plugins.copyright.lib.IndexedLineReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import javax.inject.Inject;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.ObjectStream;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.filter.TreeFilter;
+
+/** Listener to enforce review of copyright declarations and licenses. */
+public class CopyrightValidator implements RevisionCreatedListener {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private final String pluginName; // name of plugin as installed in gerrit
+ private final Metrics metrics;
+ private final GitRepositoryManager repoManager;
+ private final PluginConfigFactory pluginConfigFactory;
+ private final CopyrightReviewApi reviewApi;
+
+ private CopyrightConfig copyrightConfig;
+ private ScannerConfig scannerConfig; // configuration state for plugin
+
+ static AbstractModule module() {
+ return new AbstractModule() {
+ @Override
+ protected void configure() {
+ DynamicSet.bind(binder(), RevisionCreatedListener.class).to(CopyrightValidator.class);
+ }
+ };
+ }
+
+ @Singleton
+ private static class Metrics {
+ final Counter0 scanCount;
+ final Timer0 scanRevisionTimer;
+ final Timer0 scanFileTimer;
+ final Counter1 scanCountByProject;
+ final Timer1 scanRevisionTimerByProject;
+ final Timer1 scanFileTimerByProject;
+ final Counter1 scanCountByBranch;
+ final Timer1 scanRevisionTimerByBranch;
+ final Timer1 scanFileTimerByBranch;
+
+ @Inject
+ Metrics(MetricMaker metricMaker) {
+ Field<String> project = Field.ofString("project", "project name");
+ Field<String> branch = Field.ofString("branch", "branch name");
+ scanCount =
+ metricMaker.newCounter(
+ "plugin/copyright/scan_count",
+ new Description("Total number of copyright scans").setRate().setUnit("scans"));
+ scanRevisionTimer =
+ metricMaker.newTimer(
+ "plugin/copyright/scan_revision_latency",
+ new Description("Time spent scanning entire revisions")
+ .setCumulative()
+ .setUnit(Units.MILLISECONDS));
+ scanFileTimer =
+ metricMaker.newTimer(
+ "plugin/copyright/scan_file_latency",
+ new Description("Time spent scanning each file")
+ .setCumulative()
+ .setUnit(Units.MICROSECONDS));
+ scanCountByProject =
+ metricMaker.newCounter(
+ "plugin/copyright/scan_count_by_project",
+ new Description("Total number of copyright scans").setRate().setUnit("scans"),
+ project);
+ scanRevisionTimerByProject =
+ metricMaker.newTimer(
+ "plugin/copyright/scan_revision_latency_by_project",
+ new Description("Time spent scanning entire revisions")
+ .setCumulative()
+ .setUnit(Units.MILLISECONDS),
+ project);
+ scanFileTimerByProject =
+ metricMaker.newTimer(
+ "plugin/copyright/scan_file_latency_by_project",
+ new Description("Time spent scanning each file")
+ .setCumulative()
+ .setUnit(Units.MICROSECONDS),
+ project);
+ scanCountByBranch =
+ metricMaker.newCounter(
+ "plugin/copyright/scan_count_by_branch",
+ new Description("Total number of copyright scans").setRate().setUnit("scans"),
+ branch);
+ scanRevisionTimerByBranch =
+ metricMaker.newTimer(
+ "plugin/copyright/scan_revision_latency_by_branch",
+ new Description("Time spent scanning entire revisions")
+ .setCumulative()
+ .setUnit(Units.MILLISECONDS),
+ branch);
+ scanFileTimerByBranch =
+ metricMaker.newTimer(
+ "plugin/copyright/scan_file_latency_by_branch",
+ new Description("Time spent scanning each file")
+ .setCumulative()
+ .setUnit(Units.MICROSECONDS),
+ branch);
+ }
+ }
+
+ @Inject
+ CopyrightValidator(
+ Metrics metrics,
+ @PluginName String pluginName,
+ GitRepositoryManager repoManager,
+ CopyrightConfig copyrightConfig,
+ @Nullable ScannerConfig scannerConfig,
+ PluginConfigFactory pluginConfigFactory,
+ CopyrightReviewApi reviewApi) {
+ this.metrics = metrics;
+ this.pluginName = pluginName;
+ this.repoManager = repoManager;
+ this.copyrightConfig = copyrightConfig;
+ this.scannerConfig = scannerConfig;
+ this.pluginConfigFactory = pluginConfigFactory;
+ this.reviewApi = reviewApi;
+ }
+
+ @Override
+ public void onRevisionCreated(RevisionCreatedListener.Event event) {
+ String project = event.getChange().project;
+ String branch = event.getChange().branch;
+
+ if (branch.startsWith(RefNames.REFS)) {
+ // do not scan refs
+ return;
+ }
+ PluginConfig gerritConfig = pluginConfigFactory.getFromGerritConfig(pluginName, true);
+ if (gerritConfig == null || !gerritConfig.getBoolean(KEY_ENABLE, false)) {
+ logger.atFine().log("copyright plugin not enbled");
+ return;
+ }
+ if (scannerConfig == null) {
+ // error already reported during configuration load
+ logger.atWarning().log(
+ "%s plugin enabled with no configuration -- not scanning revision %s",
+ pluginName, event.getChange().currentRevision);
+ return;
+ }
+ if (scannerConfig.scanner == null) {
+ if (scannerConfig.hasErrors()) {
+ // error already reported during configuration load
+ logger.atWarning().log(
+ "%s plugin enabled with errors in configuration -- not scanning revision %s",
+ pluginName, event.getChange().currentRevision);
+ } // else plugin not enabled
+ return;
+ }
+ // allow project override of All-Projects to enable or disable
+ if (!copyrightConfig.isProjectEnabled(scannerConfig, project)) {
+ return;
+ }
+ try {
+ scanRevision(project, branch, event);
+ } catch (IOException | RestApiException e) {
+ logger.atSevere().withCause(e).log(
+ "%s plugin cannot scan revision %s", pluginName, event.getChange().currentRevision);
+ return;
+ }
+ }
+
+ /**
+ * Scans a pushed revision reporting all findings on its review thread.
+ *
+ * @param project the project or repository to which the change was pushed
+ * @param branch the branch the change updates
+ * @param event describes the newly created revision triggering the scan
+ * @throws IOException if an error occurred reading the repository
+ * @throws RestApiException if an error occured reporting findings to the review thread
+ */
+ private void scanRevision(String project, String branch, RevisionCreatedListener.Event event)
+ throws IOException, RestApiException {
+ Map<String, ImmutableList<Match>> findings = new HashMap<>();
+ ArrayList<String> containedPaths = new ArrayList<>();
+ long scanStart = System.nanoTime();
+ metrics.scanCount.increment();
+ metrics.scanCountByProject.increment(project);
+ metrics.scanCountByBranch.increment(branch);
+
+ try (Repository repo = repoManager.openRepository(new Project.NameKey(project));
+ RevWalk revWalk = new RevWalk(repo);
+ TreeWalk tw = new TreeWalk(revWalk.getObjectReader())) {
+ RevCommit commit = repo.parseCommit(ObjectId.fromString(event.getRevision().commit.commit));
+ tw.setRecursive(true);
+ tw.setFilter(TreeFilter.ANY_DIFF);
+ tw.addTree(commit.getTree());
+ if (commit.getParentCount() > 0) {
+ for (RevCommit p : commit.getParents()) {
+ if (p.getTree() == null) {
+ revWalk.parseHeaders(p);
+ }
+ tw.addTree(p.getTree());
+ }
+ }
+ while (tw.next()) {
+ containedPaths.add(tw.getPathString());
+ String fullPath = project + "/" + tw.getPathString();
+ if (scannerConfig.isAlwaysReviewPath(fullPath)) {
+ findings.put(tw.getPathString(), ALWAYS_REVIEW);
+ continue;
+ }
+ if (!FileMode.EXECUTABLE_FILE.equals(tw.getFileMode())
+ && !FileMode.REGULAR_FILE.equals(tw.getFileMode())) {
+ continue;
+ }
+ long fileStart = System.nanoTime();
+ try (ObjectReader reader = tw.getObjectReader();
+ ObjectStream stream = reader.open(tw.getObjectId(0)).openStream()) {
+ IndexedLineReader lineReader = new IndexedLineReader(fullPath, -1, stream);
+ ImmutableList<Match> matches =
+ scannerConfig.scanner.findMatches(fullPath, -1, lineReader);
+ if (!matches.isEmpty()) {
+ findings.put(tw.getPathString(), matches);
+ }
+ long fileTime = (System.nanoTime() - fileStart) / 1000;
+ metrics.scanFileTimer.record(fileTime, TimeUnit.MICROSECONDS);
+ metrics.scanFileTimerByProject.record(project, fileTime, TimeUnit.MICROSECONDS);
+ metrics.scanFileTimerByBranch.record(branch, fileTime, TimeUnit.MICROSECONDS);
+ }
+ }
+ }
+ long scanTime = (System.nanoTime() - scanStart) / 1000000;
+ metrics.scanRevisionTimer.record(scanTime, TimeUnit.MILLISECONDS);
+ metrics.scanRevisionTimerByProject.record(project, scanTime, TimeUnit.MILLISECONDS);
+ metrics.scanRevisionTimerByBranch.record(branch, scanTime, TimeUnit.MILLISECONDS);
+
+ ReviewResult result = reviewApi.reportScanFindings(project, scannerConfig, event, findings);
+ if (result.error != null && !result.error.equals("")) {
+ logger.atSevere().log(
+ "%s plugin revision %s: error posting review: %s",
+ pluginName, event.getChange().currentRevision, result.error);
+ }
+ for (Map.Entry<String, AddReviewerResult> entry : result.reviewers.entrySet()) {
+ AddReviewerResult arr = entry.getValue();
+ if (arr.error != null && !arr.error.equals("")) {
+ logger.atSevere().log(
+ "%s plugin revision %s: error adding reviewer %s: %s",
+ pluginName, event.getChange().currentRevision, entry.getKey(), arr.error);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/copyright/Module.java b/src/main/java/com/googlesource/gerrit/plugins/copyright/Module.java
index a319bd2..af872c7 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/copyright/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/copyright/Module.java
@@ -21,6 +21,7 @@
@Override
protected void configure() {
install(CopyrightConfig.module());
+ install(CopyrightValidator.module());
}
@Provides
diff --git a/src/test/java/com/googlesource/gerrit/plugins/copyright/CopyrightValidatorIT.java b/src/test/java/com/googlesource/gerrit/plugins/copyright/CopyrightValidatorIT.java
new file mode 100644
index 0000000..681ed26
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/copyright/CopyrightValidatorIT.java
@@ -0,0 +1,486 @@
+// 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.
+
+package com.googlesource.gerrit.plugins.copyright;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.googlesource.gerrit.plugins.copyright.ScannerConfig.KEY_CC;
+import static com.googlesource.gerrit.plugins.copyright.ScannerConfig.KEY_FROM;
+import static com.googlesource.gerrit.plugins.copyright.ScannerConfig.KEY_REVIEWER;
+import static com.googlesource.gerrit.plugins.copyright.TestConfig.LOCAL_BRANCH;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.truth.Correspondence;
+import com.google.gerrit.acceptance.GerritConfig;
+import com.google.gerrit.acceptance.GerritConfigs;
+import com.google.gerrit.acceptance.GitUtil;
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.ServerInitiated;
+import com.google.gerrit.server.group.InternalGroup;
+import com.google.gerrit.server.group.db.GroupsUpdate;
+import com.google.gerrit.server.group.db.InternalGroupCreation;
+import com.google.gerrit.server.group.db.InternalGroupUpdate;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.util.Optional;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.junit.Before;
+import org.junit.Test;
+
+@TestPlugin(name = "copyright", sysModule = "com.googlesource.gerrit.plugins.copyright.Module")
+public class CopyrightValidatorIT extends LightweightPluginDaemonTest {
+ @Test
+ @GerritConfigs({
+ @GerritConfig(name = "plugin.copyright.enable", value = "true"),
+ @GerritConfig(name = "plugin.copyright.timeTestMax", value = "0")
+ })
+ public void testCopyrightValidator_pushNoLicense() throws Exception {
+ PushOneCommit.Result result =
+ pushFactory
+ .create(author.newIdent(), testRepo, "subject", "filename", "content")
+ .to("refs/for/master");
+
+ assertNoReviewerAdded(result);
+ assertThat(result.getChange().notes().getComments()).isEmpty();
+ }
+
+ @Test
+ @GerritConfigs({
+ @GerritConfig(name = "plugin.copyright.enable", value = "true"),
+ @GerritConfig(name = "plugin.copyright.timeTestMax", value = "0")
+ })
+ public void testCopyrightValidator_pushAlwaysReview() throws Exception {
+ PushOneCommit.Result result =
+ pushFactory
+ .create(author.newIdent(), testRepo, "subject", "PATENT", "content")
+ .to("refs/for/master");
+
+ assertReviewerAdded(result);
+ assertThat(result.getChange().notes().getComments().values())
+ .comparingElementsUsing(commentContains())
+ .containsExactly(unresolved("PATENT always requires"));
+ }
+
+ @Test
+ @GerritConfigs({
+ @GerritConfig(name = "plugin.copyright.enable", value = "true"),
+ @GerritConfig(name = "plugin.copyright.timeTestMax", value = "0")
+ })
+ public void testCopyrightValidator_pushFirstPartyOwner() throws Exception {
+ PushOneCommit.Result result =
+ pushFactory
+ .create(author.newIdent(), testRepo, "subject", "source.cpp", FIRST_PARTY_OWNER)
+ .to("refs/for/master");
+
+ assertNoReviewerAdded(result);
+ assertThat(result.getChange().notes().getComments().values())
+ .comparingElementsUsing(commentContains())
+ .containsExactly(resolved("First-party author or owner"));
+ }
+
+ @Test
+ @GerritConfigs({
+ @GerritConfig(name = "plugin.copyright.enable", value = "true"),
+ @GerritConfig(name = "plugin.copyright.timeTestMax", value = "0")
+ })
+ public void testCopyrightValidator_pushFirstPartyHeader() throws Exception {
+ PushOneCommit.Result result =
+ pushFactory
+ .create(author.newIdent(), testRepo, "subject", "source.cpp", FIRST_PARTY_HEADER)
+ .to("refs/for/master");
+
+ assertNoReviewerAdded(result);
+ assertThat(result.getChange().notes().getComments().values())
+ .comparingElementsUsing(commentContains())
+ .containsExactly(resolved("First-party license"));
+ }
+
+ @Test
+ @GerritConfigs({
+ @GerritConfig(name = "plugin.copyright.enable", value = "true"),
+ @GerritConfig(name = "plugin.copyright.timeTestMax", value = "0")
+ })
+ public void testCopyrightValidator_pushFirstPartyOwnerAndHeader() throws Exception {
+ PushOneCommit.Result result =
+ pushFactory
+ .create(
+ author.newIdent(),
+ testRepo,
+ "subject",
+ "source.cpp",
+ FIRST_PARTY_OWNER + FIRST_PARTY_HEADER)
+ .to("refs/for/master");
+
+ assertNoReviewerAdded(result);
+ assertThat(result.getChange().notes().getComments().values())
+ .comparingElementsUsing(commentContains())
+ .containsExactly(resolved("First-party license")); // owner folds into license
+ }
+
+ @Test
+ @GerritConfigs({
+ @GerritConfig(name = "plugin.copyright.enable", value = "true"),
+ @GerritConfig(name = "plugin.copyright.timeTestMax", value = "0")
+ })
+ public void testCopyrightValidator_pushFirstPartyHeaderAndOwner() throws Exception {
+ PushOneCommit.Result result =
+ pushFactory
+ .create(
+ author.newIdent(),
+ testRepo,
+ "subject",
+ "source.cpp",
+ FIRST_PARTY_HEADER + FIRST_PARTY_OWNER)
+ .to("refs/for/master");
+
+ assertNoReviewerAdded(result);
+ assertThat(result.getChange().notes().getComments().values())
+ .comparingElementsUsing(commentContains())
+ .containsExactly(resolved("First-party license")); // owner folds into license
+ }
+
+ @Test
+ @GerritConfigs({
+ @GerritConfig(name = "plugin.copyright.enable", value = "true"),
+ @GerritConfig(name = "plugin.copyright.timeTestMax", value = "0")
+ })
+ public void testCopyrightValidator_pushNotAContribHeader() throws Exception {
+ PushOneCommit.Result result =
+ pushFactory
+ .create(author.newIdent(), testRepo, "subject", "source.cpp", NOT_A_CONTRIB_HEADER)
+ .to("refs/for/master");
+
+ assertReviewerAdded(result);
+ assertThat(result.getChange().notes().getComments().values())
+ .comparingElementsUsing(commentContains())
+ .containsExactly(resolved("First-party license"), unresolved("Disapproved license"));
+ }
+
+ @Test
+ @GerritConfigs({
+ @GerritConfig(name = "plugin.copyright.enable", value = "true"),
+ @GerritConfig(name = "plugin.copyright.timeTestMax", value = "0")
+ })
+ public void testCopyrightValidator_pushThirdPartyAllowed() throws Exception {
+ PushOneCommit.Result result =
+ pushFactory
+ .create(author.newIdent(), testRepo, "subject", "LICENSE", THIRD_PARTY_MIT)
+ .to("refs/for/master");
+
+ assertNoReviewerAdded(result);
+ assertThat(result.getChange().notes().getComments().values())
+ .comparingElementsUsing(commentContains())
+ .containsExactly(resolved("Third-party license allowed"));
+ }
+
+ @Test
+ @GerritConfigs({
+ @GerritConfig(name = "plugin.copyright.enable", value = "true"),
+ @GerritConfig(name = "plugin.copyright.timeTestMax", value = "0")
+ })
+ public void testCopyrightValidator_pushThirdPartyNotAllowed() throws Exception {
+ PushOneCommit.Result result =
+ pushFactory
+ .create(author.newIdent(), testRepo, "subject", "LICENSE", THIRD_PARTY_MIT)
+ .to("refs/for/master");
+
+ assertReviewerAdded(result);
+ assertThat(result.getChange().notes().getComments().values())
+ .comparingElementsUsing(commentContains())
+ .containsExactly(unresolved("Third-party license disallowed"));
+ }
+
+ @Test
+ @GerritConfigs({
+ @GerritConfig(name = "plugin.copyright.enable", value = "true"),
+ @GerritConfig(name = "plugin.copyright.timeTestMax", value = "0")
+ })
+ public void testCopyrightValidator_pushThirdPartyOwnerAllowed() throws Exception {
+ PushOneCommit.Result result =
+ pushFactory
+ .create(author.newIdent(), testRepo, "subject", "COPYING", THIRD_PARTY_OWNER)
+ .to("refs/for/master");
+
+ assertNoReviewerAdded(result);
+ assertThat(result.getChange().notes().getComments().values())
+ .comparingElementsUsing(commentContains())
+ .containsExactly(resolved("Third-party author or owner allowed"));
+ }
+
+ @Test
+ @GerritConfigs({
+ @GerritConfig(name = "plugin.copyright.enable", value = "true"),
+ @GerritConfig(name = "plugin.copyright.timeTestMax", value = "0")
+ })
+ public void testCopyrightValidator_pushThirdPartyOwnerNotAllowed() throws Exception {
+ PushOneCommit.Result result =
+ pushFactory
+ .create(author.newIdent(), testRepo, "subject", "COPYING", THIRD_PARTY_OWNER)
+ .to("refs/for/master");
+
+ assertReviewerAdded(result);
+ assertThat(result.getChange().notes().getComments().values())
+ .comparingElementsUsing(commentContains())
+ .containsExactly(unresolved("Third-party author or owner disallowed"));
+ }
+
+ @Test
+ @GerritConfigs({
+ @GerritConfig(name = "plugin.copyright.enable", value = "true"),
+ @GerritConfig(name = "plugin.copyright.timeTestMax", value = "0")
+ })
+ public void testCopyrightValidator_pushFirstPartyLicenseThirdPartyOwner() throws Exception {
+ PushOneCommit.Result result =
+ pushFactory
+ .create(
+ author.newIdent(),
+ testRepo,
+ "subject",
+ "COPYING",
+ FIRST_PARTY_HEADER + THIRD_PARTY_OWNER)
+ .to("refs/for/master");
+
+ assertNoReviewerAdded(result);
+ assertThat(result.getChange().notes().getComments().values())
+ .comparingElementsUsing(commentContains())
+ .containsExactly(
+ resolved("First-party license"),
+ resolved("Third-party author or owner")); // 1p license from 3p author is 1p
+ }
+
+ @Test
+ @GerritConfigs({
+ @GerritConfig(name = "plugin.copyright.enable", value = "true"),
+ @GerritConfig(name = "plugin.copyright.timeTestMax", value = "0")
+ })
+ public void testCopyrightValidator_pushFirstPartyOwnerThirdPartyOwner() throws Exception {
+ PushOneCommit.Result result =
+ pushFactory
+ .create(
+ author.newIdent(),
+ testRepo,
+ "subject",
+ "COPYING",
+ FIRST_PARTY_OWNER + THIRD_PARTY_OWNER)
+ .to("refs/for/master");
+
+ assertReviewerAdded(result);
+ assertThat(result.getChange().notes().getComments().values())
+ .comparingElementsUsing(commentContains())
+ .containsExactly(
+ resolved("First-party author or owner"),
+ unresolved("Third-party author or owner disallowed"));
+ }
+
+ @Test
+ @GerritConfigs({
+ @GerritConfig(name = "plugin.copyright.enable", value = "true"),
+ @GerritConfig(name = "plugin.copyright.timeTestMax", value = "0")
+ })
+ public void testCopyrightValidator_pushThirdPartyLicenseFirstPartyOwner() throws Exception {
+ PushOneCommit.Result result =
+ pushFactory
+ .create(
+ author.newIdent(),
+ testRepo,
+ "subject",
+ "COPYING",
+ THIRD_PARTY_MIT + FIRST_PARTY_OWNER)
+ .to("refs/for/master");
+
+ assertReviewerAdded(result);
+ assertThat(result.getChange().notes().getComments().values())
+ .comparingElementsUsing(commentContains())
+ .containsExactly(
+ unresolved("Third-party license disallowed"), resolved("First-party author or owner"));
+ }
+
+ private static final String FIRST_PARTY_OWNER =
+ "// Copyright (C) 2019 The Android Open Source Project\n";
+ private static final String FIRST_PARTY_HEADER =
+ "// Licensed under the Apache License, Version 2.0 (the \"License\");\n"
+ + "// you may not use this file except in compliance with the License.\n"
+ + "// You may obtain a copy of the License at\n//\n"
+ + "// http://www.apache.org/licenses/LICENSE-2.0\n//\n"
+ + "// Unless required by applicable law or agreed to in writing, software\n"
+ + "// distributed under the License is distributed on an \"AS IS\" BASIS,\n"
+ + "// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n"
+ + "// See the License for the specific language governing permissions and\n"
+ + "// limitations under the License.\n";
+ private static final String NOT_A_CONTRIB_HEADER = FIRST_PARTY_HEADER + "Not a contribution.\n";
+ private static final String THIRD_PARTY_MIT =
+ "MIT License\n\nCopyright (c) Jane Doe\n\n"
+ + "Permission is hereby granted, free of charge, to any person obtaining a copy\n"
+ + "of this software and associated documentation files (the \"Software\"), to deal\n"
+ + "in the Software without restriction, including without limitation the rights\n"
+ + "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n"
+ + "copies of the Software, and to permit persons to whom the Software is\n"
+ + "furnished to do so, subject to the following conditions:\n\n"
+ + "The above copyright notice and this permission notice shall be included in all\n"
+ + "copies or substantial portions of the Software.\n\n"
+ + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n"
+ + "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n"
+ + "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n"
+ + "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n"
+ + "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n"
+ + "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n"
+ + "SOFTWARE.\n";
+ private static final String THIRD_PARTY_OWNER = "Copyright (c) 2019 Acme Other Corp.\n";
+
+ private static int nextId = 123;
+
+ @Inject @ServerInitiated private Provider<GroupsUpdate> groupsUpdateProvider;
+
+ private InternalGroup botGroup;
+ private InternalGroup expertGroup;
+ private TestAccount pluginAccount;
+ private TestAccount reviewer;
+ private TestAccount observer;
+ private TestAccount author;
+ private String projectConfigContent;
+
+ @Before
+ public void setUp() throws Exception {
+ botGroup = testGroup("Non-Interactive Users");
+ expertGroup = testGroup("Copyright Experts");
+ pluginAccount =
+ accountCreator.create(
+ "copyright-scanner",
+ "copyright-scanner@example.com",
+ "Copyright Scanner",
+ "Non-Interactive Users",
+ expertGroup.getName());
+ reviewer =
+ accountCreator.create(
+ "lawyercat", "legal@example.com", "J. Doe J.D. LL.M. Esq.", expertGroup.getName());
+ observer = accountCreator.create("my-team", "my-team@example.com", "My Team");
+ author = accountCreator.create("author", "author@example.com", "J. Doe");
+ TestRepository<InMemoryRepository> testRepo = getTestRepo(allProjects);
+ TestConfig testConfig = new TestConfig(allProjects, plugin.getName(), admin, testRepo);
+ testConfig.copyLabel("Code-Review", "Copyright-Review");
+ testConfig.setVoters(
+ RefNames.REFS_HEADS + "*",
+ "Copyright-Review",
+ new TestConfig.Voter("Administrators", -2, +2),
+ new TestConfig.Voter(expertGroup.getNameKey().get(), -2, +2),
+ new TestConfig.Voter("Registered Users", -2, 0));
+ testConfig.addGroups(botGroup, expertGroup);
+ testConfig.updatePlugin(
+ TestConfig.BASE_CONFIG,
+ TestConfig.ENABLE_CONFIG,
+ cfg -> {
+ cfg.setStringList(KEY_REVIEWER, ImmutableList.of(reviewer.username()));
+ },
+ cfg -> {
+ cfg.setStringList(KEY_CC, ImmutableList.of(observer.username()));
+ },
+ cfg -> {
+ cfg.setInt(KEY_FROM, pluginAccount.id().get());
+ });
+ PushOneCommit.Result result = testConfig.push(pushFactory);
+ result.assertOkStatus();
+ assertThat(result.getChange().publishedComments()).isEmpty();
+ merge(result);
+ }
+
+ private AccountGroup.Id nextGroupId() {
+ return new AccountGroup.Id(nextId++);
+ }
+
+ private TestRepository<InMemoryRepository> getTestRepo(Project.NameKey projectName)
+ throws Exception {
+ TestRepository<InMemoryRepository> testRepo = cloneProject(projectName, admin);
+ GitUtil.fetch(testRepo, RefNames.REFS_CONFIG + ":" + LOCAL_BRANCH);
+ testRepo.reset(LOCAL_BRANCH);
+ return testRepo;
+ }
+
+ private InternalGroup testGroup(String name) throws Exception {
+ AccountGroup.NameKey nameKey = new AccountGroup.NameKey(name);
+ Optional<InternalGroup> g = groupCache.get(nameKey);
+ if (g.isPresent()) {
+ return g.get();
+ }
+ GroupsUpdate groupsUpdate = groupsUpdateProvider.get();
+ InternalGroupCreation gc =
+ InternalGroupCreation.builder()
+ .setGroupUUID(new AccountGroup.UUID("users-" + name.replace(" ", "_")))
+ .setNameKey(nameKey)
+ .setId(nextGroupId())
+ .build();
+ InternalGroupUpdate gu = InternalGroupUpdate.builder().setName(nameKey).build();
+ return groupsUpdate.createGroup(gc, gu);
+ }
+
+ private void assertReviewerAdded(PushOneCommit.Result result) throws Exception {
+ result.assertOkStatus();
+ result.assertChange(
+ Change.Status.NEW,
+ null,
+ ImmutableList.of(author, reviewer),
+ ImmutableList.of(pluginAccount, observer));
+ }
+
+ private void assertNoReviewerAdded(PushOneCommit.Result result) throws Exception {
+ result.assertOkStatus();
+ result.assertChange(
+ Change.Status.NEW, null, ImmutableList.of(author), ImmutableList.of(pluginAccount));
+ }
+
+ private CommentMatch resolved(String content) {
+ return new CommentMatch(true /* resolved */, content);
+ }
+
+ private CommentMatch unresolved(String content) {
+ return new CommentMatch(false /* resolved */, content);
+ }
+
+ private static Correspondence<Comment, CommentMatch> commentContains() {
+ return new Correspondence<Comment, CommentMatch>() {
+ @Override
+ public boolean compare(Comment actual, CommentMatch expected) {
+ return actual.unresolved != expected.resolved && actual.message.contains(expected.content);
+ }
+
+ @Override
+ public String toString() {
+ return "comment resolution status and content matches";
+ }
+ };
+ }
+
+ private static class CommentMatch {
+ boolean resolved;
+ String content;
+
+ CommentMatch(boolean resolved, String content) {
+ this.resolved = resolved;
+ this.content = content;
+ }
+
+ @Override
+ public String toString() {
+ return (resolved ? "" : "un") + "resolved(\"" + content + "\")";
+ }
+ }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/copyright/TestConfig.java b/src/test/java/com/googlesource/gerrit/plugins/copyright/TestConfig.java
index a8fc2c5..f8aab70 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/copyright/TestConfig.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/copyright/TestConfig.java
@@ -15,6 +15,7 @@
package com.googlesource.gerrit.plugins.copyright;
import static com.google.common.truth.Truth.assertThat;
+import static com.googlesource.gerrit.plugins.copyright.ScannerConfig.KEY_ALWAYS_REVIEW_PATH;
import static com.googlesource.gerrit.plugins.copyright.ScannerConfig.KEY_ENABLE;
import static com.googlesource.gerrit.plugins.copyright.ScannerConfig.KEY_EXCLUDE;
import static com.googlesource.gerrit.plugins.copyright.ScannerConfig.KEY_FIRST_PARTY;
@@ -22,6 +23,7 @@
import static com.googlesource.gerrit.plugins.copyright.ScannerConfig.KEY_FORBIDDEN_PATTERN;
import static com.googlesource.gerrit.plugins.copyright.ScannerConfig.KEY_REVIEW_LABEL;
import static com.googlesource.gerrit.plugins.copyright.ScannerConfig.KEY_THIRD_PARTY;
+import static com.googlesource.gerrit.plugins.copyright.ScannerConfig.KEY_THIRD_PARTY_ALLOWED_PROJECTS;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.collect.ImmutableList;
@@ -82,6 +84,9 @@
static final Consumer<PluginConfig> BASE_CONFIG =
pCfg -> {
+ pCfg.setStringList(KEY_ALWAYS_REVIEW_PATH, ImmutableList.of("PATENT$"));
+ pCfg.setStringList(
+ KEY_THIRD_PARTY_ALLOWED_PROJECTS, ImmutableList.of("ThirdParty(?:Owner)?Allowed"));
pCfg.setString(KEY_REVIEW_LABEL, "Copyright-Review");
pCfg.setStringList(KEY_EXCLUDE, ImmutableList.of("EXAMPLES"));
pCfg.setStringList(