Add initial plugin code
Based on pre-reviewed code in Gerrit core:
067697f26e3cafbd097f1ff36adc606cc928f1a7
https://gerrit-review.googlesource.com/c/gerrit/+/215003/1
BUILD files modified to build in the new directory layout (but note that
the Java package layout is the same).
Change-Id: Ie2f6df068dcc43497e55e60882d94d517ee9eaf5
diff --git a/BUILD b/BUILD
new file mode 100644
index 0000000..72b581d
--- /dev/null
+++ b/BUILD
@@ -0,0 +1,33 @@
+package_group(
+ name = "visibility",
+ packages = ["//plugins/checks/..."],
+)
+
+package(default_visibility = [":visibility"])
+
+load(
+ "//tools/bzl:plugin.bzl",
+ "gerrit_plugin",
+)
+
+gerrit_plugin(
+ name = "checks",
+ srcs = glob(["java/com/google/gerrit/plugins/checks/**/*.java"]),
+ manifest_entries = [
+ "Gerrit-PluginName: checks",
+ "Gerrit-Module: com.google.gerrit.plugins.checks.Module",
+ "Gerrit-HttpModule: com.google.gerrit.plugins.checks.api.HttpModule",
+ ],
+ deps = [":checks-deps-neverlink"],
+)
+
+java_library(
+ name = "checks-deps-neverlink",
+ neverlink = True,
+ visibility = ["//visibility:private"],
+ exports = [
+ "//java/com/google/gerrit/server/api",
+ "//lib/auto:auto-value",
+ "//lib/auto:auto-value-annotations",
+ ],
+)
diff --git a/build_defs/checks.bzl b/build_defs/checks.bzl
new file mode 100644
index 0000000..388bcbe
--- /dev/null
+++ b/build_defs/checks.bzl
@@ -0,0 +1,34 @@
+def checks_java_library(
+ name,
+ gerrit_deps = [],
+ **kwargs):
+ """Creates a java_library where core deps are not included in the jar.
+
+ Usually, Gerrit plugins are restricted to the libraries exported explicitly
+ in the plugin API. For in-tree plugins, adding an additional dep on some
+ other part of Gerrit (including from //lib) would cause that dep to get
+ compiled into the plugin jar. We don't want that, so this macro takes care
+ of excluding all specified gerrit_deps from the plugin jar.
+
+ Dependencies from outside Gerrit core, such as from the plugin itself, may
+ be supplied as regular deps.
+
+ Args:
+ name: name of the resulting java_library.
+ gerrit_deps: dependencies from within Gerrit core, which should not be
+ linked into the final plugin jar.
+ **kwargs: additional arguments for the resulting java_library.
+ """
+ deps_lib = "__%s_deps_neverlink" % name
+ native.java_library(
+ name = deps_lib,
+ neverlink = 1,
+ visibility = ["//visibility:private"],
+ exports = gerrit_deps,
+ )
+
+ native.java_library(
+ name = name,
+ deps = [":" + deps_lib],
+ **kwargs
+ )
diff --git a/java/com/google/gerrit/plugins/checks/AdministrateCheckersCapability.java b/java/com/google/gerrit/plugins/checks/AdministrateCheckersCapability.java
new file mode 100644
index 0000000..0bc40be
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/AdministrateCheckersCapability.java
@@ -0,0 +1,30 @@
+// 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.google.gerrit.plugins.checks;
+
+import static com.google.gerrit.plugins.checks.AdministrateCheckersCapability.NAME;
+
+import com.google.gerrit.extensions.annotations.Export;
+import com.google.gerrit.extensions.config.CapabilityDefinition;
+
+@Export(NAME)
+public class AdministrateCheckersCapability extends CapabilityDefinition {
+ public static final String NAME = "administrateCheckers";
+
+ @Override
+ public String getDescription() {
+ return "Administrate Checkers";
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/AdministrateCheckersPermission.java b/java/com/google/gerrit/plugins/checks/AdministrateCheckersPermission.java
new file mode 100644
index 0000000..c46572d
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/AdministrateCheckersPermission.java
@@ -0,0 +1,28 @@
+// 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.google.gerrit.plugins.checks;
+
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.api.access.PluginPermission;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+public class AdministrateCheckersPermission extends PluginPermission {
+ @Inject
+ AdministrateCheckersPermission(@PluginName String pluginName) {
+ super(pluginName, AdministrateCheckersCapability.NAME, false);
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/Checker.java b/java/com/google/gerrit/plugins/checks/Checker.java
new file mode 100644
index 0000000..dee99f6
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/Checker.java
@@ -0,0 +1,130 @@
+// 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.google.gerrit.plugins.checks;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.plugins.checks.api.CheckerStatus;
+import com.google.gerrit.reviewdb.client.Project;
+import java.sql.Timestamp;
+import java.util.Optional;
+import org.eclipse.jgit.lib.ObjectId;
+
+/** Definition of a checker. */
+@AutoValue
+public abstract class Checker {
+
+ /**
+ * Returns the UUID of the checker.
+ *
+ * <p>The UUID is a SHA-1 that is unique across all checkers.
+ *
+ * @return UUID
+ */
+ public abstract String getUuid();
+
+ /**
+ * Returns the display name of the checker.
+ *
+ * <p>Checker names are not unique, checkers with the same name may exist.
+ *
+ * @return display name of the checker
+ */
+ public abstract String getName();
+
+ /**
+ * Returns the description of the checker.
+ *
+ * <p>Checkers may not have a description, in this case {@link Optional#empty()} is returned.
+ *
+ * @return the description of the checker
+ */
+ public abstract Optional<String> getDescription();
+
+ /**
+ * Returns the URL of the checker.
+ *
+ * <p>Checkers may not have a URL, in this case {@link Optional#empty()} is returned.
+ *
+ * @return the URL of the checker
+ */
+ public abstract Optional<String> getUrl();
+
+ /**
+ * Returns the repository to which the checker applies.
+ *
+ * <p>The repository is the exact name of a repository (no prefix, no regexp).
+ *
+ * @return the repository to which the checker applies
+ */
+ public abstract Project.NameKey getRepository();
+
+ /**
+ * Returns the status of the checker.
+ *
+ * @return the status of the checker.
+ */
+ public abstract CheckerStatus getStatus();
+
+ /**
+ * Returns the creation timestamp of the checker.
+ *
+ * @return the creation timestamp
+ */
+ public abstract Timestamp getCreatedOn();
+
+ /**
+ * Returns the timestamp of when the checker was last updated.
+ *
+ * @return the last updated timestamp
+ */
+ public abstract Timestamp getUpdatedOn();
+
+ /**
+ * Returns the ref state of the checker.
+ *
+ * @return the ref state
+ */
+ public abstract ObjectId getRefState();
+
+ public abstract Builder toBuilder();
+
+ public static Builder builder(String uuid) {
+ return new AutoValue_Checker.Builder().setUuid(uuid);
+ }
+
+ /** A builder for an {@link Checker}. */
+ @AutoValue.Builder
+ public abstract static class Builder {
+ public abstract Builder setUuid(String uuid);
+
+ public abstract Builder setName(String name);
+
+ public abstract Builder setDescription(String description);
+
+ public abstract Builder setUrl(String url);
+
+ public abstract Builder setRepository(Project.NameKey repository);
+
+ public abstract Builder setStatus(CheckerStatus status);
+
+ public abstract Builder setCreatedOn(Timestamp createdOn);
+
+ public abstract Builder setUpdatedOn(Timestamp updatedOn);
+
+ public abstract Builder setRefState(ObjectId refState);
+
+ public abstract Checker build();
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/CheckerCommitValidator.java b/java/com/google/gerrit/plugins/checks/CheckerCommitValidator.java
new file mode 100644
index 0000000..b98751f
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/CheckerCommitValidator.java
@@ -0,0 +1,55 @@
+// 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.google.gerrit.plugins.checks;
+
+import com.google.gerrit.extensions.annotations.Listen;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.validators.CommitValidationListener;
+import com.google.gerrit.server.git.validators.CommitValidationMessage;
+import com.google.gerrit.server.util.MagicBranch;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.Collections;
+import java.util.List;
+
+/** Rejects updates to checker branches. */
+@Listen
+@Singleton
+public class CheckerCommitValidator implements CommitValidationListener {
+ private final AllProjectsName allProjects;
+
+ @Inject
+ public CheckerCommitValidator(AllProjectsName allProjects) {
+ this.allProjects = allProjects;
+ }
+
+ @Override
+ public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
+ throws CommitValidationException {
+ if (!allProjects.equals(receiveEvent.project.getNameKey())
+ || !CheckerRef.isRefsCheckers(receiveEvent.getRefName())) {
+ return Collections.emptyList();
+ }
+
+ if (MagicBranch.isMagicBranch(receiveEvent.command.getRefName())
+ || RefNames.isRefsChanges(receiveEvent.command.getRefName())) {
+ throw new CommitValidationException("creating change for checker ref not allowed");
+ }
+ throw new CommitValidationException("direct update of checker ref not allowed");
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/CheckerCreation.java b/java/com/google/gerrit/plugins/checks/CheckerCreation.java
new file mode 100644
index 0000000..a07188c
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/CheckerCreation.java
@@ -0,0 +1,49 @@
+// 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.google.gerrit.plugins.checks;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.reviewdb.client.Project;
+
+@AutoValue
+public abstract class CheckerCreation {
+ /**
+ * Defines the UUID the checker should have.
+ *
+ * <p>Must be a SHA-1 that is unique across all checkers.
+ */
+ public abstract String getCheckerUuid();
+
+ /** Defines the name the checker should have. */
+ public abstract String getName();
+
+ /** Defines the repository for which the checker applies. */
+ public abstract Project.NameKey getRepository();
+
+ public static Builder builder() {
+ return new AutoValue_CheckerCreation.Builder();
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+ public abstract Builder setCheckerUuid(String checkerUuid);
+
+ public abstract Builder setName(String name);
+
+ public abstract Builder setRepository(Project.NameKey repository);
+
+ public abstract CheckerCreation build();
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/CheckerJson.java b/java/com/google/gerrit/plugins/checks/CheckerJson.java
new file mode 100644
index 0000000..37683ef
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/CheckerJson.java
@@ -0,0 +1,35 @@
+// 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.google.gerrit.plugins.checks;
+
+import com.google.gerrit.plugins.checks.api.CheckerInfo;
+import com.google.inject.Singleton;
+
+/** Formats a {@link Checker} as JSON. */
+@Singleton
+public class CheckerJson {
+ public CheckerInfo format(Checker checker) {
+ CheckerInfo info = new CheckerInfo();
+ info.uuid = checker.getUuid();
+ info.name = checker.getName();
+ info.description = checker.getDescription().orElse(null);
+ info.url = checker.getUrl().orElse(null);
+ info.repository = checker.getRepository().get();
+ info.status = checker.getStatus();
+ info.createdOn = checker.getCreatedOn();
+ info.updatedOn = checker.getUpdatedOn();
+ return info;
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/CheckerMergeValidator.java b/java/com/google/gerrit/plugins/checks/CheckerMergeValidator.java
new file mode 100644
index 0000000..d90d321
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/CheckerMergeValidator.java
@@ -0,0 +1,53 @@
+// 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.google.gerrit.plugins.checks;
+
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.git.CodeReviewCommit;
+import com.google.gerrit.server.git.validators.MergeValidationException;
+import com.google.gerrit.server.git.validators.MergeValidationListener;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.Inject;
+import org.eclipse.jgit.lib.Repository;
+
+public class CheckerMergeValidator implements MergeValidationListener {
+
+ private final AllProjectsName allProjectsName;
+
+ @Inject
+ public CheckerMergeValidator(AllProjectsName allProjectsName) {
+ this.allProjectsName = allProjectsName;
+ }
+
+ @Override
+ public void onPreMerge(
+ Repository repo,
+ CodeReviewCommit commit,
+ ProjectState destProject,
+ Branch.NameKey destBranch,
+ PatchSet.Id patchSetId,
+ IdentifiedUser caller)
+ throws MergeValidationException {
+ if (!allProjectsName.equals(destProject.getNameKey())
+ || !CheckerRef.isRefsCheckers(destBranch.get())) {
+ return;
+ }
+
+ throw new MergeValidationException("submit to checker ref not allowed");
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/CheckerName.java b/java/com/google/gerrit/plugins/checks/CheckerName.java
new file mode 100644
index 0000000..3a3f837
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/CheckerName.java
@@ -0,0 +1,25 @@
+// 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.google.gerrit.plugins.checks;
+
+import com.google.common.base.Strings;
+
+public class CheckerName {
+ public static String clean(String checkerName) {
+ return Strings.nullToEmpty(checkerName).trim();
+ }
+
+ private CheckerName() {}
+}
diff --git a/java/com/google/gerrit/plugins/checks/CheckerRef.java b/java/com/google/gerrit/plugins/checks/CheckerRef.java
new file mode 100644
index 0000000..11a6d1f
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/CheckerRef.java
@@ -0,0 +1,37 @@
+// 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.google.gerrit.plugins.checks;
+
+import com.google.gerrit.reviewdb.client.RefNames;
+
+public class CheckerRef {
+ /** Ref namespace for checkers. */
+ public static final String REFS_CHECKERS = "refs/checkers/";
+
+ /** Ref that stores the repository to checkers map. */
+ public static final String REFS_META_CHECKERS = "refs/meta/checkers/";
+
+ public static String refsCheckers(String checkerUuid) {
+ return REFS_CHECKERS + RefNames.shardUuid(checkerUuid);
+ }
+
+ /**
+ * Whether the ref is a checker branch that stores NoteDb data of a checker. Returns {@code true}
+ * for all refs that start with {@code refs/checkers/}.
+ */
+ public static boolean isRefsCheckers(String ref) {
+ return ref.startsWith(REFS_CHECKERS);
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/CheckerRefOperationValidator.java b/java/com/google/gerrit/plugins/checks/CheckerRefOperationValidator.java
new file mode 100644
index 0000000..c7de4b3
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/CheckerRefOperationValidator.java
@@ -0,0 +1,52 @@
+// 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.google.gerrit.plugins.checks;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.events.RefReceivedEvent;
+import com.google.gerrit.server.git.validators.RefOperationValidationListener;
+import com.google.gerrit.server.git.validators.ValidationMessage;
+import com.google.gerrit.server.validators.ValidationException;
+import java.util.List;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+@Singleton
+public class CheckerRefOperationValidator implements RefOperationValidationListener {
+
+ private final AllProjectsName allProjectsName;
+
+ @Inject
+ CheckerRefOperationValidator(AllProjectsName allProjects) {
+ this.allProjectsName = allProjects;
+ }
+
+ @Override
+ public List<ValidationMessage> onRefOperation(RefReceivedEvent refEvent)
+ throws ValidationException {
+ if (refEvent.project.getNameKey().equals(allProjectsName)) {
+ if (CheckerRef.isRefsCheckers(refEvent.command.getRefName())) {
+ if (refEvent.command.getType().equals(ReceiveCommand.Type.CREATE)) {
+ throw new ValidationException("Not allowed to create checker ref.");
+ } else if (refEvent.command.getType().equals(ReceiveCommand.Type.DELETE)) {
+ throw new ValidationException("Not allowed to delete checker ref.");
+ }
+ }
+ }
+ return ImmutableList.of();
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/CheckerUpdate.java b/java/com/google/gerrit/plugins/checks/CheckerUpdate.java
new file mode 100644
index 0000000..a9a4595
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/CheckerUpdate.java
@@ -0,0 +1,84 @@
+// 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.google.gerrit.plugins.checks;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.plugins.checks.api.CheckerStatus;
+import com.google.gerrit.reviewdb.client.Project;
+import java.sql.Timestamp;
+import java.util.Optional;
+
+@AutoValue
+public abstract class CheckerUpdate {
+ /** Defines the new name of the checker. If not specified, the name remains unchanged. */
+ public abstract Optional<String> getName();
+
+ /**
+ * Defines the new description of the checker. If not specified, the description remains
+ * unchanged.
+ *
+ * <p><strong>Note: </strong>Passing the empty string unsets the description.
+ */
+ public abstract Optional<String> getDescription();
+
+ /**
+ * Defines the new URL of the checker. If not specified, the URL remains unchanged.
+ *
+ * <p><strong>Note: </strong>Passing the empty string unsets the URL.
+ */
+ public abstract Optional<String> getUrl();
+
+ /**
+ * Defines the new repository for which the checker applies. If not specified, the repository
+ * remains unchanged.
+ */
+ public abstract Optional<Project.NameKey> getRepository();
+
+ /** Defines the new status for the checker. If not specified, the status remains unchanged. */
+ public abstract Optional<CheckerStatus> getStatus();
+
+ /**
+ * Defines the {@code Timestamp} to be used for the NoteDb commits of the update. If not
+ * specified, the current {@code Timestamp} when creating the commit will be used.
+ *
+ * <p>If this {@code CheckerUpdate} is passed next to a {@link CheckerCreation} during a checker
+ * creation, this {@code Timestamp} is used for the NoteDb commits of the new checker. Hence, the
+ * {@link Checker#getCreatedOn()} field will match this {@code Timestamp}.
+ */
+ public abstract Optional<Timestamp> getUpdatedOn();
+
+ public abstract Builder toBuilder();
+
+ public static Builder builder() {
+ return new AutoValue_CheckerUpdate.Builder();
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+ public abstract Builder setName(String name);
+
+ public abstract Builder setDescription(String description);
+
+ public abstract Builder setUrl(String url);
+
+ public abstract Builder setRepository(Project.NameKey repository);
+
+ public abstract Builder setStatus(CheckerStatus status);
+
+ public abstract Builder setUpdatedOn(Timestamp timestamp);
+
+ public abstract CheckerUpdate build();
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/CheckerUrl.java b/java/com/google/gerrit/plugins/checks/CheckerUrl.java
new file mode 100644
index 0000000..601c4eb
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/CheckerUrl.java
@@ -0,0 +1,54 @@
+// 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.google.gerrit.plugins.checks;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+public class CheckerUrl {
+ /**
+ * Cleans a user-provided URL.
+ *
+ * @param urlString URL string. Must be either empty (after trimming), or a valid http/https URL.
+ * @return input string after trimming, guaranteed to be either the empty string or a valid
+ * http/https URL.
+ * @throws BadRequestException if the input is neither empty (after trimming) nor a valid URL.
+ */
+ public static String clean(String urlString) throws BadRequestException {
+ String trimmed = requireNonNull(urlString).trim();
+ if (trimmed.isEmpty()) {
+ return trimmed;
+ }
+ URI uri;
+ try {
+ uri = new URI(trimmed);
+ } catch (URISyntaxException e) {
+ uri = null;
+ }
+ if (uri == null || Strings.isNullOrEmpty(uri.getScheme())) {
+ throw new BadRequestException("invalid URL: " + urlString);
+ }
+ if (!uri.getScheme().equals("http") && !uri.getScheme().equals("https")) {
+ throw new BadRequestException("only http/https URLs supported: " + urlString);
+ }
+ return trimmed;
+ }
+
+ private CheckerUrl() {}
+}
diff --git a/java/com/google/gerrit/plugins/checks/CheckerUuid.java b/java/com/google/gerrit/plugins/checks/CheckerUuid.java
new file mode 100644
index 0000000..7338838
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/CheckerUuid.java
@@ -0,0 +1,93 @@
+// 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.google.gerrit.plugins.checks;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.reviewdb.client.RefNames;
+import java.security.MessageDigest;
+import java.util.Optional;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+
+public class CheckerUuid {
+ /**
+ * Creates a new UUID for a checker.
+ *
+ * <p>The creation of the UUID is non-deterministic. This means invoking this method multiple
+ * times with the same parameters will result in a different UUID for each call.
+ *
+ * @param checkerName checker name.
+ * @return checker UUID.
+ */
+ public static String make(String checkerName) {
+ MessageDigest md = Constants.newMessageDigest();
+ md.update(Constants.encode("checker " + checkerName + "\n"));
+ md.update(Constants.encode(String.valueOf(Math.random())));
+ return ObjectId.fromRaw(md.digest()).name();
+ }
+
+ /**
+ * Checks whether the given checker UUID has a valid format.
+ *
+ * @param checkerUuid the checker UUID to check
+ * @return {@code true} if the given checker UUID has a valid format, otherwise {@code false}
+ */
+ public static boolean isUuid(@Nullable String checkerUuid) {
+ return checkerUuid != null && ObjectId.isId(checkerUuid);
+ }
+
+ /**
+ * Checks whether the given checker UUID has a valid format.
+ *
+ * @param checkerUuid the checker UUID to check
+ * @return the checker UUID
+ * @throws IllegalStateException if the given checker UUID has an invalid format
+ */
+ public static String checkUuid(String checkerUuid) {
+ checkState(isUuid(checkerUuid), "invalid checker UUID: %s", checkerUuid);
+ return checkerUuid;
+ }
+
+ /**
+ * Parses a checker UUID from a checker ref.
+ *
+ * @param ref the ref from which a checker UUID should be parsed
+ * @return the checker UUID, {@link Optional#empty()} if the given ref is null or not a valid
+ * checker ref
+ */
+ public static Optional<String> fromRef(@Nullable Ref ref) {
+ return fromRef(ref != null ? ref.getName() : (String) null);
+ }
+
+ /**
+ * Parses a checker UUID from a checker ref name.
+ *
+ * @param refName the name of the ref from which a checker UUID should be parsed
+ * @return the checker UUID, {@link Optional#empty()} if the given ref name is null or not a valid
+ * checker ref name
+ */
+ public static Optional<String> fromRef(@Nullable String refName) {
+ if (refName == null || !CheckerRef.isRefsCheckers(refName)) {
+ return Optional.empty();
+ }
+ return Optional.ofNullable(
+ RefNames.parseShardedUuidFromRefPart(refName.substring(CheckerRef.REFS_CHECKERS.length())));
+ }
+
+ private CheckerUuid() {}
+}
diff --git a/java/com/google/gerrit/plugins/checks/Checkers.java b/java/com/google/gerrit/plugins/checks/Checkers.java
new file mode 100644
index 0000000..aaa1e91
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/Checkers.java
@@ -0,0 +1,69 @@
+// 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.google.gerrit.plugins.checks;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.reviewdb.client.Project;
+import java.io.IOException;
+import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+/**
+ * A database accessor for read calls related to checkers.
+ *
+ * <p>All calls which read checker related details from the database are gathered here. Other
+ * classes should always use this class instead of accessing the database directly.
+ *
+ * <p>This is an interface so that the implementation can be swapped if needed.
+ */
+public interface Checkers {
+ /**
+ * Returns the checker for the given UUID.
+ *
+ * <p>If no checker with the given UUID exists, {@link Optional#empty()} is returned.
+ *
+ * @param checkerUuid the checker UUID
+ * @return the checker, {@link Optional#empty()} if no checker with the given UUID exists
+ * @throws IOException if the checker couldn't be retrieved from the storage
+ * @throws ConfigInvalidException if the checker in the storage is invalid
+ */
+ Optional<Checker> getChecker(String checkerUuid) throws IOException, ConfigInvalidException;
+
+ /**
+ * Returns a list with all checkers.
+ *
+ * <p>Checkers with invalid configuration are silently ignored.
+ *
+ * @return all checkers, sorted by UUID
+ * @throws IOException if any checker couldn't be retrieved from the storage
+ */
+ ImmutableList<Checker> listCheckers() throws IOException;
+
+ /**
+ * Returns the checkers that apply to the given repository.
+ *
+ * <p>Checkers with invalid configuration are silently ignored.
+ *
+ * @param repositoryName the name of the repository for which the applying checkers should be
+ * returned
+ * @return the checkers that apply that apply to the given repository
+ * @throws IOException if reading the checker list fails or if any checker couldn't be retrieved
+ * from the storage
+ * @throws ConfigInvalidException if reading the checker list fails
+ */
+ ImmutableSet<Checker> checkersOf(Project.NameKey repositoryName)
+ throws IOException, ConfigInvalidException;
+}
diff --git a/java/com/google/gerrit/plugins/checks/CheckersUpdate.java b/java/com/google/gerrit/plugins/checks/CheckersUpdate.java
new file mode 100644
index 0000000..b4c57dc
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/CheckersUpdate.java
@@ -0,0 +1,64 @@
+// 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.google.gerrit.plugins.checks;
+
+import com.google.gwtorm.server.OrmDuplicateKeyException;
+import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+/**
+ * A database accessor for write calls related to checkers.
+ *
+ * <p>All calls which write checker related details to the database are gathered here. Other classes
+ * should always use this interface instead of accessing the database directly.
+ *
+ * <p>This is an interface so that the implementation can be swapped if needed.
+ *
+ * <p>Callers should use the {@link com.google.gerrit.server.UserInitiated} annotation or the {@link
+ * com.google.gerrit.server.ServerInitiated} annotation on a provider of a {@code CheckersUpdate} to
+ * get access to a {@code CheckersUpdate} instance.
+ */
+public interface CheckersUpdate {
+ /**
+ * Creates the specified checker.
+ *
+ * @param checkerCreation an {@code CheckerCreation} which specifies all mandatory properties of
+ * the checker
+ * @param checkerUpdate an {@code CheckerUpdate} which specifies optional properties of the
+ * checker. If this {@code CheckerUpdate} updates a property which was already specified by
+ * the {@code CheckerCreation}, the value of this {@code CheckerUpdate} wins.
+ * @throws OrmDuplicateKeyException if a checker with the chosen UUID already exists
+ * @throws IOException if an error occurs while reading/writing from/to storage
+ * @throws ConfigInvalidException if a checker with the same UUID already exists but can't be read
+ * due to an invalid format
+ * @return the created {@code Checker}
+ */
+ Checker createChecker(CheckerCreation checkerCreation, CheckerUpdate checkerUpdate)
+ throws OrmDuplicateKeyException, IOException, ConfigInvalidException;
+
+ /**
+ * Updates the specified checker.
+ *
+ * @param checkerUuid the UUID of the checker to update
+ * @param checkerUpdate an {@code CheckerUpdate} which indicates the desired updates on the
+ * checker
+ * @throws NoSuchCheckerException if the specified checker doesn't exist
+ * @throws IOException if an error occurs while reading/writing from/to storage
+ * @throws ConfigInvalidException if the existing checker config is invalid
+ * @return the updated {@code Checker}
+ */
+ Checker updateChecker(String checkerUuid, CheckerUpdate checkerUpdate)
+ throws NoSuchCheckerException, IOException, ConfigInvalidException;
+}
diff --git a/java/com/google/gerrit/plugins/checks/Module.java b/java/com/google/gerrit/plugins/checks/Module.java
new file mode 100644
index 0000000..9e5e828
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/Module.java
@@ -0,0 +1,41 @@
+// 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.google.gerrit.plugins.checks;
+
+import static com.google.inject.Scopes.SINGLETON;
+
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.plugins.checks.db.NoteDbCheckersModule;
+import com.google.gerrit.server.git.validators.CommitValidationListener;
+import com.google.gerrit.server.git.validators.MergeValidationListener;
+import com.google.gerrit.server.git.validators.RefOperationValidationListener;
+
+public class Module extends FactoryModule {
+ @Override
+ protected void configure() {
+ install(new NoteDbCheckersModule());
+
+ DynamicSet.bind(binder(), CommitValidationListener.class)
+ .to(CheckerCommitValidator.class)
+ .in(SINGLETON);
+ DynamicSet.bind(binder(), MergeValidationListener.class)
+ .to(CheckerMergeValidator.class)
+ .in(SINGLETON);
+ DynamicSet.bind(binder(), RefOperationValidationListener.class)
+ .to(CheckerRefOperationValidator.class)
+ .in(SINGLETON);
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/NoSuchCheckerException.java b/java/com/google/gerrit/plugins/checks/NoSuchCheckerException.java
new file mode 100644
index 0000000..ea06942
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/NoSuchCheckerException.java
@@ -0,0 +1,30 @@
+// 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.google.gerrit.plugins.checks;
+
+/** Indicates the checker does not exist. */
+public class NoSuchCheckerException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public static final String MESSAGE = "Checker Not Found: ";
+
+ public NoSuchCheckerException(String uuid) {
+ this(uuid, null);
+ }
+
+ public NoSuchCheckerException(String uuid, Throwable why) {
+ super(MESSAGE + uuid, why);
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/acceptance/AbstractCheckersTest.java b/java/com/google/gerrit/plugins/checks/acceptance/AbstractCheckersTest.java
new file mode 100644
index 0000000..5ad688b
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/acceptance/AbstractCheckersTest.java
@@ -0,0 +1,52 @@
+// 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.google.gerrit.plugins.checks.acceptance;
+
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.ProjectResetter;
+import com.google.gerrit.acceptance.SkipProjectClone;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.plugins.checks.CheckerRef;
+import com.google.gerrit.plugins.checks.acceptance.testsuite.CheckerOperations;
+import com.google.gerrit.plugins.checks.api.Checkers;
+import org.junit.Before;
+
+// TODO(dborowitz): Improve the plugin test framework so we can avoid subclassing:
+// * Defer injection until after the plugin is loaded, so we can @Inject members defined in plugin
+// modules, rather than hard-coding them non-scalably like we do here.
+// * Don't require all test classes to hard-code the @TestPlugin annotation.
+@TestPlugin(
+ name = "checks",
+ sysModule = "com.google.gerrit.plugins.checks.acceptance.TestModule",
+ httpModule = "com.google.gerrit.plugins.checks.api.HttpModule")
+@SkipProjectClone
+public class AbstractCheckersTest extends LightweightPluginDaemonTest {
+ protected CheckerOperations checkerOperations;
+ protected Checkers checkersApi;
+
+ @Override
+ protected ProjectResetter.Config resetProjects() {
+ return super.resetProjects()
+ .reset(allProjects, CheckerRef.REFS_CHECKERS + "*", CheckerRef.REFS_META_CHECKERS);
+ }
+
+ @Before
+ public void setUpCheckersPlugin() throws Exception {
+ checkerOperations = plugin.getSysInjector().getInstance(CheckerOperations.class);
+ checkersApi = plugin.getHttpInjector().getInstance(Checkers.class);
+
+ allowGlobalCapabilities(group("Administrators").getGroupUUID(), "checks-administrateCheckers");
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/acceptance/BUILD b/java/com/google/gerrit/plugins/checks/acceptance/BUILD
new file mode 100644
index 0000000..6a1d023
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/acceptance/BUILD
@@ -0,0 +1,14 @@
+package(
+ default_testonly = True,
+ default_visibility = ["//plugins/checks:visibility"],
+)
+
+java_library(
+ name = "acceptance",
+ srcs = glob(["*.java"]),
+ deps = [
+ "//java/com/google/gerrit/acceptance:lib",
+ "//plugins/checks:checks__plugin",
+ "//plugins/checks/java/com/google/gerrit/plugins/checks/acceptance/testsuite",
+ ],
+)
diff --git a/java/com/google/gerrit/plugins/checks/acceptance/TestModule.java b/java/com/google/gerrit/plugins/checks/acceptance/TestModule.java
new file mode 100644
index 0000000..7d6686a
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/acceptance/TestModule.java
@@ -0,0 +1,31 @@
+// 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.google.gerrit.plugins.checks.acceptance;
+
+import com.google.gerrit.plugins.checks.Module;
+import com.google.gerrit.plugins.checks.acceptance.testsuite.CheckerOperations;
+import com.google.gerrit.plugins.checks.acceptance.testsuite.CheckerOperationsImpl;
+import com.google.inject.AbstractModule;
+
+public class TestModule extends AbstractModule {
+ @Override
+ public void configure() {
+ install(new Module());
+
+ // Only add bindings here that are specifically required for tests, in order to keep the Guice
+ // setup in tests as realistic as possible by delegating to the original module.
+ bind(CheckerOperations.class).to(CheckerOperationsImpl.class);
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/acceptance/testsuite/BUILD b/java/com/google/gerrit/plugins/checks/acceptance/testsuite/BUILD
new file mode 100644
index 0000000..62a5299
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/acceptance/testsuite/BUILD
@@ -0,0 +1,13 @@
+package(
+ default_testonly = True,
+ default_visibility = ["//plugins/checks:visibility"],
+)
+
+java_library(
+ name = "testsuite",
+ srcs = glob(["*.java"]),
+ deps = [
+ "//java/com/google/gerrit/acceptance:lib",
+ "//plugins/checks:checks__plugin",
+ ],
+)
diff --git a/java/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckerOperations.java b/java/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckerOperations.java
new file mode 100644
index 0000000..4777697
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckerOperations.java
@@ -0,0 +1,154 @@
+// 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.google.gerrit.plugins.checks.acceptance.testsuite;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.plugins.checks.api.CheckerInfo;
+import com.google.gerrit.reviewdb.client.Project;
+import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+/**
+ * An aggregation of operations on checkers for test purposes.
+ *
+ * <p>To execute the operations, no Gerrit permissions are necessary.
+ *
+ * <p><strong>Note:</strong> This interface is not implemented using the REST or extension API.
+ * Hence, it cannot be used for testing those APIs.
+ */
+public interface CheckerOperations {
+ /**
+ * Starts the fluent chain for querying or modifying a checker. Please see the methods of {@link
+ * PerCheckerOperations} for details on possible operations.
+ *
+ * @return an aggregation of operations on a specific checker
+ */
+ PerCheckerOperations checker(String checkerUuid);
+
+ /**
+ * Starts the fluent chain to create a checker. The returned builder can be used to specify the
+ * attributes of the new checker. To create the checker for real, {@link
+ * TestCheckerCreation.Builder#create()} must be called.
+ *
+ * <p>Example:
+ *
+ * <pre>
+ * String createdCheckerUuid = checkerOperations
+ * .newChecker()
+ * .name("my-checker")
+ * .description("A simple checker.")
+ * .create();
+ * </pre>
+ *
+ * <p><strong>Note:</strong> If another checker with the provided name already exists, the
+ * creation of the checker will succeed since checker names are not unique.
+ *
+ * @return a builder to create the new checker
+ */
+ TestCheckerCreation.Builder newChecker();
+
+ /**
+ * Returns the UUIDs of the checkers that apply to the given repository.
+ *
+ * @param repositoryName repository name
+ * @return set of UUIDs of the checkers that apply to the given repository
+ * @throws IOException if reading the checker list fails
+ */
+ ImmutableSet<String> checkersOf(Project.NameKey repositoryName) throws IOException;
+
+ /**
+ * Returns the SHA1s of the repositories that have applying checkers.
+ *
+ * <p>These are the keys used in the {@code NoteMap} of {@code refs/meta/checkers}.
+ *
+ * @return the SHA1s of the repositories that have applying checkers
+ * @throws IOException if reading the repository SHA1s fails
+ */
+ ImmutableSet<ObjectId> sha1sOfRepositoriesWithCheckers() throws IOException;
+
+ /** An aggregation of methods on a specific checker. */
+ interface PerCheckerOperations {
+
+ /**
+ * Checks whether the checker exists.
+ *
+ * @return {@code true} if the checker exists
+ */
+ boolean exists();
+
+ /**
+ * Retrieves the checker.
+ *
+ * <p><strong>Note:</strong> This call will fail with an exception if the requested checker
+ * doesn't exist. If you want to check for the existence of a checker, use {@link #exists()}
+ * instead.
+ *
+ * @return the corresponding {@code TestChecker}
+ */
+ TestChecker get();
+
+ /**
+ * Retrieves the tip commit of the checker ref.
+ *
+ * <p><strong>Note:</strong>This call will fail with an exception if the checker doesn't exist.
+ *
+ * @return the tip commit of the checker ref
+ * @throws IOException if reading the commit fails
+ */
+ RevCommit commit() throws IOException;
+
+ /**
+ * Retrieves the checker config as text.
+ *
+ * <p>This call reads the checker config from the checker ref and returns it as text.
+ *
+ * <p><strong>Note:</strong>This call will fail with an exception if the checker doesn't exist.
+ *
+ * @return the checker config as text
+ * @throws IOException if reading the checker config fails
+ * @throws ConfigInvalidException if the checker config is invalid
+ */
+ String configText() throws IOException, ConfigInvalidException;
+
+ /**
+ * Returns this checker as {@link CheckerInfo}.
+ *
+ * <p><strong>Note:</strong>This call will fail with an exception if the checker doesn't exist.
+ *
+ * @return this checker as {@link CheckerInfo}
+ */
+ CheckerInfo asInfo();
+
+ /**
+ * Starts the fluent chain to update a checker. The returned builder can be used to specify how
+ * the attributes of the checker should be modified. To update the checker for real, {@link
+ * TestCheckerUpdate.Builder#update()} must be called.
+ *
+ * <p>Example:
+ *
+ * <pre>
+ * checkerOperations.forUpdate().description("Another description for this checker").update();
+ * </pre>
+ *
+ * <p><strong>Note:</strong> The update will fail with an exception if the checker to update
+ * doesn't exist. If you want to check for the existence of a checker, use {@link #exists()}.
+ *
+ * @return a builder to update the checker
+ */
+ TestCheckerUpdate.Builder forUpdate();
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckerOperationsImpl.java b/java/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckerOperationsImpl.java
new file mode 100644
index 0000000..89f23b5
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckerOperationsImpl.java
@@ -0,0 +1,256 @@
+// 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.google.gerrit.plugins.checks.acceptance.testsuite;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Streams;
+import com.google.gerrit.plugins.checks.Checker;
+import com.google.gerrit.plugins.checks.CheckerCreation;
+import com.google.gerrit.plugins.checks.CheckerJson;
+import com.google.gerrit.plugins.checks.CheckerRef;
+import com.google.gerrit.plugins.checks.CheckerUpdate;
+import com.google.gerrit.plugins.checks.CheckerUuid;
+import com.google.gerrit.plugins.checks.Checkers;
+import com.google.gerrit.plugins.checks.CheckersUpdate;
+import com.google.gerrit.plugins.checks.NoSuchCheckerException;
+import com.google.gerrit.plugins.checks.api.CheckerInfo;
+import com.google.gerrit.plugins.checks.db.CheckerConfig;
+import com.google.gerrit.plugins.checks.db.CheckersByRepositoryNotes;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.ServerInitiated;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.BlobBasedConfig;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+
+/**
+ * The implementation of {@code CheckerOperations}.
+ *
+ * <p>There is only one implementation of {@code CheckerOperations}. Nevertheless, we keep the
+ * separation between interface and implementation to enhance clarity.
+ */
+public class CheckerOperationsImpl implements CheckerOperations {
+ private final Checkers checkers;
+ private final CheckersUpdate checkersUpdate;
+ private final GitRepositoryManager repoManager;
+ private final AllProjectsName allProjectsName;
+ private final CheckerJson checkerJson;
+
+ @Inject
+ public CheckerOperationsImpl(
+ Checkers checkers,
+ @ServerInitiated CheckersUpdate checkersUpdate,
+ GitRepositoryManager repoManager,
+ AllProjectsName allProjectsName,
+ CheckerJson checkerJson) {
+ this.checkers = checkers;
+ this.checkersUpdate = checkersUpdate;
+ this.repoManager = repoManager;
+ this.allProjectsName = allProjectsName;
+ this.checkerJson = checkerJson;
+ }
+
+ @Override
+ public PerCheckerOperations checker(String checkerUuid) {
+ return new PerCheckerOperationsImpl(checkerUuid);
+ }
+
+ @Override
+ public TestCheckerCreation.Builder newChecker() {
+ return TestCheckerCreation.builder(this::createNewChecker);
+ }
+
+ private String createNewChecker(TestCheckerCreation testCheckerCreation)
+ throws OrmDuplicateKeyException, ConfigInvalidException, IOException {
+ CheckerCreation checkerCreation = toCheckerCreation(testCheckerCreation);
+ CheckerUpdate checkerUpdate = toCheckerUpdate(testCheckerCreation);
+ Checker checker = checkersUpdate.createChecker(checkerCreation, checkerUpdate);
+ return checker.getUuid();
+ }
+
+ private CheckerCreation toCheckerCreation(TestCheckerCreation checkerCreation) {
+ String checkerUuid = CheckerUuid.make("test-checker");
+ String checkerName = checkerCreation.name().orElse("checker-with-uuid-" + checkerUuid);
+ Project.NameKey repository = checkerCreation.repository().orElse(allProjectsName);
+ return CheckerCreation.builder()
+ .setCheckerUuid(checkerUuid)
+ .setName(checkerName)
+ .setRepository(repository)
+ .build();
+ }
+
+ private static CheckerUpdate toCheckerUpdate(TestCheckerCreation checkerCreation) {
+ CheckerUpdate.Builder builder = CheckerUpdate.builder();
+ checkerCreation.name().ifPresent(builder::setName);
+ checkerCreation.description().ifPresent(builder::setDescription);
+ checkerCreation.url().ifPresent(builder::setUrl);
+ checkerCreation.repository().ifPresent(builder::setRepository);
+ return builder.build();
+ }
+
+ @Override
+ public ImmutableSet<String> checkersOf(Project.NameKey repositoryName) throws IOException {
+ try (Repository repo = repoManager.openRepository(allProjectsName);
+ RevWalk rw = new RevWalk(repo);
+ ObjectReader or = repo.newObjectReader()) {
+ Ref ref = repo.exactRef(CheckerRef.REFS_META_CHECKERS);
+ if (ref == null) {
+ return ImmutableSet.of();
+ }
+
+ RevCommit c = rw.parseCommit(ref.getObjectId());
+ try (TreeWalk tw =
+ TreeWalk.forPath(
+ or,
+ CheckersByRepositoryNotes.computeRepositorySha1(repositoryName).getName(),
+ c.getTree())) {
+ if (tw == null) {
+ return ImmutableSet.of();
+ }
+
+ return ImmutableSet.copyOf(
+ Splitter.on('\n')
+ .splitToList(new String(or.open(tw.getObjectId(0), OBJ_BLOB).getBytes(), UTF_8)));
+ }
+ }
+ }
+
+ @Override
+ public ImmutableSet<ObjectId> sha1sOfRepositoriesWithCheckers() throws IOException {
+ try (Repository repo = repoManager.openRepository(allProjectsName);
+ RevWalk rw = new RevWalk(repo)) {
+ Ref ref = repo.exactRef(CheckerRef.REFS_META_CHECKERS);
+ if (ref == null) {
+ return ImmutableSet.of();
+ }
+
+ return Streams.stream(NoteMap.read(rw.getObjectReader(), rw.parseCommit(ref.getObjectId())))
+ .map(ObjectId::copy)
+ .collect(toImmutableSet());
+ }
+ }
+
+ private class PerCheckerOperationsImpl implements PerCheckerOperations {
+ private final String checkerUuid;
+
+ PerCheckerOperationsImpl(String checkerUuid) {
+ this.checkerUuid = checkerUuid;
+ }
+
+ @Override
+ public boolean exists() {
+ return getChecker(checkerUuid).isPresent();
+ }
+
+ @Override
+ public TestChecker get() {
+ Optional<Checker> checker = getChecker(checkerUuid);
+ checkState(checker.isPresent(), "Tried to get non-existing test checker");
+ return toTestChecker(checker.get());
+ }
+
+ private Optional<Checker> getChecker(String checkerUuid) {
+ try {
+ return checkers.getChecker(checkerUuid);
+ } catch (IOException | ConfigInvalidException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private TestChecker toTestChecker(Checker checker) {
+ return TestChecker.builder()
+ .uuid(checker.getUuid())
+ .name(checker.getName())
+ .description(checker.getDescription())
+ .url(checker.getUrl())
+ .repository(checker.getRepository())
+ .createdOn(checker.getCreatedOn())
+ .updatedOn(checker.getUpdatedOn())
+ .refState(checker.getRefState())
+ .build();
+ }
+
+ @Override
+ public RevCommit commit() throws IOException {
+ Optional<Checker> checker = getChecker(checkerUuid);
+ checkState(checker.isPresent(), "Tried to get commit for a non-existing test checker");
+
+ try (Repository repo = repoManager.openRepository(allProjectsName);
+ RevWalk rw = new RevWalk(repo)) {
+ return rw.parseCommit(checker.get().getRefState());
+ }
+ }
+
+ @Override
+ public String configText() throws IOException, ConfigInvalidException {
+ Optional<Checker> checker = getChecker(checkerUuid);
+ checkState(checker.isPresent(), "Tried to get config text for a non-existing test checker");
+
+ try (Repository repo = repoManager.openRepository(allProjectsName);
+ RevWalk rw = new RevWalk(repo);
+ ObjectReader or = repo.newObjectReader()) {
+ // Parse as Config to ensure it's a valid config file.
+ return new BlobBasedConfig(
+ null, repo, checker.get().getRefState(), CheckerConfig.CHECKER_CONFIG_FILE)
+ .toText();
+ }
+ }
+
+ @Override
+ public CheckerInfo asInfo() {
+ Optional<Checker> checker = getChecker(checkerUuid);
+ checkState(checker.isPresent(), "Tried to get a non-existing test checker as CheckerInfo");
+ return checkerJson.format(checker.get());
+ }
+
+ public TestCheckerUpdate.Builder forUpdate() {
+ return TestCheckerUpdate.builder(this::updateChecker);
+ }
+
+ private void updateChecker(TestCheckerUpdate testCheckerUpdate)
+ throws NoSuchCheckerException, ConfigInvalidException, IOException {
+ CheckerUpdate checkerUpdate = toCheckerUpdate(testCheckerUpdate);
+ checkersUpdate.updateChecker(checkerUuid, checkerUpdate);
+ }
+
+ private CheckerUpdate toCheckerUpdate(TestCheckerUpdate checkerUpdate) {
+ CheckerUpdate.Builder builder = CheckerUpdate.builder();
+ checkerUpdate.name().ifPresent(builder::setName);
+ checkerUpdate.description().ifPresent(builder::setDescription);
+ checkerUpdate.url().ifPresent(builder::setUrl);
+ checkerUpdate.repository().ifPresent(builder::setRepository);
+ checkerUpdate.status().ifPresent(builder::setStatus);
+ return builder.build();
+ }
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/acceptance/testsuite/TestChecker.java b/java/com/google/gerrit/plugins/checks/acceptance/testsuite/TestChecker.java
new file mode 100644
index 0000000..cdc0df4
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/acceptance/testsuite/TestChecker.java
@@ -0,0 +1,70 @@
+// 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.google.gerrit.plugins.checks.acceptance.testsuite;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.reviewdb.client.Project;
+import java.sql.Timestamp;
+import java.util.Optional;
+import org.eclipse.jgit.lib.ObjectId;
+
+@AutoValue
+public abstract class TestChecker {
+ public abstract String uuid();
+
+ public abstract String name();
+
+ public abstract Optional<String> description();
+
+ public abstract Optional<String> url();
+
+ public abstract Project.NameKey repository();
+
+ public abstract Timestamp createdOn();
+
+ public abstract Timestamp updatedOn();
+
+ public abstract ObjectId refState();
+
+ static Builder builder() {
+ return new AutoValue_TestChecker.Builder();
+ }
+
+ @AutoValue.Builder
+ abstract static class Builder {
+
+ public abstract Builder uuid(String checkerUuid);
+
+ public abstract Builder name(String name);
+
+ public abstract Builder description(String description);
+
+ public abstract Builder description(Optional<String> description);
+
+ public abstract Builder url(String url);
+
+ public abstract Builder url(Optional<String> url);
+
+ public abstract Builder repository(Project.NameKey repository);
+
+ public abstract Builder createdOn(Timestamp createdOn);
+
+ public abstract Builder updatedOn(Timestamp updatedOn);
+
+ public abstract Builder refState(ObjectId refState);
+
+ abstract TestChecker build();
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/acceptance/testsuite/TestCheckerCreation.java b/java/com/google/gerrit/plugins/checks/acceptance/testsuite/TestCheckerCreation.java
new file mode 100644
index 0000000..43636a4
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/acceptance/testsuite/TestCheckerCreation.java
@@ -0,0 +1,72 @@
+// 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.google.gerrit.plugins.checks.acceptance.testsuite;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.acceptance.testsuite.ThrowingFunction;
+import com.google.gerrit.reviewdb.client.Project;
+import java.util.Optional;
+
+@AutoValue
+public abstract class TestCheckerCreation {
+
+ public abstract Optional<String> name();
+
+ public abstract Optional<String> description();
+
+ public abstract Optional<String> url();
+
+ public abstract Optional<Project.NameKey> repository();
+
+ abstract ThrowingFunction<TestCheckerCreation, String> checkerCreator();
+
+ public static Builder builder(ThrowingFunction<TestCheckerCreation, String> checkerCreator) {
+ return new AutoValue_TestCheckerCreation.Builder().checkerCreator(checkerCreator);
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+
+ public abstract Builder name(String name);
+
+ public abstract Builder description(String description);
+
+ public Builder clearDescription() {
+ return description("");
+ }
+
+ public abstract Builder url(String url);
+
+ public Builder clearUrl() {
+ return url("");
+ }
+
+ public abstract Builder repository(Project.NameKey repository);
+
+ abstract Builder checkerCreator(ThrowingFunction<TestCheckerCreation, String> checkerCreator);
+
+ abstract TestCheckerCreation autoBuild();
+
+ /**
+ * Executes the checker creation as specified.
+ *
+ * @return the UUID of the created checker
+ */
+ public String create() {
+ TestCheckerCreation checkerCreation = autoBuild();
+ return checkerCreation.checkerCreator().applyAndThrowSilently(checkerCreation);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/acceptance/testsuite/TestCheckerUpdate.java b/java/com/google/gerrit/plugins/checks/acceptance/testsuite/TestCheckerUpdate.java
new file mode 100644
index 0000000..9fb77bb
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/acceptance/testsuite/TestCheckerUpdate.java
@@ -0,0 +1,80 @@
+// 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.google.gerrit.plugins.checks.acceptance.testsuite;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.acceptance.testsuite.ThrowingConsumer;
+import com.google.gerrit.plugins.checks.api.CheckerStatus;
+import com.google.gerrit.reviewdb.client.Project;
+import java.util.Optional;
+
+@AutoValue
+public abstract class TestCheckerUpdate {
+ public abstract Optional<String> name();
+
+ public abstract Optional<String> description();
+
+ public abstract Optional<String> url();
+
+ public abstract Optional<Project.NameKey> repository();
+
+ public abstract Optional<CheckerStatus> status();
+
+ abstract ThrowingConsumer<TestCheckerUpdate> checkerUpdater();
+
+ public static Builder builder(ThrowingConsumer<TestCheckerUpdate> checkerUpdater) {
+ return new AutoValue_TestCheckerUpdate.Builder().checkerUpdater(checkerUpdater);
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+
+ public abstract Builder name(String name);
+
+ public abstract Builder description(String description);
+
+ public Builder clearDescription() {
+ return description("");
+ }
+
+ public abstract Builder url(String url);
+
+ public Builder clearUrl() {
+ return url("");
+ }
+
+ public abstract Builder repository(Project.NameKey repository);
+
+ abstract Builder status(CheckerStatus status);
+
+ public Builder enable() {
+ return status(CheckerStatus.ENABLED);
+ }
+
+ public Builder disable() {
+ return status(CheckerStatus.DISABLED);
+ }
+
+ abstract Builder checkerUpdater(ThrowingConsumer<TestCheckerUpdate> checkerUpdater);
+
+ abstract TestCheckerUpdate autoBuild();
+
+ /** Executes the checker update as specified. */
+ public void update() {
+ TestCheckerUpdate checkerUpdater = autoBuild();
+ checkerUpdater.checkerUpdater().acceptAndThrowSilently(checkerUpdater);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/api/CheckerApi.java b/java/com/google/gerrit/plugins/checks/api/CheckerApi.java
new file mode 100644
index 0000000..156fdc4
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/api/CheckerApi.java
@@ -0,0 +1,62 @@
+// 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.google.gerrit.plugins.checks.api;
+
+import com.google.gerrit.extensions.restapi.NotImplementedException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+
+public interface CheckerApi {
+ /** @return checker info. */
+ CheckerInfo get() throws RestApiException;
+
+ /**
+ * Updates a checker.
+ *
+ * <p>This method supports partial updates of the checker property set. Only properties that are
+ * set in the given input are updated. Properties that are not set in the input (that have `null`
+ * as value) are not touched.
+ *
+ * <p>Unsetting properties:
+ *
+ * <ul>
+ * <li>{@code name}: Cannot be unset. Attempting to set it to an empty string ("") or a string
+ * that is empty after trim is rejected as bad request.
+ * <li>{@code description}: Can be unset by setting an empty string ("") for it.
+ * <li>{@code url}: Can be unset by setting an empty string ("") for it.
+ * <li>{@code repository}: Cannot be unset. Attempting to set it to an empty string ("") or a
+ * string that is empty after trim is rejected as bad request.
+ * </ul>
+ *
+ * @param input input with updated properties
+ * @return updated checker info
+ */
+ CheckerInfo update(CheckerInput input) throws RestApiException;
+
+ /**
+ * A default implementation which allows source compatibility when adding new methods to the
+ * interface.
+ */
+ class NotImplemented implements CheckerApi {
+ @Override
+ public CheckerInfo get() throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ public CheckerInfo update(CheckerInput input) throws RestApiException {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/api/CheckerApiImpl.java b/java/com/google/gerrit/plugins/checks/api/CheckerApiImpl.java
new file mode 100644
index 0000000..db74c00
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/api/CheckerApiImpl.java
@@ -0,0 +1,58 @@
+// 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.google.gerrit.plugins.checks.api;
+
+import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
+
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+class CheckerApiImpl implements CheckerApi {
+ interface Factory {
+ CheckerApiImpl create(CheckerResource rsrc);
+ }
+
+ private final GetChecker getChecker;
+ private final UpdateChecker updateChecker;
+ private final CheckerResource rsrc;
+
+ @Inject
+ CheckerApiImpl(
+ GetChecker getChecker, UpdateChecker updateChecker, @Assisted CheckerResource rsrc) {
+ this.getChecker = getChecker;
+ this.updateChecker = updateChecker;
+
+ this.rsrc = rsrc;
+ }
+
+ @Override
+ public CheckerInfo get() throws RestApiException {
+ try {
+ return getChecker.apply(rsrc);
+ } catch (Exception e) {
+ throw asRestApiException("Cannot retrieve checker", e);
+ }
+ }
+
+ @Override
+ public CheckerInfo update(CheckerInput input) throws RestApiException {
+ try {
+ return updateChecker.apply(rsrc, input);
+ } catch (Exception e) {
+ throw asRestApiException("Cannot update checker", e);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/api/CheckerInfo.java b/java/com/google/gerrit/plugins/checks/api/CheckerInfo.java
new file mode 100644
index 0000000..7dfee5a
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/api/CheckerInfo.java
@@ -0,0 +1,65 @@
+// 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.google.gerrit.plugins.checks.api;
+
+import com.google.common.base.MoreObjects;
+import java.sql.Timestamp;
+import java.util.Objects;
+
+public class CheckerInfo {
+ public String uuid;
+ public String name;
+ public String description;
+ public String url;
+ public String repository;
+ public CheckerStatus status;
+ public Timestamp createdOn;
+ public Timestamp updatedOn;
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(uuid, name, description, url, repository, status, createdOn, updatedOn);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof CheckerInfo)) {
+ return false;
+ }
+ CheckerInfo o = (CheckerInfo) obj;
+ return Objects.equals(uuid, o.uuid)
+ && Objects.equals(name, o.name)
+ && Objects.equals(description, o.description)
+ && Objects.equals(url, o.url)
+ && Objects.equals(repository, o.repository)
+ && Objects.equals(status, o.status)
+ && Objects.equals(createdOn, o.createdOn)
+ && Objects.equals(updatedOn, o.updatedOn);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("uuid", uuid)
+ .add("name", name)
+ .add("description", description)
+ .add("repository", repository)
+ .add("url", url)
+ .add("status", status)
+ .add("createdOn", createdOn)
+ .add("updatedOn", updatedOn)
+ .toString();
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/api/CheckerInput.java b/java/com/google/gerrit/plugins/checks/api/CheckerInput.java
new file mode 100644
index 0000000..c0b20c4
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/api/CheckerInput.java
@@ -0,0 +1,25 @@
+// 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.google.gerrit.plugins.checks.api;
+
+import com.google.gerrit.extensions.restapi.DefaultInput;
+
+public class CheckerInput {
+ @DefaultInput public String name;
+ public String description;
+ public String url;
+ public String repository;
+ public CheckerStatus status;
+}
diff --git a/java/com/google/gerrit/plugins/checks/api/CheckerResource.java b/java/com/google/gerrit/plugins/checks/api/CheckerResource.java
new file mode 100644
index 0000000..da62429
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/api/CheckerResource.java
@@ -0,0 +1,35 @@
+// 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.google.gerrit.plugins.checks.api;
+
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.plugins.checks.Checker;
+import com.google.inject.TypeLiteral;
+
+public class CheckerResource implements RestResource {
+ public static final TypeLiteral<RestView<CheckerResource>> CHECKER_KIND =
+ new TypeLiteral<RestView<CheckerResource>>() {};
+
+ private final Checker checker;
+
+ public CheckerResource(Checker checker) {
+ this.checker = checker;
+ }
+
+ public Checker getChecker() {
+ return checker;
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/api/CheckerStatus.java b/java/com/google/gerrit/plugins/checks/api/CheckerStatus.java
new file mode 100644
index 0000000..3a8f040
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/api/CheckerStatus.java
@@ -0,0 +1,33 @@
+// 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.google.gerrit.plugins.checks.api;
+
+/**
+ * Status of a configured checker.
+ *
+ * <p>This status is a property of the checker's configuration; not to be confused with {@code
+ * CheckState}, which is the state of an individual check performed by a checker against a specific
+ * change.
+ */
+public enum CheckerStatus {
+ /** The checker is enabled. */
+ ENABLED,
+
+ /**
+ * The checker is disabled, meaning its checks are not displayed alongside any changes, and the
+ * results are not considered when determining submit requirements.
+ */
+ DISABLED
+}
diff --git a/java/com/google/gerrit/plugins/checks/api/Checkers.java b/java/com/google/gerrit/plugins/checks/api/Checkers.java
new file mode 100644
index 0000000..b8a56c1
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/api/Checkers.java
@@ -0,0 +1,62 @@
+// 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.google.gerrit.plugins.checks.api;
+
+import com.google.gerrit.extensions.restapi.NotImplementedException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import java.util.List;
+
+public interface Checkers {
+ /**
+ * Look up a checker by ID.
+ *
+ * <p><strong>Note:</strong> This method eagerly reads the checker. Methods that mutate the
+ * checker do not necessarily re-read the checker. Therefore, calling a getter method on an
+ * instance after calling a mutation method on that same instance is not guaranteed to reflect the
+ * mutation. It is not recommended to store references to {@code checkerApi} instances.
+ *
+ * @param id any identifier supported by the REST API, including checker UUID.
+ * @return API for accessing the checker.
+ * @throws RestApiException if an error occurred.
+ */
+ CheckerApi id(String id) throws RestApiException;
+
+ /** Create a new checker. */
+ CheckerApi create(CheckerInput input) throws RestApiException;
+
+ /** Returns a list of all checkers, sorted by UUID. */
+ List<CheckerInfo> all() throws RestApiException;
+
+ /**
+ * A default implementation which allows source compatibility when adding new methods to the
+ * interface.
+ */
+ class NotImplemented implements Checkers {
+ @Override
+ public CheckerApi id(String id) throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ public CheckerApi create(CheckerInput input) throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ public List<CheckerInfo> all() throws RestApiException {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/api/CheckersCollection.java b/java/com/google/gerrit/plugins/checks/api/CheckersCollection.java
new file mode 100644
index 0000000..aa22d90
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/api/CheckersCollection.java
@@ -0,0 +1,90 @@
+// 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.google.gerrit.plugins.checks.api;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestCollection;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.plugins.checks.AdministrateCheckersPermission;
+import com.google.gerrit.plugins.checks.Checker;
+import com.google.gerrit.plugins.checks.Checkers;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+@Singleton
+public class CheckersCollection implements RestCollection<TopLevelResource, CheckerResource> {
+ private final Provider<CurrentUser> self;
+ private final PermissionBackend permissionBackend;
+ private final ListCheckers listCheckers;
+ private final Checkers checkers;
+ private final DynamicMap<RestView<CheckerResource>> views;
+ private final AdministrateCheckersPermission permission;
+
+ @Inject
+ public CheckersCollection(
+ Provider<CurrentUser> self,
+ PermissionBackend permissionBackend,
+ ListCheckers listCheckers,
+ Checkers checkers,
+ DynamicMap<RestView<CheckerResource>> views,
+ AdministrateCheckersPermission permission) {
+ this.self = self;
+ this.permissionBackend = permissionBackend;
+ this.listCheckers = listCheckers;
+ this.checkers = checkers;
+ this.views = views;
+ this.permission = permission;
+ }
+
+ @Override
+ public RestView<TopLevelResource> list() throws RestApiException {
+ return listCheckers;
+ }
+
+ @Override
+ public CheckerResource parse(TopLevelResource parent, IdString id)
+ throws AuthException, ResourceNotFoundException, PermissionBackendException, IOException,
+ ConfigInvalidException {
+ CurrentUser user = self.get();
+ if (user instanceof AnonymousUser) {
+ throw new AuthException("Authentication required");
+ } else if (!(user.isIdentifiedUser())) {
+ throw new ResourceNotFoundException(id);
+ }
+
+ permissionBackend.currentUser().check(permission);
+
+ Checker checker =
+ checkers.getChecker(id.get()).orElseThrow(() -> new ResourceNotFoundException(id));
+ return new CheckerResource(checker);
+ }
+
+ @Override
+ public DynamicMap<RestView<CheckerResource>> views() {
+ return views;
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/api/CheckersImpl.java b/java/com/google/gerrit/plugins/checks/api/CheckersImpl.java
new file mode 100644
index 0000000..73e0f5d
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/api/CheckersImpl.java
@@ -0,0 +1,72 @@
+// 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.google.gerrit.plugins.checks.api;
+
+import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
+
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.List;
+
+@Singleton
+public class CheckersImpl implements Checkers {
+ private final CheckerApiImpl.Factory api;
+ private final CreateChecker createChecker;
+ private final ListCheckers listCheckers;
+ private final CheckersCollection checkers;
+
+ @Inject
+ CheckersImpl(
+ CheckerApiImpl.Factory api,
+ CreateChecker createChecker,
+ ListCheckers listCheckers,
+ CheckersCollection checkers) {
+ this.api = api;
+ this.createChecker = createChecker;
+ this.listCheckers = listCheckers;
+ this.checkers = checkers;
+ }
+
+ @Override
+ public CheckerApi id(String id) throws RestApiException {
+ try {
+ return api.create(checkers.parse(TopLevelResource.INSTANCE, IdString.fromDecoded(id)));
+ } catch (Exception e) {
+ throw asRestApiException("Cannot retrieve checker " + id, e);
+ }
+ }
+
+ @Override
+ public CheckerApi create(CheckerInput input) throws RestApiException {
+ try {
+ CheckerInfo info = createChecker.apply(TopLevelResource.INSTANCE, input).value();
+ return id(info.uuid);
+ } catch (Exception e) {
+ throw asRestApiException("Cannot create checker " + input.name, e);
+ }
+ }
+
+ @Override
+ public List<CheckerInfo> all() throws RestApiException {
+ try {
+ return listCheckers.apply(TopLevelResource.INSTANCE);
+ } catch (Exception e) {
+ throw asRestApiException("Cannot list all checkers ", e);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/api/CheckersRestApiServlet.java b/java/com/google/gerrit/plugins/checks/api/CheckersRestApiServlet.java
new file mode 100644
index 0000000..efda053
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/api/CheckersRestApiServlet.java
@@ -0,0 +1,94 @@
+// 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.google.gerrit.plugins.checks.api;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.gerrit.httpd.restapi.RestApiServlet;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+public class CheckersRestApiServlet extends RestApiServlet {
+ private static final long serialVersionUID = 1L;
+
+ @Inject
+ CheckersRestApiServlet(RestApiServlet.Globals globals, Provider<CheckersCollection> checkers) {
+ super(globals, checkers);
+ }
+
+ // TODO(dborowitz): Consider making
+ // RestApiServlet#service(HttpServletRequest, HttpServletResponse) non-final of overriding the
+ // non-HTTP overload.
+ @Override
+ public void service(ServletRequest servletRequest, ServletResponse servletResponse)
+ throws ServletException, IOException {
+ // This is...unfortunate. HttpPluginServlet (and/or ContextMapper) doesn't properly set the
+ // servlet path on the wrapped request. Based on what RestApiServlet produces for non-plugin
+ // requests, it should be:
+ // contextPath = "/plugins/checks"
+ // servletPath = "/checkers/"
+ // pathInfo = checkerUuid
+ // Instead it does:
+ // contextPath = "/plugins/checks"
+ // servletPath = ""
+ // pathInfo = "/checkers/" + checkerUuid
+ // This results in RestApiServlet splitting the pathInfo into ["", "checkers", checkerUuid], and
+ // it passes the "" to CheckersCollection#parse, which understandably, but unfortunately, fails.
+ //
+ // This frankly seems like a bug that should be fixed, but it would quite likely break existing
+ // plugins in confusing ways. So, we work around it by introducing our own request wrapper with
+ // the correct paths.
+ HttpServletRequest req = (HttpServletRequest) servletRequest;
+
+ String pathInfo = req.getPathInfo();
+ String correctServletPath = "/checkers/";
+
+ // Ensure actual request object matches the format explained above.
+ checkState(
+ req.getContextPath().endsWith("/checks"),
+ "unexpected context path: %s",
+ req.getContextPath());
+ checkState(req.getServletPath().isEmpty(), "unexpected servlet path: %s", req.getServletPath());
+ checkState(
+ req.getPathInfo().startsWith(correctServletPath),
+ "unexpected servlet path: %s",
+ req.getServletPath());
+
+ String fixedPathInfo = pathInfo.substring(correctServletPath.length());
+ HttpServletRequestWrapper wrapped =
+ new HttpServletRequestWrapper(req) {
+ @Override
+ public String getServletPath() {
+ return correctServletPath;
+ }
+
+ @Override
+ public String getPathInfo() {
+ return fixedPathInfo;
+ }
+ };
+
+ super.service(wrapped, (HttpServletResponse) servletResponse);
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/api/CreateChecker.java b/java/com/google/gerrit/plugins/checks/api/CreateChecker.java
new file mode 100644
index 0000000..16a22f8
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/api/CreateChecker.java
@@ -0,0 +1,120 @@
+// 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.google.gerrit.plugins.checks.api;
+
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestCollectionModifyView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.plugins.checks.AdministrateCheckersPermission;
+import com.google.gerrit.plugins.checks.Checker;
+import com.google.gerrit.plugins.checks.CheckerCreation;
+import com.google.gerrit.plugins.checks.CheckerJson;
+import com.google.gerrit.plugins.checks.CheckerName;
+import com.google.gerrit.plugins.checks.CheckerUpdate;
+import com.google.gerrit.plugins.checks.CheckerUrl;
+import com.google.gerrit.plugins.checks.CheckerUuid;
+import com.google.gerrit.plugins.checks.CheckersUpdate;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.UserInitiated;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+@Singleton
+public class CreateChecker
+ implements RestCollectionModifyView<TopLevelResource, CheckerResource, CheckerInput> {
+ private final PermissionBackend permissionBackend;
+ private final Provider<CheckersUpdate> checkersUpdate;
+ private final CheckerJson checkerJson;
+ private final AdministrateCheckersPermission permission;
+ private final ProjectCache projectCache;
+
+ @Inject
+ public CreateChecker(
+ PermissionBackend permissionBackend,
+ @UserInitiated Provider<CheckersUpdate> checkersUpdate,
+ CheckerJson checkerJson,
+ AdministrateCheckersPermission permission,
+ ProjectCache projectCache) {
+ this.permissionBackend = permissionBackend;
+ this.checkersUpdate = checkersUpdate;
+ this.checkerJson = checkerJson;
+ this.permission = permission;
+ this.projectCache = projectCache;
+ }
+
+ @Override
+ public Response<CheckerInfo> apply(TopLevelResource parentResource, CheckerInput input)
+ throws RestApiException, PermissionBackendException, IOException, ConfigInvalidException,
+ OrmDuplicateKeyException {
+ permissionBackend.currentUser().check(permission);
+
+ if (input == null) {
+ input = new CheckerInput();
+ }
+
+ String name = CheckerName.clean(input.name);
+ if (name.isEmpty()) {
+ throw new BadRequestException("name is required");
+ }
+ Project.NameKey repository = resolveRepository(input.repository);
+
+ String checkerUuid = CheckerUuid.make(name);
+ CheckerCreation.Builder checkerCreationBuilder =
+ CheckerCreation.builder()
+ .setCheckerUuid(checkerUuid)
+ .setName(name)
+ .setRepository(repository);
+ CheckerUpdate.Builder checkerUpdateBuilder = CheckerUpdate.builder();
+ if (input.description != null && !input.description.trim().isEmpty()) {
+ checkerUpdateBuilder.setDescription(input.description.trim());
+ }
+ if (input.url != null) {
+ checkerUpdateBuilder.setUrl(CheckerUrl.clean(input.url));
+ }
+ if (input.status != null) {
+ checkerUpdateBuilder.setStatus(input.status);
+ }
+ Checker checker =
+ checkersUpdate
+ .get()
+ .createChecker(checkerCreationBuilder.build(), checkerUpdateBuilder.build());
+ return Response.created(checkerJson.format(checker));
+ }
+
+ private Project.NameKey resolveRepository(String repository)
+ throws BadRequestException, UnprocessableEntityException, IOException {
+ if (repository == null || repository.trim().isEmpty()) {
+ throw new BadRequestException("repository is required");
+ }
+
+ ProjectState projectState = projectCache.checkedGet(new Project.NameKey(repository.trim()));
+ if (projectState == null) {
+ throw new UnprocessableEntityException(String.format("repository %s not found", repository));
+ }
+
+ return projectState.getNameKey();
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/api/GetChecker.java b/java/com/google/gerrit/plugins/checks/api/GetChecker.java
new file mode 100644
index 0000000..9a84a91
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/api/GetChecker.java
@@ -0,0 +1,35 @@
+// 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.google.gerrit.plugins.checks.api;
+
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.plugins.checks.CheckerJson;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+public class GetChecker implements RestReadView<CheckerResource> {
+ private final CheckerJson checkerJson;
+
+ @Inject
+ public GetChecker(CheckerJson checkerJson) {
+ this.checkerJson = checkerJson;
+ }
+
+ @Override
+ public CheckerInfo apply(CheckerResource resource) {
+ return checkerJson.format(resource.getChecker());
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/api/HttpModule.java b/java/com/google/gerrit/plugins/checks/api/HttpModule.java
new file mode 100644
index 0000000..bd5c447
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/api/HttpModule.java
@@ -0,0 +1,52 @@
+// 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.google.gerrit.plugins.checks.api;
+
+import static com.google.gerrit.plugins.checks.api.CheckerResource.CHECKER_KIND;
+
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.RestApiModule;
+import com.google.gerrit.httpd.plugins.HttpPluginModule;
+
+public class HttpModule extends HttpPluginModule {
+ @Override
+ protected void configureServlets() {
+ bind(CheckersCollection.class);
+
+ bind(Checkers.class).to(CheckersImpl.class);
+
+ serveRegex("^/checkers/(.*)$").with(CheckersRestApiServlet.class);
+
+ install(
+ new RestApiModule() {
+ @Override
+ public void configure() {
+ DynamicMap.mapOf(binder(), CHECKER_KIND);
+ postOnCollection(CHECKER_KIND).to(CreateChecker.class);
+ get(CHECKER_KIND).to(GetChecker.class);
+ post(CHECKER_KIND).to(UpdateChecker.class);
+ }
+ });
+
+ install(
+ new FactoryModule() {
+ @Override
+ public void configure() {
+ factory(CheckerApiImpl.Factory.class);
+ }
+ });
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/api/ListCheckers.java b/java/com/google/gerrit/plugins/checks/api/ListCheckers.java
new file mode 100644
index 0000000..a912d6e
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/api/ListCheckers.java
@@ -0,0 +1,58 @@
+// 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.google.gerrit.plugins.checks.api;
+
+import static java.util.stream.Collectors.toList;
+
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.plugins.checks.AdministrateCheckersPermission;
+import com.google.gerrit.plugins.checks.CheckerJson;
+import com.google.gerrit.plugins.checks.Checkers;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.List;
+
+@Singleton
+public class ListCheckers implements RestReadView<TopLevelResource> {
+ private final PermissionBackend permissionBackend;
+ private final Checkers checkers;
+ private final CheckerJson checkerJson;
+ private final AdministrateCheckersPermission permission;
+
+ @Inject
+ public ListCheckers(
+ PermissionBackend permissionBackend,
+ Checkers checkers,
+ CheckerJson checkerJson,
+ AdministrateCheckersPermission permission) {
+ this.permissionBackend = permissionBackend;
+ this.checkers = checkers;
+ this.checkerJson = checkerJson;
+ this.permission = permission;
+ }
+
+ @Override
+ public List<CheckerInfo> apply(TopLevelResource resource)
+ throws RestApiException, PermissionBackendException, IOException {
+ permissionBackend.currentUser().check(permission);
+
+ return checkers.listCheckers().stream().map(checkerJson::format).collect(toList());
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/api/UpdateChecker.java b/java/com/google/gerrit/plugins/checks/api/UpdateChecker.java
new file mode 100644
index 0000000..e1f2245
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/api/UpdateChecker.java
@@ -0,0 +1,118 @@
+// 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.google.gerrit.plugins.checks.api;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.plugins.checks.AdministrateCheckersPermission;
+import com.google.gerrit.plugins.checks.Checker;
+import com.google.gerrit.plugins.checks.CheckerJson;
+import com.google.gerrit.plugins.checks.CheckerName;
+import com.google.gerrit.plugins.checks.CheckerUpdate;
+import com.google.gerrit.plugins.checks.CheckerUrl;
+import com.google.gerrit.plugins.checks.CheckersUpdate;
+import com.google.gerrit.plugins.checks.NoSuchCheckerException;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.UserInitiated;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.io.IOException;
+import javax.inject.Singleton;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+@Singleton
+public class UpdateChecker implements RestModifyView<CheckerResource, CheckerInput> {
+ private final PermissionBackend permissionBackend;
+ private final Provider<CheckersUpdate> checkersUpdate;
+ private final CheckerJson checkerJson;
+ private final ProjectCache projectCache;
+
+ private final AdministrateCheckersPermission permission;
+
+ @Inject
+ public UpdateChecker(
+ PermissionBackend permissionBackend,
+ @UserInitiated Provider<CheckersUpdate> checkersUpdate,
+ CheckerJson checkerJson,
+ AdministrateCheckersPermission permission,
+ ProjectCache projectCache) {
+ this.permissionBackend = permissionBackend;
+ this.checkersUpdate = checkersUpdate;
+ this.checkerJson = checkerJson;
+ this.permission = permission;
+ this.projectCache = projectCache;
+ }
+
+ @Override
+ public CheckerInfo apply(CheckerResource resource, CheckerInput input)
+ throws RestApiException, PermissionBackendException, NoSuchCheckerException, IOException,
+ ConfigInvalidException {
+ permissionBackend.currentUser().check(permission);
+
+ CheckerUpdate.Builder checkerUpdateBuilder = CheckerUpdate.builder();
+
+ if (input.name != null) {
+ String newName = CheckerName.clean(input.name);
+ if (newName.isEmpty()) {
+ throw new BadRequestException("name cannot be unset");
+ }
+ checkerUpdateBuilder.setName(newName);
+ }
+
+ if (input.description != null) {
+ checkerUpdateBuilder.setDescription(Strings.nullToEmpty(input.description).trim());
+ }
+
+ if (input.url != null) {
+ checkerUpdateBuilder.setUrl(CheckerUrl.clean(input.url));
+ }
+
+ if (input.repository != null) {
+ Project.NameKey repository = resolveRepository(input.repository);
+ checkerUpdateBuilder.setRepository(repository);
+ }
+
+ if (input.status != null) {
+ checkerUpdateBuilder.setStatus(input.status);
+ }
+
+ Checker updatedChecker =
+ checkersUpdate
+ .get()
+ .updateChecker(resource.getChecker().getUuid(), checkerUpdateBuilder.build());
+ return checkerJson.format(updatedChecker);
+ }
+
+ private Project.NameKey resolveRepository(String repository)
+ throws BadRequestException, UnprocessableEntityException, IOException {
+ if (repository == null || repository.trim().isEmpty()) {
+ throw new BadRequestException("repository cannot be unset");
+ }
+
+ ProjectState projectState = projectCache.checkedGet(new Project.NameKey(repository.trim()));
+ if (projectState == null) {
+ throw new UnprocessableEntityException(String.format("repository %s not found", repository));
+ }
+
+ return projectState.getNameKey();
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/db/CheckerConfig.java b/java/com/google/gerrit/plugins/checks/db/CheckerConfig.java
new file mode 100644
index 0000000..cd5c99e
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/db/CheckerConfig.java
@@ -0,0 +1,366 @@
+// 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.google.gerrit.plugins.checks.db;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+import com.google.gerrit.plugins.checks.Checker;
+import com.google.gerrit.plugins.checks.CheckerCreation;
+import com.google.gerrit.plugins.checks.CheckerName;
+import com.google.gerrit.plugins.checks.CheckerRef;
+import com.google.gerrit.plugins.checks.CheckerUpdate;
+import com.google.gerrit.plugins.checks.CheckerUuid;
+import com.google.gerrit.plugins.checks.Checkers;
+import com.google.gerrit.plugins.checks.CheckersUpdate;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.git.meta.VersionedMetaData;
+import com.google.gerrit.server.util.time.TimeUtil;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.Arrays;
+import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevSort;
+
+/**
+ * A representation of a checker in NoteDb.
+ *
+ * <p>Checkers in NoteDb can be created by following the descriptions of {@link
+ * #createForNewChecker(Project.NameKey, Repository, CheckerCreation)}. For reading checkers from
+ * NoteDb or updating them, refer to {@link #loadForChecker(Project.NameKey, Repository, String)}.
+ *
+ * <p><strong>Note:</strong> Any modification (checker creation or update) only becomes permanent
+ * (and hence written to NoteDb) if {@link #commit(MetaDataUpdate)} is called.
+ *
+ * <p><strong>Warning:</strong> This class is a low-level API for checkers in NoteDb. Most code
+ * which deals with checkers should use {@link Checkers} or {@link CheckersUpdate} instead.
+ *
+ * <h2>Internal details</h2>
+ *
+ * <p>Each checker is represented by a commit on a branch as defined by {@link
+ * CheckerRef#refsCheckers(String)}. Previous versions of the checker exist as older commits on the
+ * same branch and can be reached by following along the parent references. New commits for updates
+ * are only created if a real modification occurs.
+ *
+ * <p>Within each commit, the properties of a checker are stored in <em>checker.config</em> file
+ * (further specified by {@link CheckerConfigEntry}). The <em>checker.config</em> file is formatted
+ * as a JGit {@link Config} file.
+ */
+@VisibleForTesting
+public class CheckerConfig extends VersionedMetaData {
+ @VisibleForTesting public static final String CHECKER_CONFIG_FILE = "checker.config";
+
+ /**
+ * Creates a {@code CheckerConfig} for a new checker from the {@code CheckerCreation} blueprint.
+ * Further, optional properties can be specified by setting an {@code CheckerUpdate} via {@link
+ * #setCheckerUpdate(CheckerUpdate)} on the returned {@code CheckerConfig}.
+ *
+ * <p><strong>Note:</strong> The returned {@code CheckerConfig} has to be committed via {@link
+ * #commit(MetaDataUpdate)} in order to create the checker for real.
+ *
+ * @param projectName the name of the project which holds the NoteDb commits for checkers
+ * @param repository the repository which holds the NoteDb commits for checkers
+ * @param checkerCreation a {@code CheckerCreation} specifying all properties which are required
+ * for a new checker
+ * @return a {@code CheckerConfig} for a checker creation
+ * @throws IOException if the repository can't be accessed for some reason
+ * @throws ConfigInvalidException if a checker with the same UUID already exists but can't be read
+ * due to an invalid format
+ * @throws OrmDuplicateKeyException if a checker with the same UUID already exists
+ */
+ public static CheckerConfig createForNewChecker(
+ Project.NameKey projectName, Repository repository, CheckerCreation checkerCreation)
+ throws IOException, ConfigInvalidException, OrmDuplicateKeyException {
+ CheckerConfig checkerConfig = new CheckerConfig(checkerCreation.getCheckerUuid());
+ checkerConfig.load(projectName, repository);
+ checkerConfig.setCheckerCreation(checkerCreation);
+ return checkerConfig;
+ }
+
+ /**
+ * Creates a {@code CheckerConfig} for an existing checker.
+ *
+ * <p>The checker is automatically loaded within this method and can be accessed via {@link
+ * #getLoadedChecker()}.
+ *
+ * <p>It's safe to call this method for non-existing checkers. In that case, {@link
+ * #getLoadedChecker()} won't return any checker. Thus, the existence of a checker can be easily
+ * tested.
+ *
+ * <p>The checker represented by the returned {@code CheckerConfig} can be updated by setting an
+ * {@code CheckerUpdate} via {@link #setCheckerUpdate(CheckerUpdate)} and committing the {@code
+ * CheckerConfig} via {@link #commit(MetaDataUpdate)}.
+ *
+ * @param projectName the name of the project which holds the NoteDb commits for checkers
+ * @param repository the repository which holds the NoteDb commits for checkers
+ * @param checkerUuid the UUID of the checker
+ * @return a {@code CheckerConfig} for the checker with the specified UUID
+ * @throws IOException if the repository can't be accessed for some reason
+ * @throws ConfigInvalidException if the checker exists but can't be read due to an invalid format
+ */
+ public static CheckerConfig loadForChecker(
+ Project.NameKey projectName, Repository repository, String checkerUuid)
+ throws IOException, ConfigInvalidException {
+ CheckerConfig checkerConfig = new CheckerConfig(checkerUuid);
+ checkerConfig.load(projectName, repository);
+ return checkerConfig;
+ }
+
+ private final String checkerUuid;
+ private final String ref;
+
+ private Optional<Checker> loadedChecker = Optional.empty();
+ private Optional<CheckerCreation> checkerCreation = Optional.empty();
+ private Optional<CheckerUpdate> checkerUpdate = Optional.empty();
+ private Optional<Checker.Builder> updatedCheckerBuilder = Optional.empty();
+ private Config config;
+ private boolean isLoaded = false;
+
+ private CheckerConfig(String checkerUuid) {
+ this.checkerUuid = CheckerUuid.checkUuid(checkerUuid);
+ this.ref = CheckerRef.refsCheckers(checkerUuid);
+ }
+
+ /**
+ * Returns the checker loaded from NoteDb.
+ *
+ * <p>If not any NoteDb commits exist for the checker represented by this {@code CheckerConfig},
+ * no checker is returned.
+ *
+ * <p>After {@link #commit(MetaDataUpdate)} was called on this {@code CheckerConfig}, this method
+ * returns a checker which is in line with the latest NoteDb commit for this checker. So, after
+ * creating a {@code CheckerConfig} for a new checker and committing it, this method can be used
+ * to retrieve a representation of the created checker. The same holds for the representation of
+ * an updated checker.
+ *
+ * @return the loaded checker, or an empty {@code Optional} if the checker doesn't exist
+ */
+ public Optional<Checker> getLoadedChecker() {
+ checkLoaded();
+
+ if (updatedCheckerBuilder.isPresent()) {
+ // There have been updates to the checker that have not been applied to the loaded checker
+ // yet, apply them now. This has to be done here because in the onSave(CommitBuilder) method
+ // where the checker updates are committed we do not know the new SHA1 for the ref state
+ // yet.
+ loadedChecker = Optional.of(updatedCheckerBuilder.get().setRefState(revision).build());
+ updatedCheckerBuilder = Optional.empty();
+ }
+
+ return loadedChecker;
+ }
+
+ /**
+ * Specifies how the current checker should be updated.
+ *
+ * <p>If the checker is newly created, the {@code CheckerUpdate} can be used to specify optional
+ * properties.
+ *
+ * <p><strong>Note:</strong> This method doesn't perform the update. It only contains the
+ * instructions for the update. To apply the update for real and write the result back to NoteDb,
+ * call {@link #commit(MetaDataUpdate)} on this {@code CheckerConfig}.
+ *
+ * @param checkerUpdate an {@code CheckerUpdate} outlining the modifications which should be
+ * applied
+ */
+ public void setCheckerUpdate(CheckerUpdate checkerUpdate) {
+ this.checkerUpdate = Optional.of(checkerUpdate);
+ }
+
+ private void setCheckerCreation(CheckerCreation checkerCreation) throws OrmDuplicateKeyException {
+ checkLoaded();
+ if (loadedChecker.isPresent()) {
+ throw new OrmDuplicateKeyException(String.format("Checker %s already exists", checkerUuid));
+ }
+
+ this.checkerCreation = Optional.of(checkerCreation);
+ }
+
+ @Override
+ protected String getRefName() {
+ return ref;
+ }
+
+ @VisibleForTesting
+ public Config getConfigForTesting() {
+ return config;
+ }
+
+ @Override
+ protected void onLoad() throws IOException, ConfigInvalidException {
+ if (revision != null) {
+ rw.reset();
+ rw.markStart(revision);
+ rw.sort(RevSort.REVERSE);
+ RevCommit earliestCommit = rw.next();
+ Timestamp createdOn = new Timestamp(earliestCommit.getCommitTime() * 1000L);
+ Timestamp updatedOn = new Timestamp(rw.parseCommit(revision).getCommitTime() * 1000L);
+
+ config = readConfig(CHECKER_CONFIG_FILE);
+ loadedChecker =
+ Optional.of(createFrom(checkerUuid, config, createdOn, updatedOn, revision.toObjectId()));
+ }
+
+ isLoaded = true;
+ }
+
+ @Override
+ protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException {
+ checkLoaded();
+ if (!checkerCreation.isPresent() && !checkerUpdate.isPresent()) {
+ // Checker was neither created nor changed. -> A new commit isn't necessary.
+ return false;
+ }
+
+ ensureThatMandatoryPropertiesAreSet();
+
+ // Commit timestamps are internally truncated to seconds. To return the correct 'createdOn' time
+ // for new checkers, we explicitly need to truncate the timestamp here.
+ Timestamp commitTimestamp =
+ TimeUtil.truncateToSecond(
+ checkerUpdate.flatMap(CheckerUpdate::getUpdatedOn).orElseGet(TimeUtil::nowTs));
+ commit.setAuthor(new PersonIdent(commit.getAuthor(), commitTimestamp));
+ commit.setCommitter(new PersonIdent(commit.getCommitter(), commitTimestamp));
+
+ updatedCheckerBuilder = Optional.of(updateChecker(commitTimestamp));
+
+ String commitMessage = createCommitMessage(loadedChecker, checkerUpdate);
+ commit.setMessage(commitMessage);
+
+ checkerCreation = Optional.empty();
+ checkerUpdate = Optional.empty();
+ return true;
+ }
+
+ private void ensureThatMandatoryPropertiesAreSet() throws ConfigInvalidException {
+ if (getNewName().equals(Optional.of(""))) {
+ throw new ConfigInvalidException(
+ String.format("Name of the checker %s must be defined", checkerUuid));
+ }
+
+ if (getNewRepository().equals(Optional.of(""))) {
+ throw new ConfigInvalidException(
+ String.format("Repository of the checker %s must be defined", checkerUuid));
+ }
+ }
+
+ private void checkLoaded() {
+ checkState(isLoaded, "Checker %s not loaded yet", checkerUuid);
+ }
+
+ private Optional<String> getNewName() {
+ if (checkerUpdate.isPresent()) {
+ return checkerUpdate.get().getName().map(CheckerName::clean);
+ }
+ if (checkerCreation.isPresent()) {
+ return Optional.of(CheckerName.clean(checkerCreation.get().getName()));
+ }
+ return Optional.empty();
+ }
+
+ private Optional<String> getNewRepository() {
+ if (checkerUpdate.isPresent()) {
+ return checkerUpdate
+ .get()
+ .getRepository()
+ .map(Project.NameKey::get)
+ .map(Strings::nullToEmpty)
+ .map(String::trim);
+ }
+ if (checkerCreation.isPresent()) {
+ return Optional.of(Strings.nullToEmpty(checkerCreation.get().getRepository().get()).trim());
+ }
+ return Optional.empty();
+ }
+
+ private Checker.Builder updateChecker(Timestamp commitTimestamp)
+ throws IOException, ConfigInvalidException {
+ Config config = updateCheckerProperties();
+ Timestamp createdOn = loadedChecker.map(Checker::getCreatedOn).orElse(commitTimestamp);
+ return createBuilderFrom(checkerUuid, config, createdOn, commitTimestamp);
+ }
+
+ private Config updateCheckerProperties() throws IOException, ConfigInvalidException {
+ Config config = readConfig(CHECKER_CONFIG_FILE);
+ checkerCreation.ifPresent(
+ checkerCreation ->
+ Arrays.stream(CheckerConfigEntry.values())
+ .forEach(configEntry -> configEntry.initNewConfig(config, checkerCreation)));
+ checkerUpdate.ifPresent(
+ checkerUpdate ->
+ Arrays.stream(CheckerConfigEntry.values())
+ .forEach(configEntry -> configEntry.updateConfigValue(config, checkerUpdate)));
+ saveConfig(CHECKER_CONFIG_FILE, config);
+ return config;
+ }
+
+ private static Checker.Builder createBuilderFrom(
+ String checkerUuid, Config config, Timestamp createdOn, Timestamp updatedOn)
+ throws ConfigInvalidException {
+ Checker.Builder checker = Checker.builder(checkerUuid);
+ for (CheckerConfigEntry configEntry : CheckerConfigEntry.values()) {
+ configEntry.readFromConfig(checkerUuid, checker, config);
+ }
+ checker.setCreatedOn(createdOn).setUpdatedOn(updatedOn);
+ return checker;
+ }
+
+ private static Checker createFrom(
+ String checkerUuid,
+ Config config,
+ Timestamp createdOn,
+ Timestamp updatedOn,
+ ObjectId refState)
+ throws ConfigInvalidException {
+ return createBuilderFrom(checkerUuid, config, createdOn, updatedOn)
+ .setRefState(refState)
+ .build();
+ }
+
+ private static String createCommitMessage(
+ Optional<Checker> originalChecker, Optional<CheckerUpdate> checkerUpdate) {
+ Optional<String> newCheckerName = checkerUpdate.flatMap(CheckerUpdate::getName);
+ String summaryLine = originalChecker.isPresent() ? "Update checker" : "Create checker";
+ Optional<String> footerForRename = getFooterForRename(originalChecker, newCheckerName);
+ if (footerForRename.isPresent()) {
+ return summaryLine + "\n\n" + footerForRename.get();
+ }
+ return summaryLine;
+ }
+
+ private static Optional<String> getFooterForRename(
+ Optional<Checker> originalChecker, Optional<String> newCheckerName) {
+ if (!originalChecker.isPresent() || !newCheckerName.isPresent()) {
+ return Optional.empty();
+ }
+
+ String originalName = originalChecker.get().getName();
+ String newName = newCheckerName.get();
+ if (originalName.equals(newName)) {
+ return Optional.empty();
+ }
+ return Optional.of("Rename from " + originalName + " to " + newName);
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/db/CheckerConfigEntry.java b/java/com/google/gerrit/plugins/checks/db/CheckerConfigEntry.java
new file mode 100644
index 0000000..30b2a2b
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/db/CheckerConfigEntry.java
@@ -0,0 +1,236 @@
+// 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.google.gerrit.plugins.checks.db;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.plugins.checks.Checker;
+import com.google.gerrit.plugins.checks.CheckerCreation;
+import com.google.gerrit.plugins.checks.CheckerUpdate;
+import com.google.gerrit.plugins.checks.api.CheckerStatus;
+import com.google.gerrit.reviewdb.client.Project;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+
+/**
+ * A basic property of a checker.
+ *
+ * <p>Each property knows how to read and write its value from/to a JGit {@link Config} file.
+ */
+enum CheckerConfigEntry {
+ /**
+ * The name of a checker. This property is equivalent to {@link Checker#getName()}.
+ *
+ * <p>This is a mandatory property.
+ */
+ NAME("name") {
+ @Override
+ void readFromConfig(String checkerUuid, Checker.Builder checker, Config config)
+ throws ConfigInvalidException {
+ String name = config.getString(SECTION_NAME, null, super.keyName);
+ // An empty name is invalid in NoteDb; CheckerConfig will refuse to store it
+ if (name == null) {
+ throw new ConfigInvalidException(String.format("name of checker %s not set", checkerUuid));
+ }
+ checker.setName(name);
+ }
+
+ @Override
+ void initNewConfig(Config config, CheckerCreation checkerCreation) {
+ String name = checkerCreation.getName();
+ config.setString(SECTION_NAME, null, super.keyName, name);
+ }
+
+ @Override
+ void updateConfigValue(Config config, CheckerUpdate checkerUpdate) {
+ checkerUpdate
+ .getName()
+ .ifPresent(name -> config.setString(SECTION_NAME, null, super.keyName, name));
+ }
+ },
+
+ /**
+ * The description of a checker. This property is equivalent to {@link Checker#getDescription()}.
+ *
+ * <p>It defaults to {@code null} if not set.
+ */
+ DESCRIPTION("description") {
+ @Override
+ void readFromConfig(String checkerUuid, Checker.Builder checker, Config config) {
+ String description = config.getString(SECTION_NAME, null, super.keyName);
+ if (!Strings.isNullOrEmpty(description)) {
+ checker.setDescription(description);
+ }
+ }
+
+ @Override
+ void initNewConfig(Config config, CheckerCreation checkerCreation) {
+ // Do nothing. Description key will be set by updateConfigValue.
+ }
+
+ @Override
+ void updateConfigValue(Config config, CheckerUpdate checkerUpdate) {
+ checkerUpdate
+ .getDescription()
+ .ifPresent(
+ description -> {
+ if (Strings.isNullOrEmpty(description)) {
+ config.unset(SECTION_NAME, null, super.keyName);
+ } else {
+ config.setString(SECTION_NAME, null, super.keyName, description);
+ }
+ });
+ }
+ },
+
+ /**
+ * The URL of a checker. This property is equivalent to {@link Checker#getUrl()}.
+ *
+ * <p>It defaults to {@code null} if not set.
+ */
+ URL("url") {
+ @Override
+ void readFromConfig(String checkerUuid, Checker.Builder checker, Config config) {
+ String url = config.getString(SECTION_NAME, null, super.keyName);
+ if (!Strings.isNullOrEmpty(url)) {
+ checker.setUrl(url);
+ }
+ }
+
+ @Override
+ void initNewConfig(Config config, CheckerCreation checkerCreation) {
+ // Do nothing. URL key will be set by updateConfigValue.
+ }
+
+ @Override
+ void updateConfigValue(Config config, CheckerUpdate checkerUpdate) {
+ checkerUpdate
+ .getUrl()
+ .ifPresent(
+ url -> {
+ if (Strings.isNullOrEmpty(url)) {
+ config.unset(SECTION_NAME, null, super.keyName);
+ } else {
+ config.setString(SECTION_NAME, null, super.keyName, url);
+ }
+ });
+ }
+ },
+
+ /**
+ * The repository for which the checker applies. This property is equivalent to {@link
+ * Checker#getRepository()}.
+ *
+ * <p>This is a mandatory property.
+ */
+ REPOSITORY("repository") {
+ @Override
+ void readFromConfig(String checkerUuid, Checker.Builder checker, Config config)
+ throws ConfigInvalidException {
+ String repository = config.getString(SECTION_NAME, null, super.keyName);
+ // An empty repository is invalid in NoteDb; CheckerConfig will refuse to store it
+ if (repository == null) {
+ throw new ConfigInvalidException(
+ String.format("repository of checker %s not set", checkerUuid));
+ }
+ checker.setRepository(new Project.NameKey(repository));
+ }
+
+ @Override
+ void initNewConfig(Config config, CheckerCreation checkerCreation) {
+ String repository = checkerCreation.getRepository().get();
+ config.setString(SECTION_NAME, null, super.keyName, repository);
+ }
+
+ @Override
+ void updateConfigValue(Config config, CheckerUpdate checkerUpdate) {
+ checkerUpdate
+ .getRepository()
+ .ifPresent(
+ repository -> config.setString(SECTION_NAME, null, super.keyName, repository.get()));
+ }
+ },
+
+ STATUS("status") {
+ @Override
+ void readFromConfig(String checkerUuid, Checker.Builder checker, Config config)
+ throws ConfigInvalidException {
+ String value = config.getString(SECTION_NAME, null, super.keyName);
+ if (value == null) {
+ throw new ConfigInvalidException(
+ String.format("status of checker %s not set", checkerUuid));
+ }
+ checker.setStatus(config.getEnum(SECTION_NAME, null, super.keyName, CheckerStatus.ENABLED));
+ }
+
+ @Override
+ void initNewConfig(Config config, CheckerCreation checkerCreation) {
+ // New checkers default to enabled.
+ config.setEnum(SECTION_NAME, null, super.keyName, CheckerStatus.ENABLED);
+ }
+
+ @Override
+ void updateConfigValue(Config config, CheckerUpdate checkerUpdate) {
+ checkerUpdate
+ .getStatus()
+ .ifPresent(status -> config.setEnum(SECTION_NAME, null, super.keyName, status));
+ }
+ };
+
+ private static final String SECTION_NAME = "checker";
+
+ private final String keyName;
+
+ CheckerConfigEntry(String keyName) {
+ this.keyName = keyName;
+ }
+
+ /**
+ * Reads the corresponding property of this {@code CheckerConfigEntry} from the given {@code
+ * Config}. The read value is written to the corresponding property of {@code Checker.Builder}.
+ *
+ * @param checkerUuid the UUID of the checker (necessary for helpful error messages)
+ * @param checker the {@code Checker.Builder} whose property value should be set
+ * @param config the {@code Config} from which the value of the property should be read
+ * @throws ConfigInvalidException if the property has an unexpected value
+ */
+ abstract void readFromConfig(String checkerUuid, Checker.Builder checker, Config config)
+ throws ConfigInvalidException;
+
+ /**
+ * Initializes the corresponding property of this {@code CheckerConfigEntry} in the given {@code
+ * Config}.
+ *
+ * <p>If the specified {@code CheckerCreation} has an entry for the property, that value is used.
+ * If not, the default value for the property is set. In any case, an existing entry for the
+ * property in the {@code Config} will be overwritten.
+ *
+ * @param config a new {@code Config}, typically without an entry for the property
+ * @param checkerCreation an {@code CheckerCreation} detailing the initial value of mandatory
+ * checker properties
+ */
+ abstract void initNewConfig(Config config, CheckerCreation checkerCreation);
+
+ /**
+ * Updates the corresponding property of this {@code CheckerConfigEntry} in the given {@code
+ * Config} if the {@code CheckerUpdate} mentions a modification.
+ *
+ * <p>This call is a no-op if the {@code CheckerUpdate} doesn't contain a modification for the
+ * property.
+ *
+ * @param config a {@code Config} for which the property should be updated
+ * @param checkerUpdate an {@code CheckerUpdate} detailing the modifications on a checker
+ */
+ abstract void updateConfigValue(Config config, CheckerUpdate checkerUpdate);
+}
diff --git a/java/com/google/gerrit/plugins/checks/db/CheckersByRepositoryNotes.java b/java/com/google/gerrit/plugins/checks/db/CheckersByRepositoryNotes.java
new file mode 100644
index 0000000..be5c322
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/db/CheckersByRepositoryNotes.java
@@ -0,0 +1,427 @@
+// 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.google.gerrit.plugins.checks.db;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Comparator.naturalOrder;
+import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.joining;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.flogger.FluentLogger;
+import com.google.common.hash.Hashing;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.plugins.checks.CheckerRef;
+import com.google.gerrit.plugins.checks.CheckerUuid;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.git.meta.VersionedMetaData;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+/**
+ * {@link VersionedMetaData} subclass to read/update the repository to checkers map.
+ *
+ * <p>The map of repository to checkers is stored in the {@code refs/meta/checkers} notes branch in
+ * the {@code All-Projects} repository. The note ID is a SHA1 that is computed from the repository
+ * name. The node content is a plain list of checker UUIDs, one checker UUID per line.
+ *
+ * <p>This is a low-level API. Reading of the repository to checkers map should be done through
+ * {@link
+ * com.google.gerrit.plugins.checks.Checkers#checkersOf(com.google.gerrit.reviewdb.client.Project.NameKey)}.
+ * Updates to the repository to checkers map are done automatically when creating/updating checkers
+ * through {@link com.google.gerrit.plugins.checks.CheckersUpdate}.
+ *
+ * <p>On load the note map from {@code refs/meta/checkers} is read, but the checker lists are not
+ * parsed yet (see {@link #onLoad()}).
+ *
+ * <p>After loading the note map callers can access the checker list for a single repository. Only
+ * now the requested checker list is parsed.
+ *
+ * <p>After loading the note map callers can stage various updates for the repository to checker map
+ * (insert, update, remove).
+ *
+ * <p>On save the staged updates for the repository to checkers map are performed (see {@link
+ * #onSave(CommitBuilder)}).
+ */
+@VisibleForTesting
+public class CheckersByRepositoryNotes extends VersionedMetaData {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private static final int MAX_NOTE_SZ = 1 << 19;
+
+ public static CheckersByRepositoryNotes load(
+ AllProjectsName allProjectsName, Repository allProjectsRepo)
+ throws IOException, ConfigInvalidException {
+ return new CheckersByRepositoryNotes(allProjectsName, allProjectsRepo).load();
+ }
+
+ public static CheckersByRepositoryNotes load(
+ AllProjectsName allProjectsName, Repository allProjectsRepo, @Nullable ObjectId rev)
+ throws IOException, ConfigInvalidException {
+ return new CheckersByRepositoryNotes(allProjectsName, allProjectsRepo).load(rev);
+ }
+
+ private final AllProjectsName allProjectsName;
+ private final Repository repo;
+
+ // the loaded note map
+ private NoteMap noteMap;
+
+ // Staged note map updates that should be executed on save.
+ private List<NoteMapUpdate> noteMapUpdates = new ArrayList<>();
+
+ private CheckersByRepositoryNotes(AllProjectsName allProjectsName, Repository allProjectsRepo) {
+ this.allProjectsName = requireNonNull(allProjectsName, "allProjectsName");
+ this.repo = requireNonNull(allProjectsRepo, "allProjectsRepo");
+ }
+
+ public Repository getRepository() {
+ return repo;
+ }
+
+ @Override
+ protected String getRefName() {
+ return CheckerRef.REFS_META_CHECKERS;
+ }
+
+ /**
+ * Loads the checkers by repository notes from the current tip of the {@code refs/meta/checkers}
+ * branch.
+ *
+ * @return {@link CheckersByRepositoryNotes} instance for chaining
+ */
+ private CheckersByRepositoryNotes load() throws IOException, ConfigInvalidException {
+ super.load(allProjectsName, repo);
+ return this;
+ }
+
+ /**
+ * Loads the checkers by repository notes from the specified revision of the {@code
+ * refs/meta/checkers} branch.
+ *
+ * @param rev the revision from which the checkers by repository notes should be loaded, if {@code
+ * null} the checkers by repository notes are loaded from the current tip, if {@link
+ * ObjectId#zeroId()} it's assumed that the {@code refs/meta/checkers} branch doesn't exist
+ * and the loaded checkers by repository will be empty
+ * @return {@link CheckersByRepositoryNotes} instance for chaining
+ */
+ CheckersByRepositoryNotes load(@Nullable ObjectId rev)
+ throws IOException, ConfigInvalidException {
+ if (rev == null) {
+ return load();
+ }
+ if (ObjectId.zeroId().equals(rev)) {
+ load(allProjectsName, repo, null);
+ return this;
+ }
+ load(allProjectsName, repo, rev);
+ return this;
+ }
+
+ /**
+ * Parses and returns the set of checker UUIDs for the specified repository.
+ *
+ * <p>Invalid checker UUIDs are silently ignored.
+ *
+ * @param repositoryName the name of the repository for which the set of checker UUIDs should be
+ * parsed and returned
+ * @return the set of checker UUIDs for the specified repository, empty set if no checkers apply
+ * for this repository
+ * @throws IOException if reading the note with the checker UUID list fails
+ */
+ public ImmutableSortedSet<String> get(Project.NameKey repositoryName) throws IOException {
+ checkLoaded();
+ ObjectId noteId = computeRepositorySha1(repositoryName);
+ if (!noteMap.contains(noteId)) {
+ return ImmutableSortedSet.of();
+ }
+
+ try (RevWalk rw = new RevWalk(repo)) {
+ ObjectId noteDataId = noteMap.get(noteId);
+ byte[] raw = readNoteData(rw, noteDataId);
+ return parseCheckerUuidsFromNote(noteId, raw, noteDataId);
+ }
+ }
+
+ /**
+ * Inserts a new checker for a repository.
+ *
+ * <p><strong>Note:</strong> This method doesn't perform the update. It only contains the
+ * instructions for the update. To apply the update for real and write the result back to NoteDb,
+ * call {@link #commit(MetaDataUpdate)} on this {@code CheckersByRepositoryNotes}.
+ *
+ * @param checkerUuid the UUID of the checker that should be inserted for the given repository
+ * @param repositoryName the name of the repository for which the checker should be inserted
+ */
+ public void insert(String checkerUuid, Project.NameKey repositoryName) {
+ checkLoaded();
+
+ noteMapUpdates.add(
+ (rw, n, f) -> {
+ insert(rw, inserter, n, f, checkerUuid, repositoryName);
+ });
+ }
+
+ /**
+ * Removes a checker from a repository.
+ *
+ * <p><strong>Note:</strong> This method doesn't perform the update. It only contains the
+ * instructions for the update. To apply the update for real and write the result back to NoteDb,
+ * call {@link #commit(MetaDataUpdate)} on this {@code CheckersByRepositoryNotes}.
+ *
+ * @param checkerUuid the UUID of the checker that should be removed from the given repository
+ * @param repositoryName the name of the repository for which the checker should be removed
+ */
+ public void remove(String checkerUuid, Project.NameKey repositoryName) {
+ checkLoaded();
+
+ noteMapUpdates.add(
+ (rw, n, f) -> {
+ remove(rw, inserter, n, f, checkerUuid, repositoryName);
+ });
+ }
+
+ /**
+ * Updates the repository for a checker.
+ *
+ * <p><strong>Note:</strong> This method doesn't perform the update. It only contains the
+ * instructions for the update. To apply the update for real and write the result back to NoteDb,
+ * call {@link #commit(MetaDataUpdate)} on this {@code CheckersByRepositoryNotes}.
+ *
+ * @param checkerUuid the UUID of the checker that should be removed from the given repository
+ * @param oldRepositoryName the name of the repository for which the checker should be removed
+ * @param newRepositoryName the name of the repository for which the checker should be inserted
+ */
+ public void update(
+ String checkerUuid, Project.NameKey oldRepositoryName, Project.NameKey newRepositoryName) {
+ checkLoaded();
+
+ if (oldRepositoryName.equals(newRepositoryName)) {
+ return;
+ }
+
+ noteMapUpdates.add(
+ (rw, n, f) -> {
+ remove(rw, inserter, n, f, checkerUuid, oldRepositoryName);
+ insert(rw, inserter, n, f, checkerUuid, newRepositoryName);
+ });
+ }
+
+ @Override
+ protected void onLoad() throws IOException, ConfigInvalidException {
+ logger.atFine().log("Reading checkers by repository note map");
+
+ noteMap = revision != null ? NoteMap.read(reader, revision) : NoteMap.newEmptyMap();
+ }
+
+ private void checkLoaded() {
+ checkState(noteMap != null, "Checkers by repository not loaded yet");
+ }
+
+ @Override
+ protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException {
+ if (noteMapUpdates.isEmpty()) {
+ return false;
+ }
+
+ logger.atFine().log("Updating checkers by repository");
+
+ if (Strings.isNullOrEmpty(commit.getMessage())) {
+ commit.setMessage("Update checkers by repository\n");
+ }
+
+ try (RevWalk rw = new RevWalk(reader)) {
+ ImmutableSortedSet.Builder<String> footersBuilder = ImmutableSortedSet.naturalOrder();
+ for (NoteMapUpdate noteMapUpdate : noteMapUpdates) {
+ noteMapUpdate.execute(rw, noteMap, footersBuilder);
+ }
+ noteMapUpdates.clear();
+ ImmutableSortedSet<String> footers = footersBuilder.build();
+ if (!footers.isEmpty()) {
+ commit.setMessage(
+ footers.stream().collect(joining("\n", commit.getMessage().trim() + "\n\n", "")));
+ }
+
+ RevTree oldTree = revision != null ? rw.parseTree(revision) : null;
+ ObjectId newTreeId = noteMap.writeTree(inserter);
+ if (newTreeId.equals(oldTree)) {
+ return false;
+ }
+
+ commit.setTreeId(newTreeId);
+ return true;
+ }
+ }
+
+ private static byte[] readNoteData(RevWalk rw, ObjectId noteDataId) throws IOException {
+ return rw.getObjectReader().open(noteDataId, OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
+ }
+
+ /**
+ * Parses a list of checker UUIDs from a byte array that contain the checker UUIDs as a plain text
+ * with one checker UUID per line:
+ *
+ * <pre>
+ * e021147da7713263c46d3126b77a863930ff555b
+ * e497b37e55074b7a11832a7e2d18c44b4dab8017
+ * 8cc1d2415fd4fc78b7d5cc02ac59ee3939d0e1da
+ * </pre>
+ *
+ * <p>Invalid checker UUIDs are silently ignored.
+ */
+ private static ImmutableSortedSet<String> parseCheckerUuidsFromNote(
+ ObjectId noteId, byte[] raw, ObjectId blobId) {
+ ImmutableSortedSet<String> lines = parseNote(raw);
+ ImmutableSortedSet.Builder<String> checkerUuids = ImmutableSortedSet.naturalOrder();
+ lines.forEach(
+ line -> {
+ if (CheckerUuid.isUuid(line)) {
+ checkerUuids.add(line);
+ } else {
+ logger.atWarning().log(
+ "Ignoring invalid checker UUID %s in note %s with blob ID %s.",
+ line, noteId.name(), blobId.name());
+ }
+ });
+ return checkerUuids.build();
+ }
+
+ /**
+ * Parses all entries from a note, one entry per line.
+ *
+ * <p>Doesn't validate the entries are valid checker UUIDs.
+ */
+ private static ImmutableSortedSet<String> parseNote(byte[] raw) {
+ return Splitter.on('\n')
+ .splitToList(new String(raw, UTF_8))
+ .stream()
+ .collect(toImmutableSortedSet(naturalOrder()));
+ }
+
+ /**
+ * Insert a checker UUID for a repository and updates the note map.
+ *
+ * <p>No-op if the checker UUID is already recorded for the repository.
+ */
+ private static void insert(
+ RevWalk rw,
+ ObjectInserter ins,
+ NoteMap noteMap,
+ ImmutableSortedSet.Builder<String> footers,
+ String checkerUuid,
+ Project.NameKey repositoryName)
+ throws IOException {
+ ObjectId noteId = computeRepositorySha1(repositoryName);
+ ImmutableSortedSet.Builder<String> newLinesBuilder = ImmutableSortedSet.naturalOrder();
+ if (noteMap.contains(noteId)) {
+ ObjectId noteDataId = noteMap.get(noteId);
+ byte[] raw = readNoteData(rw, noteDataId);
+ ImmutableSortedSet<String> oldLines = parseNote(raw);
+ if (oldLines.contains(checkerUuid)) {
+ return;
+ }
+ newLinesBuilder.addAll(oldLines);
+ }
+
+ newLinesBuilder.add(checkerUuid);
+ byte[] raw = Joiner.on("\n").join(newLinesBuilder.build()).getBytes(UTF_8);
+ ObjectId noteData = ins.insert(OBJ_BLOB, raw);
+ noteMap.set(noteId, noteData);
+ addFooters(footers, checkerUuid, repositoryName);
+ }
+
+ /**
+ * Removes a checker UUID from a repository and updates the note map.
+ *
+ * <p>No-op if the checker UUID is already not recorded for the repository.
+ */
+ private static void remove(
+ RevWalk rw,
+ ObjectInserter ins,
+ NoteMap noteMap,
+ ImmutableSortedSet.Builder<String> footers,
+ String checkerUuid,
+ Project.NameKey repositoryName)
+ throws IOException {
+ ObjectId noteId = computeRepositorySha1(repositoryName);
+ ImmutableSortedSet.Builder<String> newLinesBuilder = ImmutableSortedSet.naturalOrder();
+ if (noteMap.contains(noteId)) {
+ ObjectId noteDataId = noteMap.get(noteId);
+ byte[] raw = readNoteData(rw, noteDataId);
+ ImmutableSortedSet<String> oldLines = parseNote(raw);
+ if (!oldLines.contains(checkerUuid)) {
+ return;
+ }
+ oldLines.stream().filter(line -> !line.equals(checkerUuid)).forEach(newLinesBuilder::add);
+ }
+
+ ImmutableSortedSet<String> newLines = newLinesBuilder.build();
+ if (newLines.isEmpty()) {
+ noteMap.remove(noteId);
+ return;
+ }
+
+ byte[] raw = Joiner.on("\n").join(newLines).getBytes(UTF_8);
+ ObjectId noteData = ins.insert(OBJ_BLOB, raw);
+ noteMap.set(noteId, noteData);
+ addFooters(footers, checkerUuid, repositoryName);
+ }
+
+ private static void addFooters(
+ ImmutableSortedSet.Builder<String> footers,
+ String checkerUuid,
+ Project.NameKey repositoryName) {
+ footers.add("Repository: " + repositoryName.get());
+ footers.add("Checker: " + checkerUuid);
+ }
+
+ /**
+ * Returns the SHA1 of the repository that is used as note ID in the {@code refs/meta/checkers}
+ * notes branch.
+ *
+ * @param repositoryName the name of the repository for which the SHA1 should be computed and
+ * returned
+ * @return SHA1 for the given repository name
+ */
+ @VisibleForTesting
+ @SuppressWarnings("deprecation") // Use Hashing.sha1 for compatibility.
+ public static ObjectId computeRepositorySha1(Project.NameKey repositoryName) {
+ return ObjectId.fromRaw(Hashing.sha1().hashString(repositoryName.get(), UTF_8).asBytes());
+ }
+
+ @FunctionalInterface
+ private interface NoteMapUpdate {
+ void execute(RevWalk rw, NoteMap noteMap, ImmutableSortedSet.Builder<String> footers)
+ throws IOException;
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/db/NoteDbCheckers.java b/java/com/google/gerrit/plugins/checks/db/NoteDbCheckers.java
new file mode 100644
index 0000000..ddb5025
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/db/NoteDbCheckers.java
@@ -0,0 +1,116 @@
+// 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.google.gerrit.plugins.checks.db;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Streams;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.plugins.checks.Checker;
+import com.google.gerrit.plugins.checks.CheckerRef;
+import com.google.gerrit.plugins.checks.CheckerUuid;
+import com.google.gerrit.plugins.checks.Checkers;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.List;
+import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+
+/** Class to read checkers from NoteDb. */
+@Singleton
+class NoteDbCheckers implements Checkers {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private final GitRepositoryManager repoManager;
+ private final AllProjectsName allProjectsName;
+
+ @Inject
+ NoteDbCheckers(GitRepositoryManager repoManager, AllProjectsName allProjectsName) {
+ this.repoManager = repoManager;
+ this.allProjectsName = allProjectsName;
+ }
+
+ @Override
+ public Optional<Checker> getChecker(String checkerUuid)
+ throws IOException, ConfigInvalidException {
+ if (!CheckerUuid.isUuid(checkerUuid)) {
+ return Optional.empty();
+ }
+
+ try (Repository allProjectsRepo = repoManager.openRepository(allProjectsName)) {
+ CheckerConfig checkerConfig =
+ CheckerConfig.loadForChecker(allProjectsName, allProjectsRepo, checkerUuid);
+ return checkerConfig.getLoadedChecker();
+ }
+ }
+
+ @Override
+ public ImmutableList<Checker> listCheckers() throws IOException {
+ try (Repository allProjectsRepo = repoManager.openRepository(allProjectsName)) {
+ List<Ref> checkerRefs =
+ allProjectsRepo.getRefDatabase().getRefsByPrefix(CheckerRef.REFS_CHECKERS);
+ ImmutableList<String> sortedCheckerUuids =
+ checkerRefs
+ .stream()
+ .map(CheckerUuid::fromRef)
+ .flatMap(Streams::stream)
+ .sorted()
+ .collect(toImmutableList());
+ ImmutableList.Builder<Checker> sortedCheckers = ImmutableList.builder();
+ for (String checkerUuid : sortedCheckerUuids) {
+ try {
+ CheckerConfig checkerConfig =
+ CheckerConfig.loadForChecker(allProjectsName, allProjectsRepo, checkerUuid);
+ checkerConfig.getLoadedChecker().ifPresent(sortedCheckers::add);
+ } catch (ConfigInvalidException e) {
+ logger.atWarning().withCause(e).log(
+ "Ignore invalid checker %s on listing checkers", checkerUuid);
+ }
+ }
+ return sortedCheckers.build();
+ }
+ }
+
+ @Override
+ public ImmutableSet<Checker> checkersOf(Project.NameKey repositoryName)
+ throws IOException, ConfigInvalidException {
+ try (Repository allProjectsRepo = repoManager.openRepository(allProjectsName)) {
+ ImmutableSet<String> checkerUuids =
+ CheckersByRepositoryNotes.load(allProjectsName, allProjectsRepo).get(repositoryName);
+
+ ImmutableSet.Builder<Checker> checkers = ImmutableSet.builder();
+ for (String checkerUuid : checkerUuids) {
+ try {
+ CheckerConfig checkerConfig =
+ CheckerConfig.loadForChecker(allProjectsName, allProjectsRepo, checkerUuid);
+ checkerConfig.getLoadedChecker().ifPresent(checkers::add);
+ } catch (ConfigInvalidException e) {
+ logger.atWarning().withCause(e).log(
+ "Ignore invalid checker %s on listing checkers for repository %s",
+ checkerUuid, repositoryName);
+ }
+ }
+ return checkers.build();
+ }
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/db/NoteDbCheckersModule.java b/java/com/google/gerrit/plugins/checks/db/NoteDbCheckersModule.java
new file mode 100644
index 0000000..0d2fd17
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/db/NoteDbCheckersModule.java
@@ -0,0 +1,46 @@
+// 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.google.gerrit.plugins.checks.db;
+
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.plugins.checks.Checkers;
+import com.google.gerrit.plugins.checks.CheckersUpdate;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.ServerInitiated;
+import com.google.gerrit.server.UserInitiated;
+import com.google.inject.Provides;
+
+/** Bind NoteDb implementation for checker storage layer. */
+public class NoteDbCheckersModule extends FactoryModule {
+ @Override
+ protected void configure() {
+ bind(Checkers.class).to(NoteDbCheckers.class);
+ factory(NoteDbCheckersUpdate.Factory.class);
+ }
+
+ @Provides
+ @ServerInitiated
+ CheckersUpdate provideServerInitiatedCheckersUpdate(
+ NoteDbCheckersUpdate.Factory checkersUpdateFactory) {
+ return checkersUpdateFactory.createWithServerIdent();
+ }
+
+ @Provides
+ @UserInitiated
+ CheckersUpdate provideUserInitiatedCheckersUpdate(
+ NoteDbCheckersUpdate.Factory checkersUpdateFactory, IdentifiedUser currentUser) {
+ return checkersUpdateFactory.create(currentUser);
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/db/NoteDbCheckersUpdate.java b/java/com/google/gerrit/plugins/checks/db/NoteDbCheckersUpdate.java
new file mode 100644
index 0000000..371cfbc
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/db/NoteDbCheckersUpdate.java
@@ -0,0 +1,294 @@
+// 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.google.gerrit.plugins.checks.db;
+
+import com.google.common.base.Throwables;
+import com.google.gerrit.git.LockFailureException;
+import com.google.gerrit.git.RefUpdateUtil;
+import com.google.gerrit.plugins.checks.Checker;
+import com.google.gerrit.plugins.checks.CheckerCreation;
+import com.google.gerrit.plugins.checks.CheckerUpdate;
+import com.google.gerrit.plugins.checks.CheckersUpdate;
+import com.google.gerrit.plugins.checks.NoSuchCheckerException;
+import com.google.gerrit.plugins.checks.api.CheckerStatus;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryHelper.ActionType;
+import com.google.gerrit.server.util.time.TimeUtil;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+
+/** Class to write checkers to NoteDb. */
+class NoteDbCheckersUpdate implements CheckersUpdate {
+ interface Factory {
+ /**
+ * Creates a {@code CheckersUpdate} which uses the identity of the specified user to mark
+ * database modifications executed by it. For NoteDb, this identity is used as author and
+ * committer for all related commits.
+ *
+ * <p><strong>Note</strong>: Please use this method with care and consider using the {@link
+ * com.google.gerrit.server.UserInitiated} annotation on the provider of a {@code
+ * CheckersUpdate} instead.
+ *
+ * @param currentUser the user to which modifications should be attributed
+ */
+ NoteDbCheckersUpdate create(IdentifiedUser currentUser);
+
+ /**
+ * Creates a {@code CheckersUpdate} which uses the server identity to mark database
+ * modifications executed by it. For NoteDb, this identity is used as author and committer for
+ * all related commits.
+ *
+ * <p><strong>Note</strong>: Please use this method with care and consider using the {@link
+ * com.google.gerrit.server.ServerInitiated} annotation on the provider of a {@code
+ * CheckersUpdate} instead.
+ */
+ NoteDbCheckersUpdate createWithServerIdent();
+ }
+
+ private final GitRepositoryManager repoManager;
+ private final AllProjectsName allProjectsName;
+ private final MetaDataUpdateFactory metaDataUpdateFactory;
+ private final GitReferenceUpdated gitRefUpdated;
+ private final RetryHelper retryHelper;
+ private final Optional<IdentifiedUser> currentUser;
+
+ @AssistedInject
+ NoteDbCheckersUpdate(
+ GitRepositoryManager repoManager,
+ AllProjectsName allProjectsName,
+ MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory,
+ GitReferenceUpdated gitRefUpdated,
+ RetryHelper retryHelper,
+ @GerritPersonIdent PersonIdent serverIdent) {
+ this(
+ repoManager,
+ allProjectsName,
+ metaDataUpdateInternalFactory,
+ gitRefUpdated,
+ retryHelper,
+ serverIdent,
+ Optional.empty());
+ }
+
+ @AssistedInject
+ NoteDbCheckersUpdate(
+ GitRepositoryManager repoManager,
+ AllProjectsName allProjectsName,
+ MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory,
+ GitReferenceUpdated gitRefUpdated,
+ RetryHelper retryHelper,
+ @GerritPersonIdent PersonIdent serverIdent,
+ @Assisted IdentifiedUser currentUser) {
+ this(
+ repoManager,
+ allProjectsName,
+ metaDataUpdateInternalFactory,
+ gitRefUpdated,
+ retryHelper,
+ serverIdent,
+ Optional.of(currentUser));
+ }
+
+ private NoteDbCheckersUpdate(
+ GitRepositoryManager repoManager,
+ AllProjectsName allProjectsName,
+ MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory,
+ GitReferenceUpdated gitRefUpdated,
+ RetryHelper retryHelper,
+ @GerritPersonIdent PersonIdent serverIdent,
+ Optional<IdentifiedUser> currentUser) {
+ this.repoManager = repoManager;
+ this.allProjectsName = allProjectsName;
+ this.gitRefUpdated = gitRefUpdated;
+ this.retryHelper = retryHelper;
+ this.currentUser = currentUser;
+
+ metaDataUpdateFactory =
+ getMetaDataUpdateFactory(metaDataUpdateInternalFactory, currentUser, serverIdent);
+ }
+
+ @Override
+ public Checker createChecker(CheckerCreation checkerCreation, CheckerUpdate checkerUpdate)
+ throws OrmDuplicateKeyException, IOException, ConfigInvalidException {
+ try {
+ return retryHelper.execute(
+ RetryHelper.ActionType.PLUGIN_UPDATE,
+ () -> createCheckerInNoteDb(checkerCreation, checkerUpdate),
+ LockFailureException.class::isInstance);
+ } catch (Exception e) {
+ Throwables.throwIfUnchecked(e);
+ Throwables.throwIfInstanceOf(e, OrmDuplicateKeyException.class);
+ Throwables.throwIfInstanceOf(e, IOException.class);
+ Throwables.throwIfInstanceOf(e, ConfigInvalidException.class);
+ throw new IOException(e);
+ }
+ }
+
+ private Checker createCheckerInNoteDb(
+ CheckerCreation checkerCreation, CheckerUpdate checkerUpdate)
+ throws OrmDuplicateKeyException, IOException, ConfigInvalidException {
+ try (Repository allProjectsRepo = repoManager.openRepository(allProjectsName)) {
+ CheckerConfig checkerConfig =
+ CheckerConfig.createForNewChecker(allProjectsName, allProjectsRepo, checkerCreation);
+ checkerConfig.setCheckerUpdate(checkerUpdate);
+
+ CheckersByRepositoryNotes checkersByRepositoryNotes =
+ CheckersByRepositoryNotes.load(allProjectsName, allProjectsRepo);
+ checkersByRepositoryNotes.insert(
+ checkerCreation.getCheckerUuid(), checkerCreation.getRepository());
+
+ commit(allProjectsRepo, checkerConfig, checkersByRepositoryNotes);
+
+ return checkerConfig
+ .getLoadedChecker()
+ .orElseThrow(
+ () -> new IllegalStateException("Created checker wasn't automatically loaded"));
+ }
+ }
+
+ private void commit(
+ Repository allProjectsRepo,
+ CheckerConfig checkerConfig,
+ CheckersByRepositoryNotes checkersByRepositoryNotes)
+ throws IOException {
+ BatchRefUpdate batchRefUpdate = allProjectsRepo.getRefDatabase().newBatchUpdate();
+ try (MetaDataUpdate metaDataUpdate =
+ metaDataUpdateFactory.create(allProjectsName, allProjectsRepo, batchRefUpdate)) {
+ checkerConfig.commit(metaDataUpdate);
+ checkersByRepositoryNotes.commit(metaDataUpdate);
+ }
+ RefUpdateUtil.executeChecked(batchRefUpdate, allProjectsRepo);
+
+ gitRefUpdated.fire(
+ allProjectsName, batchRefUpdate, currentUser.map(user -> user.state()).orElse(null));
+ }
+
+ private static MetaDataUpdateFactory getMetaDataUpdateFactory(
+ MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory,
+ Optional<IdentifiedUser> currentUser,
+ PersonIdent serverIdent) {
+ return (projectName, repository, batchRefUpdate) -> {
+ MetaDataUpdate metaDataUpdate =
+ metaDataUpdateInternalFactory.create(projectName, repository, batchRefUpdate);
+ metaDataUpdate.getCommitBuilder().setCommitter(serverIdent);
+ PersonIdent authorIdent;
+ if (currentUser.isPresent()) {
+ metaDataUpdate.setAuthor(currentUser.get());
+ authorIdent =
+ currentUser.get().newCommitterIdent(serverIdent.getWhen(), serverIdent.getTimeZone());
+ } else {
+ authorIdent = serverIdent;
+ }
+ metaDataUpdate.getCommitBuilder().setAuthor(authorIdent);
+ return metaDataUpdate;
+ };
+ }
+
+ @FunctionalInterface
+ private interface MetaDataUpdateFactory {
+ MetaDataUpdate create(
+ Project.NameKey projectName, Repository repository, BatchRefUpdate batchRefUpdate)
+ throws IOException;
+ }
+
+ @Override
+ public Checker updateChecker(String checkerUuid, CheckerUpdate checkerUpdate)
+ throws NoSuchCheckerException, IOException, ConfigInvalidException {
+ Optional<Timestamp> updatedOn = checkerUpdate.getUpdatedOn();
+ if (!updatedOn.isPresent()) {
+ updatedOn = Optional.of(TimeUtil.nowTs());
+ checkerUpdate = checkerUpdate.toBuilder().setUpdatedOn(updatedOn.get()).build();
+ }
+ return updateCheckerWithRetry(checkerUuid, checkerUpdate);
+ }
+
+ private Checker updateCheckerWithRetry(String checkerUuid, CheckerUpdate checkerUpdate)
+ throws NoSuchCheckerException, IOException, ConfigInvalidException {
+ try {
+ return retryHelper.execute(
+ ActionType.PLUGIN_UPDATE,
+ () -> updateCheckerInNoteDb(checkerUuid, checkerUpdate),
+ LockFailureException.class::isInstance);
+ } catch (Exception e) {
+ Throwables.throwIfUnchecked(e);
+ Throwables.throwIfInstanceOf(e, IOException.class);
+ Throwables.throwIfInstanceOf(e, ConfigInvalidException.class);
+ Throwables.throwIfInstanceOf(e, NoSuchCheckerException.class);
+ throw new IOException(e);
+ }
+ }
+
+ private Checker updateCheckerInNoteDb(String checkerUuid, CheckerUpdate checkerUpdate)
+ throws IOException, ConfigInvalidException, NoSuchCheckerException {
+ try (Repository allProjectsRepo = repoManager.openRepository(allProjectsName)) {
+ CheckerConfig checkerConfig =
+ CheckerConfig.loadForChecker(allProjectsName, allProjectsRepo, checkerUuid);
+ checkerConfig.setCheckerUpdate(checkerUpdate);
+ if (!checkerConfig.getLoadedChecker().isPresent()) {
+ throw new NoSuchCheckerException(checkerUuid);
+ }
+
+ CheckersByRepositoryNotes checkersByRepositoryNotes =
+ CheckersByRepositoryNotes.load(allProjectsName, allProjectsRepo);
+
+ Checker checker = checkerConfig.getLoadedChecker().get();
+ Project.NameKey oldRepositoryName = checker.getRepository();
+ Project.NameKey newRepositoryName = checkerUpdate.getRepository().orElse(oldRepositoryName);
+
+ CheckerStatus newStatus = checkerUpdate.getStatus().orElse(checker.getStatus());
+ switch (newStatus) {
+ // May produce some redundant notes updates, but CheckersByRepositoryNotes knows how to
+ // short-circuit on no-ops, and the logic in this method is simple.
+ case DISABLED:
+ checkersByRepositoryNotes.remove(checkerUuid, oldRepositoryName);
+ checkersByRepositoryNotes.remove(checkerUuid, newRepositoryName);
+ break;
+ case ENABLED:
+ if (oldRepositoryName.equals(newRepositoryName)) {
+ checkersByRepositoryNotes.insert(checkerUuid, newRepositoryName);
+ } else {
+ checkersByRepositoryNotes.update(checkerUuid, oldRepositoryName, newRepositoryName);
+ }
+ break;
+ default:
+ throw new IllegalStateException("invalid checker status: " + newStatus);
+ }
+
+ commit(allProjectsRepo, checkerConfig, checkersByRepositoryNotes);
+
+ Checker updatedChecker =
+ checkerConfig
+ .getLoadedChecker()
+ .orElseThrow(
+ () -> new IllegalStateException("Updated checker wasn't automatically loaded"));
+ return updatedChecker;
+ }
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/testing/BUILD b/java/com/google/gerrit/plugins/checks/testing/BUILD
new file mode 100644
index 0000000..f50b6a0
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/testing/BUILD
@@ -0,0 +1,18 @@
+package(
+ default_testonly = True,
+ default_visibility = ["//plugins/checks:visibility"],
+)
+
+java_library(
+ name = "testing",
+ srcs = glob(["*.java"]),
+ deps = [
+ "//java/com/google/gerrit/reviewdb:server",
+ "//java/com/google/gerrit/server/testing",
+ "//java/com/google/gerrit/truth",
+ "//lib/truth",
+ "//lib/truth:truth-java8-extension",
+ "//plugins:plugin-api",
+ "//plugins/checks:checks__plugin",
+ ],
+)
diff --git a/java/com/google/gerrit/plugins/checks/testing/CheckerConfigSubject.java b/java/com/google/gerrit/plugins/checks/testing/CheckerConfigSubject.java
new file mode 100644
index 0000000..250036f
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/testing/CheckerConfigSubject.java
@@ -0,0 +1,90 @@
+// 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.google.gerrit.plugins.checks.testing;
+
+import static com.google.common.truth.Truth.assertAbout;
+
+import com.google.common.truth.ComparableSubject;
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.IterableSubject;
+import com.google.common.truth.StringSubject;
+import com.google.common.truth.Subject;
+import com.google.common.truth.Truth;
+import com.google.common.truth.Truth8;
+import com.google.gerrit.plugins.checks.Checker;
+import com.google.gerrit.plugins.checks.api.CheckerStatus;
+import com.google.gerrit.plugins.checks.db.CheckerConfig;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.testing.ObjectIdSubject;
+import com.google.gerrit.truth.OptionalSubject;
+import java.sql.Timestamp;
+import java.util.Optional;
+
+public class CheckerConfigSubject extends Subject<CheckerConfigSubject, CheckerConfig> {
+ public static CheckerConfigSubject assertThat(CheckerConfig checkerConfig) {
+ return assertAbout(CheckerConfigSubject::new).that(checkerConfig);
+ }
+
+ private CheckerConfigSubject(FailureMetadata metadata, CheckerConfig actual) {
+ super(metadata, actual);
+ }
+
+ public void hasUuid(String expectedUuid) {
+ Truth.assertThat(checker().getUuid()).named("uuid").isEqualTo(expectedUuid);
+ }
+
+ public void hasName(String expectedName) {
+ Truth.assertThat(checker().getName()).named("name").isEqualTo(expectedName);
+ }
+
+ public OptionalSubject<StringSubject, String> hasDescriptionThat() {
+ return OptionalSubject.assertThat(checker().getDescription(), Truth::assertThat)
+ .named("description");
+ }
+
+ public OptionalSubject<StringSubject, String> hasUrlThat() {
+ return OptionalSubject.assertThat(checker().getUrl(), Truth::assertThat).named("url");
+ }
+
+ public void hasRepository(Project.NameKey expectedRepository) {
+ Truth.assertThat(checker().getRepository()).named("repository").isEqualTo(expectedRepository);
+ }
+
+ public void hasStatus(CheckerStatus expectedStatus) {
+ Truth.assertThat(checker().getStatus()).named("status").isEqualTo(expectedStatus);
+ }
+
+ public ComparableSubject<?, Timestamp> hasCreatedOnThat() {
+ return Truth.assertThat(checker().getCreatedOn()).named("createdOn");
+ }
+
+ public ObjectIdSubject hasRefStateThat() {
+ return ObjectIdSubject.assertThat(checker().getRefState()).named("refState");
+ }
+
+ public IterableSubject configStringList(String name) {
+ isNotNull();
+ return Truth.assertThat(actual().getConfigForTesting().getStringList("checker", null, name))
+ .asList()
+ .named("value of checker.%s", name);
+ }
+
+ private Checker checker() {
+ isNotNull();
+ Optional<Checker> checker = actual().getLoadedChecker();
+ Truth8.assertThat(checker).named("checker is loaded").isPresent();
+ return checker.get();
+ }
+}
diff --git a/javatests/com/google/gerrit/plugins/checks/BUILD b/javatests/com/google/gerrit/plugins/checks/BUILD
new file mode 100644
index 0000000..b659377
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/checks/BUILD
@@ -0,0 +1,19 @@
+package(default_visibility = ["//plugins/checks:visibility"])
+
+load("//tools/bzl:junit.bzl", "junit_tests")
+
+junit_tests(
+ name = "checks_tests",
+ srcs = glob(["*.java"]),
+ deps = [
+ "//java/com/google/gerrit/common:annotations",
+ "//java/com/google/gerrit/extensions:api",
+ "//java/com/google/gerrit/reviewdb:server",
+ "//java/com/google/gerrit/testing:gerrit-test-util",
+ "//lib:guava",
+ "//lib/jgit/org.eclipse.jgit:jgit",
+ "//lib/truth",
+ "//lib/truth:truth-java8-extension",
+ "//plugins/checks:checks__plugin",
+ ],
+)
diff --git a/javatests/com/google/gerrit/plugins/checks/CheckerUrlTest.java b/javatests/com/google/gerrit/plugins/checks/CheckerUrlTest.java
new file mode 100644
index 0000000..cdefe42
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/checks/CheckerUrlTest.java
@@ -0,0 +1,70 @@
+// 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.google.gerrit.plugins.checks;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
+
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.testing.GerritBaseTests;
+import org.junit.Test;
+
+public class CheckerUrlTest extends GerritBaseTests {
+ @Test
+ public void validUrls() throws Exception {
+ assertThat(CheckerUrl.clean("https://foo.com/")).isEqualTo("https://foo.com/");
+ assertThat(CheckerUrl.clean("http://foo.com/")).isEqualTo("http://foo.com/");
+ }
+
+ @Test
+ public void emptyUrls() throws Exception {
+ assertThat(CheckerUrl.clean("")).isEqualTo("");
+ assertThat(CheckerUrl.clean(" ")).isEqualTo("");
+ assertThat(CheckerUrl.clean(" \t ")).isEqualTo("");
+ }
+
+ @Test
+ public void trimUrls() throws Exception {
+ assertThat(CheckerUrl.clean(" https://foo.com/")).isEqualTo("https://foo.com/");
+ assertThat(CheckerUrl.clean("https://foo.com/ ")).isEqualTo("https://foo.com/");
+ assertThat(CheckerUrl.clean(" https://foo.com/ ")).isEqualTo("https://foo.com/");
+ }
+
+ @Test
+ public void notUrls() throws Exception {
+ assertInvalidUrl("foobar", "invalid URL: foobar");
+ assertInvalidUrl("foobar:", "invalid URL: foobar:");
+ assertInvalidUrl("foo http://bar.com/", "invalid URL: foo http://bar.com/");
+ }
+
+ @Test
+ public void nonHttpUrls() throws Exception {
+ assertInvalidUrl("ftp://foo.com/", "only http/https URLs supported: ftp://foo.com/");
+ assertInvalidUrl(
+ "mailto:user@example.com", "only http/https URLs supported: mailto:user@example.com");
+ assertInvalidUrl(
+ "javascript:alert('h4x0r3d')",
+ "only http/https URLs supported: javascript:alert('h4x0r3d')");
+ }
+
+ private static void assertInvalidUrl(String url, String expectedMessage) {
+ try {
+ CheckerUrl.clean(url);
+ assert_().fail("expected BadRequestException");
+ } catch (BadRequestException e) {
+ assertThat(e).hasMessageThat().isEqualTo(expectedMessage);
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/plugins/checks/CheckerUuidTest.java b/javatests/com/google/gerrit/plugins/checks/CheckerUuidTest.java
new file mode 100644
index 0000000..ebea465
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/checks/CheckerUuidTest.java
@@ -0,0 +1,109 @@
+// 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.google.gerrit.plugins.checks;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
+import static com.google.common.truth.Truth8.assertThat;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.testing.GerritBaseTests;
+import org.eclipse.jgit.lib.Ref;
+import org.junit.Test;
+
+public class CheckerUuidTest extends GerritBaseTests {
+ private static final ImmutableSet<String> INVALID_CHECKER_UUIDS =
+ ImmutableSet.of(
+ "",
+ "437ee3",
+ "Id852b02b44d3148de21603fecbc817d03d6899fe",
+ "foo",
+ "437ee373885fbc47b103dc722800448320e8bc61-foo",
+ "437ee373885fbc47b103dc722800448320e8bc61 foo");
+
+ @Test
+ public void createdUuidsForSameInputShouldBeDifferent() {
+ String checkerName = "my-checker";
+ String uuid1 = CheckerUuid.make(checkerName);
+ String uuid2 = CheckerUuid.make(checkerName);
+ assertThat(uuid2).isNotEqualTo(uuid1);
+ }
+
+ @Test
+ public void isUuid() {
+ // valid UUIDs
+ assertThat(CheckerUuid.isUuid("437ee373885fbc47b103dc722800448320e8bc61")).isTrue();
+ assertThat(CheckerUuid.isUuid(CheckerUuid.make("my-checker"))).isTrue();
+
+ // invalid UUIDs
+ assertThat(CheckerUuid.isUuid(null)).isFalse();
+ for (String invalidCheckerUuid : INVALID_CHECKER_UUIDS) {
+ assertThat(CheckerUuid.isUuid(invalidCheckerUuid)).isFalse();
+ }
+ }
+
+ @Test
+ public void checkUuid() {
+ // valid UUIDs
+ assertThat(CheckerUuid.checkUuid("437ee373885fbc47b103dc722800448320e8bc61"))
+ .isEqualTo("437ee373885fbc47b103dc722800448320e8bc61");
+
+ String checkerUuid = CheckerUuid.make("my-checker");
+ assertThat(CheckerUuid.checkUuid(checkerUuid)).isEqualTo(checkerUuid);
+
+ // invalid UUIDs
+ assertThatCheckUuidThrowsIllegalStateExceptionFor(null);
+ for (String invalidCheckerUuid : INVALID_CHECKER_UUIDS) {
+ assertThatCheckUuidThrowsIllegalStateExceptionFor(invalidCheckerUuid);
+ }
+ }
+
+ private void assertThatCheckUuidThrowsIllegalStateExceptionFor(@Nullable String checkerUuid) {
+ try {
+ CheckerUuid.checkUuid(checkerUuid);
+ assert_()
+ .fail("expected IllegalStateException when checking checker UUID \"%s\"", checkerUuid);
+ } catch (IllegalStateException e) {
+ assertThat(e.getMessage()).isEqualTo(String.format("invalid checker UUID: %s", checkerUuid));
+ }
+ }
+
+ @Test
+ public void fromRef() throws Exception {
+ // valid checker refs
+ assertThat(CheckerUuid.fromRef("refs/checkers/43/437ee373885fbc47b103dc722800448320e8bc61"))
+ .hasValue("437ee373885fbc47b103dc722800448320e8bc61");
+
+ String checkerUuid = CheckerUuid.make("my-checker");
+ assertThat(CheckerUuid.fromRef(CheckerRef.refsCheckers(checkerUuid))).hasValue(checkerUuid);
+
+ // invalid checker refs
+ assertThat(CheckerUuid.fromRef((Ref) null)).isEmpty();
+ assertThat(CheckerUuid.fromRef((String) null)).isEmpty();
+ assertThat(CheckerUuid.fromRef("")).isEmpty();
+ assertThat(CheckerUuid.fromRef("refs/checkers/437ee373885fbc47b103dc722800448320e8bc61"))
+ .isEmpty();
+ assertThat(CheckerUuid.fromRef("refs/checkers/61/437ee373885fbc47b103dc722800448320e8bc61"))
+ .isEmpty();
+ assertThat(CheckerUuid.fromRef("refs/checker/43/437ee373885fbc47b103dc722800448320e8bc61"))
+ .isEmpty();
+ assertThat(CheckerUuid.fromRef("refs/checker/43/7ee373885fbc47b103dc722800448320e8bc61"))
+ .isEmpty();
+ assertThat(CheckerUuid.fromRef("refs/checkers/foo")).isEmpty();
+ assertThat(CheckerUuid.fromRef("refs/groups/43/437ee373885fbc47b103dc722800448320e8bc61"))
+ .isEmpty();
+ }
+}
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/BUILD b/javatests/com/google/gerrit/plugins/checks/acceptance/BUILD
new file mode 100644
index 0000000..e279935
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/BUILD
@@ -0,0 +1,15 @@
+package(default_visibility = ["//plugins/checks:visibility"])
+
+load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
+
+acceptance_tests(
+ srcs = glob(["*IT.java"]),
+ group = "checks",
+ deps = [
+ "//java/com/google/gerrit/server/util/time",
+ "//javatests/com/google/gerrit/acceptance/rest/util",
+ "//plugins/checks:checks__plugin",
+ "//plugins/checks/java/com/google/gerrit/plugins/checks/acceptance",
+ "//plugins/checks/java/com/google/gerrit/plugins/checks/acceptance/testsuite",
+ ],
+)
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/CheckerOperationsImplTest.java b/javatests/com/google/gerrit/plugins/checks/acceptance/CheckerOperationsImplTest.java
new file mode 100644
index 0000000..5f21c9e
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/CheckerOperationsImplTest.java
@@ -0,0 +1,388 @@
+// 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.google.gerrit.plugins.checks.acceptance;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+
+import com.google.common.base.Joiner;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.plugins.checks.CheckerRef;
+import com.google.gerrit.plugins.checks.CheckerUuid;
+import com.google.gerrit.plugins.checks.acceptance.testsuite.CheckerOperationsImpl;
+import com.google.gerrit.plugins.checks.acceptance.testsuite.TestChecker;
+import com.google.gerrit.plugins.checks.api.CheckerInfo;
+import com.google.gerrit.plugins.checks.api.CheckerInput;
+import com.google.gerrit.plugins.checks.api.CheckerStatus;
+import com.google.gerrit.plugins.checks.db.CheckerConfig;
+import com.google.gerrit.plugins.checks.db.CheckersByRepositoryNotes;
+import com.google.gerrit.reviewdb.client.Project;
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.Optional;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Ref;
+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.junit.Before;
+import org.junit.Test;
+
+public class CheckerOperationsImplTest extends AbstractCheckersTest {
+ // Use specific subclass instead of depending on the interface field from the base class.
+ private CheckerOperationsImpl checkerOperations;
+
+ @Before
+ public void setUp() {
+ checkerOperations = plugin.getSysInjector().getInstance(CheckerOperationsImpl.class);
+ }
+
+ @Test
+ public void checkerCanBeCreatedWithoutSpecifyingAnyParameters() throws Exception {
+ String checkerUuid = checkerOperations.newChecker().create();
+
+ CheckerInfo foundChecker = getCheckerFromServer(checkerUuid);
+ assertThat(foundChecker.uuid).isEqualTo(checkerUuid);
+ assertThat(foundChecker.name).isNotEmpty();
+ assertThat(foundChecker.description).isNull();
+ assertThat(foundChecker.createdOn).isNotNull();
+ }
+
+ @Test
+ public void twoCheckersWithoutAnyParametersDoNotClash() throws Exception {
+ String checkerUuid1 = checkerOperations.newChecker().create();
+ String checkerUuid2 = checkerOperations.newChecker().create();
+
+ TestChecker checker1 = checkerOperations.checker(checkerUuid1).get();
+ TestChecker checker2 = checkerOperations.checker(checkerUuid2).get();
+ assertThat(checker1.uuid()).isNotEqualTo(checker2.uuid());
+ }
+
+ @Test
+ public void checkerCreatedByTestApiCanBeRetrievedViaOfficialApi() throws Exception {
+ String checkerUuid = checkerOperations.newChecker().create();
+
+ CheckerInfo foundChecker = getCheckerFromServer(checkerUuid);
+ assertThat(foundChecker.uuid).isEqualTo(checkerUuid);
+ }
+
+ @Test
+ public void specifiedNameIsRespectedForCheckerCreation() throws Exception {
+ String checkerUuid =
+ checkerOperations.newChecker().name("XYZ-123-this-name-must-be-unique").create();
+
+ CheckerInfo checker = getCheckerFromServer(checkerUuid);
+ assertThat(checker.name).isEqualTo("XYZ-123-this-name-must-be-unique");
+ }
+
+ @Test
+ public void specifiedDescriptionIsRespectedForCheckerCreation() throws Exception {
+ String checkerUuid = checkerOperations.newChecker().description("A simple checker.").create();
+
+ CheckerInfo checker = getCheckerFromServer(checkerUuid);
+ assertThat(checker.description).isEqualTo("A simple checker.");
+ }
+
+ @Test
+ public void requestingNoDescriptionIsPossibleForCheckerCreation() throws Exception {
+ String checkerUuid = checkerOperations.newChecker().clearDescription().create();
+
+ CheckerInfo checker = getCheckerFromServer(checkerUuid);
+ assertThat(checker.description).isNull();
+ }
+
+ @Test
+ public void existingCheckerCanBeCheckedForExistence() throws Exception {
+ String checkerUuid = createCheckerInServer(createArbitraryCheckerInput());
+
+ boolean exists = checkerOperations.checker(checkerUuid).exists();
+
+ assertThat(exists).isTrue();
+ }
+
+ @Test
+ public void notExistingCheckerCanBeCheckedForExistence() throws Exception {
+ String notExistingCheckerUuid = "not-existing-checker";
+
+ boolean exists = checkerOperations.checker(notExistingCheckerUuid).exists();
+
+ assertThat(exists).isFalse();
+ }
+
+ @Test
+ public void retrievingNotExistingCheckerFails() throws Exception {
+ String notExistingCheckerUuid = "not-existing-checker";
+
+ exception.expect(IllegalStateException.class);
+ checkerOperations.checker(notExistingCheckerUuid).get();
+ }
+
+ @Test
+ public void checkerNotCreatedByTestApiCanBeRetrieved() throws Exception {
+ CheckerInput input = createArbitraryCheckerInput();
+ input.name = "unique checker not created via test API";
+ String checkerUuid = createCheckerInServer(input);
+
+ TestChecker foundChecker = checkerOperations.checker(checkerUuid).get();
+
+ assertThat(foundChecker.uuid()).isEqualTo(checkerUuid);
+ assertThat(foundChecker.name()).isEqualTo("unique checker not created via test API");
+ }
+
+ @Test
+ public void uuidOfExistingCheckerCanBeRetrieved() throws Exception {
+ String checkerUuid = checkerOperations.newChecker().create();
+
+ String foundCheckerUuid = checkerOperations.checker(checkerUuid).get().uuid();
+
+ assertThat(foundCheckerUuid).isEqualTo(checkerUuid);
+ }
+
+ @Test
+ public void nameOfExistingCheckerCanBeRetrieved() throws Exception {
+ String checkerUuid =
+ checkerOperations.newChecker().name("ABC-789-this-name-must-be-unique").create();
+
+ String checkerName = checkerOperations.checker(checkerUuid).get().name();
+
+ assertThat(checkerName).isEqualTo("ABC-789-this-name-must-be-unique");
+ }
+
+ @Test
+ public void descriptionOfExistingCheckerCanBeRetrieved() throws Exception {
+ String checkerUuid =
+ checkerOperations
+ .newChecker()
+ .description("This is a very detailed description of this checker.")
+ .create();
+
+ Optional<String> description = checkerOperations.checker(checkerUuid).get().description();
+
+ assertThat(description).hasValue("This is a very detailed description of this checker.");
+ }
+
+ @Test
+ public void emptyDescriptionOfExistingCheckerCanBeRetrieved() throws Exception {
+ String checkerUuid = checkerOperations.newChecker().clearDescription().create();
+
+ Optional<String> description = checkerOperations.checker(checkerUuid).get().description();
+
+ assertThat(description).isEmpty();
+ }
+
+ @Test
+ public void createdOnOfExistingCheckerCanBeRetrieved() throws Exception {
+ CheckerInfo checker = checkersApi.create(createArbitraryCheckerInput()).get();
+
+ Timestamp createdOn = checkerOperations.checker(checker.uuid).get().createdOn();
+
+ assertThat(createdOn).isEqualTo(checker.createdOn);
+ }
+
+ @Test
+ public void updateWithoutAnyParametersIsANoop() throws Exception {
+ String checkerUuid = checkerOperations.newChecker().create();
+ TestChecker originalChecker = checkerOperations.checker(checkerUuid).get();
+
+ checkerOperations.checker(checkerUuid).forUpdate().update();
+
+ TestChecker updatedChecker = checkerOperations.checker(checkerUuid).get();
+ assertThat(updatedChecker).isEqualTo(originalChecker);
+ }
+
+ @Test
+ public void updateWritesToInternalCheckerSystem() throws Exception {
+ String checkerUuid =
+ checkerOperations.newChecker().description("original description").create();
+
+ checkerOperations.checker(checkerUuid).forUpdate().description("updated description").update();
+
+ String currentDescription = getCheckerFromServer(checkerUuid).description;
+ assertThat(currentDescription).isEqualTo("updated description");
+ }
+
+ @Test
+ public void nameCanBeUpdated() throws Exception {
+ String checkerUuid = checkerOperations.newChecker().name("original name").create();
+
+ checkerOperations.checker(checkerUuid).forUpdate().name("updated name").update();
+
+ String currentName = checkerOperations.checker(checkerUuid).get().name();
+ assertThat(currentName).isEqualTo("updated name");
+ }
+
+ @Test
+ public void descriptionCanBeUpdated() throws Exception {
+ String checkerUuid =
+ checkerOperations.newChecker().description("original description").create();
+
+ checkerOperations.checker(checkerUuid).forUpdate().description("updated description").update();
+
+ Optional<String> currentDescription =
+ checkerOperations.checker(checkerUuid).get().description();
+ assertThat(currentDescription).hasValue("updated description");
+ }
+
+ @Test
+ public void descriptionCanBeCleared() throws Exception {
+ String checkerUuid =
+ checkerOperations.newChecker().description("original description").create();
+
+ checkerOperations.checker(checkerUuid).forUpdate().clearDescription().update();
+
+ Optional<String> currentDescription =
+ checkerOperations.checker(checkerUuid).get().description();
+ assertThat(currentDescription).isEmpty();
+ }
+
+ @Test
+ public void statusCanBeUpdated() throws Exception {
+ String checkerUuid =
+ checkerOperations.newChecker().description("original description").create();
+ assertThat(checkerOperations.checker(checkerUuid).asInfo().status)
+ .isEqualTo(CheckerStatus.ENABLED);
+
+ checkerOperations.checker(checkerUuid).forUpdate().disable().update();
+ assertThat(checkerOperations.checker(checkerUuid).asInfo().status)
+ .isEqualTo(CheckerStatus.DISABLED);
+
+ checkerOperations.checker(checkerUuid).forUpdate().enable().update();
+ assertThat(checkerOperations.checker(checkerUuid).asInfo().status)
+ .isEqualTo(CheckerStatus.ENABLED);
+ }
+
+ @Test
+ public void getCommit() throws Exception {
+ CheckerInfo checker = checkersApi.create(createArbitraryCheckerInput()).get();
+
+ RevCommit commit = checkerOperations.checker(checker.uuid).commit();
+ assertThat(commit).isEqualTo(readCheckerCommitSha1(checker.uuid));
+ }
+
+ private ObjectId readCheckerCommitSha1(String checkerUuid) throws IOException {
+ try (Repository repo = repoManager.openRepository(allProjects)) {
+ return repo.exactRef(CheckerRef.refsCheckers(checkerUuid)).getObjectId();
+ }
+ }
+
+ @Test
+ public void getConfigText() throws Exception {
+ CheckerInfo checker = checkersApi.create(createArbitraryCheckerInput()).get();
+
+ String configText = checkerOperations.checker(checker.uuid).configText();
+ assertThat(configText).isEqualTo(readCheckerConfigFile(checker.uuid));
+ }
+
+ private String readCheckerConfigFile(String checkerUuid) throws IOException {
+ try (Repository repo = repoManager.openRepository(allProjects);
+ RevWalk rw = new RevWalk(repo);
+ ObjectReader or = repo.newObjectReader()) {
+ Ref checkerRef = repo.exactRef(CheckerRef.refsCheckers(checkerUuid));
+ RevCommit commit = rw.parseCommit(checkerRef.getObjectId());
+ try (TreeWalk tw =
+ TreeWalk.forPath(or, CheckerConfig.CHECKER_CONFIG_FILE, commit.getTree())) {
+ return new String(or.open(tw.getObjectId(0), OBJ_BLOB).getBytes(), UTF_8);
+ }
+ }
+ }
+
+ @Test
+ public void asInfo() throws Exception {
+ String checkerUuid =
+ checkerOperations
+ .newChecker()
+ .name("my-checker")
+ .description("A description.")
+ .url("http://example.com/my-checker")
+ .create();
+ TestChecker checker = checkerOperations.checker(checkerUuid).get();
+ CheckerInfo checkerInfo = checkerOperations.checker(checkerUuid).asInfo();
+ assertThat(checkerInfo.uuid).isEqualTo(checker.uuid());
+ assertThat(checkerInfo.name).isEqualTo(checker.name());
+ assertThat(checkerInfo.description).isEqualTo(checker.description().get());
+ assertThat(checkerInfo.url).isEqualTo(checker.url().get());
+ assertThat(checkerInfo.createdOn).isEqualTo(checker.createdOn());
+ assertThat(checkerInfo.updatedOn).isEqualTo(checker.updatedOn());
+ }
+
+ @Test
+ public void getCheckersOfRepository() throws Exception {
+ String checkerUuid1 = CheckerUuid.make("my-checker1");
+ String checkerUuid2 = CheckerUuid.make("my-checker2");
+
+ try (Repository repo = repoManager.openRepository(allProjects)) {
+ new TestRepository<>(repo)
+ .branch(CheckerRef.REFS_META_CHECKERS)
+ .commit()
+ .add(
+ CheckersByRepositoryNotes.computeRepositorySha1(project).getName(),
+ Joiner.on('\n').join(checkerUuid1, checkerUuid2))
+ .create();
+ }
+
+ assertThat(checkerOperations.checkersOf(project)).containsExactly(checkerUuid1, checkerUuid2);
+ }
+
+ @Test
+ public void getCheckersOfRepositoryWithoutCheckers() throws Exception {
+ assertThat(checkerOperations.checkersOf(project)).isEmpty();
+ }
+
+ @Test
+ public void getCheckersOfNonExistingRepositor() throws Exception {
+ assertThat(checkerOperations.checkersOf(new Project.NameKey("non-existing"))).isEmpty();
+ }
+
+ @Test
+ public void getSha1sOfRepositoriesWithCheckers() throws Exception {
+ String checkerUuid1 = CheckerUuid.make("my-checker1");
+ String checkerUuid2 = CheckerUuid.make("my-checker2");
+
+ try (Repository repo = repoManager.openRepository(allProjects)) {
+ new TestRepository<>(repo)
+ .branch(CheckerRef.REFS_META_CHECKERS)
+ .commit()
+ .add(CheckersByRepositoryNotes.computeRepositorySha1(project).getName(), checkerUuid1)
+ .add(CheckersByRepositoryNotes.computeRepositorySha1(allProjects).getName(), checkerUuid2)
+ .create();
+ }
+
+ assertThat(checkerOperations.sha1sOfRepositoriesWithCheckers())
+ .containsExactly(
+ CheckersByRepositoryNotes.computeRepositorySha1(project),
+ CheckersByRepositoryNotes.computeRepositorySha1(allProjects));
+ }
+
+ private CheckerInput createArbitraryCheckerInput() {
+ CheckerInput checkerInput = new CheckerInput();
+ checkerInput.name = name("test-checker");
+ checkerInput.repository = allProjects.get();
+ return checkerInput;
+ }
+
+ private CheckerInfo getCheckerFromServer(String checkerUuid) throws RestApiException {
+ return checkersApi.id(checkerUuid).get();
+ }
+
+ private String createCheckerInServer(CheckerInput input) throws RestApiException {
+ CheckerInfo checker = checkersApi.create(input).get();
+ return checker.uuid;
+ }
+}
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/CheckerRefsIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/CheckerRefsIT.java
new file mode 100644
index 0000000..fa02f71
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/CheckerRefsIT.java
@@ -0,0 +1,195 @@
+// 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.google.gerrit.plugins.checks.acceptance;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.GitUtil.deleteRef;
+import static com.google.gerrit.acceptance.GitUtil.fetch;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.SkipProjectClone;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.plugins.checks.CheckerRef;
+import com.google.gerrit.plugins.checks.CheckerUuid;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.change.ChangeInserter;
+import com.google.gerrit.server.notedb.Sequences;
+import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.util.time.TimeUtil;
+import com.google.inject.Inject;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.PushResult;
+import org.eclipse.jgit.transport.RemoteRefUpdate;
+import org.junit.Test;
+
+@SkipProjectClone
+public class CheckerRefsIT extends AbstractCheckersTest {
+ @Inject private RequestScopeOperations requestScopeOperations;
+ @Inject private Sequences seq;
+ @Inject private ChangeInserter.Factory changeInserterFactory;
+ @Inject private BatchUpdate.Factory updateFactory;
+
+ @Test
+ public void cannotCreateCheckerRef() throws Exception {
+ grant(allProjects, CheckerRef.REFS_CHECKERS + "*", Permission.CREATE);
+ grant(allProjects, CheckerRef.REFS_CHECKERS + "*", Permission.PUSH);
+
+ String checkerRef = CheckerRef.refsCheckers(CheckerUuid.make("my-checker"));
+
+ TestRepository<InMemoryRepository> testRepo = cloneProject(allProjects);
+ PushOneCommit.Result r = pushFactory.create(admin.getIdent(), testRepo).to(checkerRef);
+ r.assertErrorStatus();
+ assertThat(r.getMessage()).contains("Not allowed to create checker ref.");
+
+ try (Repository repo = repoManager.openRepository(allProjects)) {
+ assertThat(repo.exactRef(checkerRef)).isNull();
+ }
+ }
+
+ @Test
+ public void cannotDeleteCheckerRef() throws Exception {
+ grant(allProjects, CheckerRef.REFS_CHECKERS + "*", Permission.DELETE, true, REGISTERED_USERS);
+
+ String checkerUuid = checkerOperations.newChecker().create();
+ String checkerRef = CheckerRef.refsCheckers(checkerUuid);
+
+ TestRepository<InMemoryRepository> testRepo = cloneProject(allProjects);
+ PushResult r = deleteRef(testRepo, checkerRef);
+ RemoteRefUpdate refUpdate = r.getRemoteUpdate(checkerRef);
+ assertThat(refUpdate.getStatus()).isEqualTo(RemoteRefUpdate.Status.REJECTED_OTHER_REASON);
+ assertThat(refUpdate.getMessage()).contains("Not allowed to delete checker ref.");
+
+ try (Repository repo = repoManager.openRepository(allProjects)) {
+ assertThat(repo.exactRef(checkerRef)).isNotNull();
+ }
+ }
+
+ @Test
+ public void updateCheckerRefsByPushIsDisabled() throws Exception {
+ String checkerUuid = checkerOperations.newChecker().create();
+ String checkerRef = CheckerRef.refsCheckers(checkerUuid);
+
+ TestRepository<InMemoryRepository> repo = cloneProject(allProjects, admin);
+ fetch(repo, checkerRef + ":checkerRef");
+ repo.reset("checkerRef");
+
+ grant(allProjects, CheckerRef.REFS_CHECKERS + "*", Permission.PUSH);
+ PushOneCommit.Result r = pushFactory.create(admin.getIdent(), repo).to(checkerRef);
+ r.assertErrorStatus();
+ r.assertMessage("direct update of checker ref not allowed");
+ }
+
+ @Test
+ public void submitToCheckerRefsIsDisabled() throws Exception {
+ String checkerUuid = checkerOperations.newChecker().create();
+ String checkerRef = CheckerRef.refsCheckers(checkerUuid);
+
+ String changeId = createChangeWithoutCommitValidation(checkerRef);
+
+ grantLabel(
+ "Code-Review",
+ -2,
+ 2,
+ allProjects,
+ CheckerRef.REFS_CHECKERS + "*",
+ false,
+ adminGroupUuid(),
+ false);
+ approve(changeId);
+
+ grant(allProjects, CheckerRef.REFS_CHECKERS + "*", Permission.SUBMIT);
+
+ exception.expect(ResourceConflictException.class);
+ exception.expectMessage("submit to checker ref not allowed");
+ gApi.changes().id(changeId).current().submit();
+ }
+
+ @Test
+ public void createChangeForCheckerRefsByPushIsDisabled() throws Exception {
+ String checkerUuid = checkerOperations.newChecker().create();
+ String checkerRef = CheckerRef.refsCheckers(checkerUuid);
+
+ TestRepository<InMemoryRepository> repo = cloneProject(allProjects, admin);
+ fetch(repo, checkerRef + ":checkerRef");
+ repo.reset("checkerRef");
+
+ grant(allProjects, CheckerRef.REFS_CHECKERS + "*", Permission.PUSH);
+ PushOneCommit.Result r =
+ pushFactory.create(admin.getIdent(), repo).to("refs/for/" + checkerRef);
+ r.assertErrorStatus();
+ r.assertMessage("creating change for checker ref not allowed");
+ }
+
+ @Test
+ public void createChangeForCheckerRefsViaApiIsDisabled() throws Exception {
+ String checkerUuid = checkerOperations.newChecker().create();
+ String checkerRef = CheckerRef.refsCheckers(checkerUuid);
+
+ TestRepository<InMemoryRepository> repo = cloneProject(allProjects, admin);
+ fetch(repo, checkerRef + ":checkerRef");
+ repo.reset("checkerRef");
+ RevCommit head = getHead(repo.getRepository(), "HEAD");
+
+ ChangeInput input = new ChangeInput();
+ input.project = allProjects.get();
+ input.branch = checkerRef;
+ input.baseCommit = head.name();
+ input.subject = "A change.";
+
+ exception.expect(ResourceConflictException.class);
+ exception.expectMessage("creating change for checker ref not allowed");
+ gApi.changes().create(input);
+ }
+
+ private String createChangeWithoutCommitValidation(String targetRef) throws Exception {
+ try (Repository git = repoManager.openRepository(allProjects);
+ ObjectInserter oi = git.newObjectInserter();
+ ObjectReader reader = oi.newReader();
+ RevWalk rw = new RevWalk(reader)) {
+ RevCommit head = rw.parseCommit(git.exactRef(targetRef).getObjectId());
+ RevCommit commit =
+ new TestRepository<>(git)
+ .commit()
+ .author(admin.getIdent())
+ .message("A change.")
+ .insertChangeId()
+ .parent(head)
+ .create();
+
+ Change.Id changeId = new Change.Id(seq.nextChangeId());
+ ChangeInserter ins = changeInserterFactory.create(changeId, commit, targetRef);
+ ins.setValidate(false);
+ ins.setMessage(String.format("Uploaded patch set %s.", ins.getPatchSetId().get()));
+ try (BatchUpdate bu =
+ updateFactory.create(
+ allProjects, identifiedUserFactory.create(admin.id), TimeUtil.nowTs())) {
+ bu.setRepository(git, rw, oi);
+ bu.insertChange(ins);
+ bu.execute();
+ }
+ return changeId.toString();
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/ChecksRestApiBindingsIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/ChecksRestApiBindingsIT.java
new file mode 100644
index 0000000..84f568e
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/ChecksRestApiBindingsIT.java
@@ -0,0 +1,33 @@
+// 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.google.gerrit.plugins.checks.acceptance;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.rest.util.RestApiCallHelper;
+import com.google.gerrit.acceptance.rest.util.RestCall;
+import org.junit.Test;
+
+public class ChecksRestApiBindingsIT extends AbstractCheckersTest {
+ private static final ImmutableList<RestCall> CHECKER_ENDPOINTS =
+ ImmutableList.of(
+ RestCall.get("/plugins/checks/checkers/%s"),
+ RestCall.post("/plugins/checks/checkers/%s"));
+
+ @Test
+ public void checkerEndpoints() throws Exception {
+ String checkerUuid = checkerOperations.newChecker().create();
+ RestApiCallHelper.execute(adminRestSession, CHECKER_ENDPOINTS, checkerUuid);
+ }
+}
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/GetCheckerIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/GetCheckerIT.java
new file mode 100644
index 0000000..c02a485
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/GetCheckerIT.java
@@ -0,0 +1,85 @@
+// 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.google.gerrit.plugins.checks.acceptance;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.plugins.checks.CheckerUuid;
+import com.google.gerrit.plugins.checks.api.CheckerInfo;
+import com.google.inject.Inject;
+import org.junit.Test;
+
+public class GetCheckerIT extends AbstractCheckersTest {
+ @Inject private RequestScopeOperations requestScopeOperations;
+
+ @Test
+ public void getChecker() throws Exception {
+ String name = "my-checker";
+ String uuid = checkerOperations.newChecker().name(name).create();
+
+ CheckerInfo info = checkersApi.id(uuid).get();
+ assertThat(info.uuid).isEqualTo(uuid);
+ assertThat(info.name).isEqualTo(name);
+ assertThat(info.description).isNull();
+ assertThat(info.createdOn).isNotNull();
+ }
+
+ @Test
+ public void getCheckerWithDescription() throws Exception {
+ String name = "my-checker";
+ String description = "some description";
+ String uuid = checkerOperations.newChecker().name(name).description(description).create();
+
+ CheckerInfo info = checkersApi.id(uuid).get();
+ assertThat(info.uuid).isEqualTo(uuid);
+ assertThat(info.name).isEqualTo(name);
+ assertThat(info.description).isEqualTo(description);
+ assertThat(info.createdOn).isNotNull();
+ }
+
+ @Test
+ public void getNonExistingCheckerFails() throws Exception {
+ String checkerUuid = CheckerUuid.make("non-existing");
+
+ exception.expect(ResourceNotFoundException.class);
+ exception.expectMessage("Not found: " + checkerUuid);
+ checkersApi.id(checkerUuid);
+ }
+
+ @Test
+ public void getCheckerByNameFails() throws Exception {
+ String name = "my-checker";
+ checkerOperations.newChecker().name(name).create();
+
+ exception.expect(ResourceNotFoundException.class);
+ exception.expectMessage("Not found: " + name);
+ checkersApi.id(name);
+ }
+
+ @Test
+ public void getCheckerWithoutAdministrateCheckersCapabilityFails() throws Exception {
+ String name = "my-checker";
+ String uuid = checkerOperations.newChecker().name(name).create();
+
+ requestScopeOperations.setApiUser(user.getId());
+
+ exception.expect(AuthException.class);
+ exception.expectMessage("administrateCheckers for plugin checks not permitted");
+ checkersApi.id(uuid);
+ }
+}
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/api/BUILD b/javatests/com/google/gerrit/plugins/checks/acceptance/api/BUILD
new file mode 100644
index 0000000..4404551
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/api/BUILD
@@ -0,0 +1,17 @@
+load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
+
+acceptance_tests(
+ srcs = glob(["*IT.java"]),
+ group = "api_checker",
+ labels = [
+ "api",
+ "noci",
+ ],
+ deps = [
+ "//java/com/google/gerrit/server/testing",
+ "//java/com/google/gerrit/server/util/time",
+ "//plugins/checks:checks__plugin",
+ "//plugins/checks/java/com/google/gerrit/plugins/checks/acceptance",
+ "//plugins/checks/java/com/google/gerrit/plugins/checks/acceptance/testsuite",
+ ],
+)
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/api/CreateCheckerIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/api/CreateCheckerIT.java
new file mode 100644
index 0000000..1e3daf6
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/api/CreateCheckerIT.java
@@ -0,0 +1,305 @@
+// 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.google.gerrit.plugins.checks.acceptance.api;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.testing.CommitSubject.assertCommit;
+
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.plugins.checks.acceptance.AbstractCheckersTest;
+import com.google.gerrit.plugins.checks.acceptance.testsuite.CheckerOperations.PerCheckerOperations;
+import com.google.gerrit.plugins.checks.api.CheckerInfo;
+import com.google.gerrit.plugins.checks.api.CheckerInput;
+import com.google.gerrit.plugins.checks.api.CheckerStatus;
+import com.google.gerrit.plugins.checks.db.CheckersByRepositoryNotes;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.testing.TestTimeUtil;
+import com.google.inject.Inject;
+import java.util.concurrent.TimeUnit;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class CreateCheckerIT extends AbstractCheckersTest {
+ @Inject private RequestScopeOperations requestScopeOperations;
+ @Inject private ProjectOperations projectOperations;
+
+ @Before
+ public void setTimeForTesting() {
+ TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS);
+ }
+
+ @After
+ public void resetTime() {
+ TestTimeUtil.useSystemTime();
+ }
+
+ @Test
+ public void createChecker() throws Exception {
+ Project.NameKey repositoryName = projectOperations.newProject().create();
+
+ CheckerInput input = new CheckerInput();
+ input.name = "my-checker";
+ input.repository = repositoryName.get();
+ CheckerInfo info = checkersApi.create(input).get();
+ assertThat(info.uuid).isNotNull();
+ assertThat(info.name).isEqualTo(input.name);
+ assertThat(info.description).isNull();
+ assertThat(info.url).isNull();
+ assertThat(info.repository).isEqualTo(input.repository);
+ assertThat(info.status).isEqualTo(CheckerStatus.ENABLED);
+ assertThat(info.createdOn).isNotNull();
+ assertThat(info.updatedOn).isEqualTo(info.createdOn);
+
+ PerCheckerOperations perCheckerOps = checkerOperations.checker(info.uuid);
+ assertCommit(
+ perCheckerOps.commit(), "Create checker", info.createdOn, perCheckerOps.get().refState());
+ assertThat(checkerOperations.sha1sOfRepositoriesWithCheckers())
+ .containsExactly(CheckersByRepositoryNotes.computeRepositorySha1(repositoryName));
+ assertThat(checkerOperations.checkersOf(repositoryName)).containsExactly(info.uuid);
+ }
+
+ @Test
+ public void createCheckerWithDescription() throws Exception {
+ CheckerInput input = new CheckerInput();
+ input.name = "my-checker";
+ input.description = "some description";
+ input.repository = allProjects.get();
+ CheckerInfo info = checkersApi.create(input).get();
+ assertThat(info.description).isEqualTo(input.description);
+
+ PerCheckerOperations perCheckerOps = checkerOperations.checker(info.uuid);
+ assertCommit(
+ perCheckerOps.commit(), "Create checker", info.createdOn, perCheckerOps.get().refState());
+ }
+
+ @Test
+ public void createCheckerWithUrl() throws Exception {
+ CheckerInput input = new CheckerInput();
+ input.name = "my-checker";
+ input.url = "http://example.com/my-checker";
+ input.repository = allProjects.get();
+ CheckerInfo info = checkersApi.create(input).get();
+ assertThat(info.url).isEqualTo(input.url);
+
+ PerCheckerOperations perCheckerOps = checkerOperations.checker(info.uuid);
+ assertCommit(
+ perCheckerOps.commit(), "Create checker", info.createdOn, perCheckerOps.get().refState());
+ }
+
+ @Test
+ public void createCheckerNameIsTrimmed() throws Exception {
+ CheckerInput input = new CheckerInput();
+ input.name = " my-checker ";
+ input.repository = allProjects.get();
+ CheckerInfo info = checkersApi.create(input).get();
+ assertThat(info.name).isEqualTo("my-checker");
+
+ PerCheckerOperations perCheckerOps = checkerOperations.checker(info.uuid);
+ assertCommit(
+ perCheckerOps.commit(), "Create checker", info.createdOn, perCheckerOps.get().refState());
+ }
+
+ @Test
+ public void createCheckerDescriptionIsTrimmed() throws Exception {
+ CheckerInput input = new CheckerInput();
+ input.name = "my-checker";
+ input.description = " some description ";
+ input.repository = allProjects.get();
+ CheckerInfo info = checkersApi.create(input).get();
+ assertThat(info.description).isEqualTo("some description");
+
+ PerCheckerOperations perCheckerOps = checkerOperations.checker(info.uuid);
+ assertCommit(
+ perCheckerOps.commit(), "Create checker", info.createdOn, perCheckerOps.get().refState());
+ }
+
+ @Test
+ public void createCheckerUrlIsTrimmed() throws Exception {
+ CheckerInput input = new CheckerInput();
+ input.name = "my-checker";
+ input.url = " http://example.com/my-checker ";
+ input.repository = allProjects.get();
+ CheckerInfo info = checkersApi.create(input).get();
+ assertThat(info.url).isEqualTo("http://example.com/my-checker");
+
+ PerCheckerOperations perCheckerOps = checkerOperations.checker(info.uuid);
+ assertCommit(
+ perCheckerOps.commit(), "Create checker", info.createdOn, perCheckerOps.get().refState());
+ }
+
+ @Test
+ public void createCheckerRepositoryIsTrimmed() throws Exception {
+ CheckerInput input = new CheckerInput();
+ input.name = "my-checker";
+ input.repository = " " + allProjects.get() + " ";
+ CheckerInfo info = checkersApi.create(input).get();
+ assertThat(info.repository).isEqualTo(allProjects.get());
+
+ PerCheckerOperations perCheckerOps = checkerOperations.checker(info.uuid);
+ assertCommit(
+ perCheckerOps.commit(), "Create checker", info.createdOn, perCheckerOps.get().refState());
+ }
+
+ @Test
+ public void createCheckerWithInvalidUrlFails() throws Exception {
+ String checkerUuid = checkerOperations.newChecker().name("my-checker").create();
+
+ CheckerInput input = new CheckerInput();
+ input.name = "my-checker";
+ input.url = "ftp://example.com/my-checker";
+ exception.expect(BadRequestException.class);
+ exception.expectMessage("only http/https URLs supported: ftp://example.com/my-checker");
+ checkersApi.id(checkerUuid).update(input);
+ }
+
+ @Test
+ public void createCheckersWithSameName() throws Exception {
+ CheckerInput input = new CheckerInput();
+ input.name = "my-checker";
+ input.repository = allProjects.get();
+ CheckerInfo info1 = checkersApi.create(input).get();
+ assertThat(info1.name).isEqualTo(input.name);
+
+ CheckerInfo info2 = checkersApi.create(input).get();
+ assertThat(info2.name).isEqualTo(input.name);
+
+ assertThat(info2.uuid).isNotEqualTo(info1.uuid);
+ }
+
+ @Test
+ public void createCheckerWithoutNameFails() throws Exception {
+ CheckerInput input = new CheckerInput();
+ input.repository = allProjects.get();
+
+ exception.expect(BadRequestException.class);
+ exception.expectMessage("name is required");
+ checkersApi.create(input);
+ }
+
+ @Test
+ public void createCheckerWithEmptyNameFails() throws Exception {
+ CheckerInput input = new CheckerInput();
+ input.name = "";
+ input.repository = allProjects.get();
+
+ exception.expect(BadRequestException.class);
+ exception.expectMessage("name is required");
+ checkersApi.create(input);
+ }
+
+ @Test
+ public void createCheckerWithEmptyNameAfterTrimFails() throws Exception {
+ CheckerInput input = new CheckerInput();
+ input.name = " ";
+ input.repository = allProjects.get();
+
+ exception.expect(BadRequestException.class);
+ exception.expectMessage("name is required");
+ checkersApi.create(input);
+ }
+
+ @Test
+ public void createCheckerWithoutRepositoryFails() throws Exception {
+ CheckerInput input = new CheckerInput();
+ input.name = "my-checker";
+
+ exception.expect(BadRequestException.class);
+ exception.expectMessage("repository is required");
+ checkersApi.create(input);
+ }
+
+ @Test
+ public void createCheckerWithEmptyRepositoryFails() throws Exception {
+ CheckerInput input = new CheckerInput();
+ input.name = "my-checker";
+ input.repository = "";
+
+ exception.expect(BadRequestException.class);
+ exception.expectMessage("repository is required");
+ checkersApi.create(input);
+ }
+
+ @Test
+ public void createCheckerWithEmptyRepositoryAfterTrimFails() throws Exception {
+ CheckerInput input = new CheckerInput();
+ input.name = "my-checker";
+ input.repository = " ";
+
+ exception.expect(BadRequestException.class);
+ exception.expectMessage("repository is required");
+ checkersApi.create(input);
+ }
+
+ @Test
+ public void createCheckerWithNonExistingRepositoryFails() throws Exception {
+ CheckerInput input = new CheckerInput();
+ input.name = "my-checker";
+ input.repository = "non-existing";
+
+ exception.expect(UnprocessableEntityException.class);
+ exception.expectMessage("repository non-existing not found");
+ checkersApi.create(input);
+ }
+
+ @Test
+ public void createDisabledChecker() throws Exception {
+ CheckerInput input = new CheckerInput();
+ input.name = "my-checker";
+ input.repository = allProjects.get();
+ input.status = CheckerStatus.DISABLED;
+
+ CheckerInfo info = checkersApi.create(input).get();
+ assertThat(info.status).isEqualTo(CheckerStatus.DISABLED);
+ }
+
+ @Test
+ public void createMultipleCheckers() throws Exception {
+ Project.NameKey repositoryName1 = projectOperations.newProject().create();
+ Project.NameKey repositoryName2 = projectOperations.newProject().create();
+
+ String checkerUuid1 = checkerOperations.newChecker().repository(repositoryName1).create();
+ String checkerUuid2 = checkerOperations.newChecker().repository(repositoryName1).create();
+ String checkerUuid3 = checkerOperations.newChecker().repository(repositoryName1).create();
+ String checkerUuid4 = checkerOperations.newChecker().repository(repositoryName2).create();
+ String checkerUuid5 = checkerOperations.newChecker().repository(repositoryName2).create();
+
+ assertThat(checkerOperations.sha1sOfRepositoriesWithCheckers())
+ .containsExactly(
+ CheckersByRepositoryNotes.computeRepositorySha1(repositoryName1),
+ CheckersByRepositoryNotes.computeRepositorySha1(repositoryName2));
+ assertThat(checkerOperations.checkersOf(repositoryName1))
+ .containsExactly(checkerUuid1, checkerUuid2, checkerUuid3);
+ assertThat(checkerOperations.checkersOf(repositoryName2))
+ .containsExactly(checkerUuid4, checkerUuid5);
+ }
+
+ @Test
+ public void createCheckerWithoutAdministrateCheckersCapabilityFails() throws Exception {
+ requestScopeOperations.setApiUser(user.getId());
+
+ CheckerInput input = new CheckerInput();
+ input.name = "my-checker";
+ input.repository = allProjects.get();
+
+ exception.expect(AuthException.class);
+ exception.expectMessage("administrateCheckers for plugin checks not permitted");
+ checkersApi.create(input);
+ }
+}
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/api/ListCheckersIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/api/ListCheckersIT.java
new file mode 100644
index 0000000..71b7a77
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/api/ListCheckersIT.java
@@ -0,0 +1,96 @@
+// 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.google.gerrit.plugins.checks.acceptance.api;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
+import static java.util.stream.Collectors.toList;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.plugins.checks.CheckerRef;
+import com.google.gerrit.plugins.checks.CheckerUuid;
+import com.google.gerrit.plugins.checks.acceptance.AbstractCheckersTest;
+import com.google.gerrit.plugins.checks.api.CheckerInfo;
+import com.google.gerrit.plugins.checks.db.CheckerConfig;
+import com.google.inject.Inject;
+import java.util.List;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Test;
+
+public class ListCheckersIT extends AbstractCheckersTest {
+ @Inject private RequestScopeOperations requestScopeOperations;
+
+ @Test
+ public void listAll() throws Exception {
+ String checkerUuid1 = checkerOperations.newChecker().name("checker-with-name-only").create();
+ String checkerUuid2 =
+ checkerOperations
+ .newChecker()
+ .name("checker-with-description")
+ .description("A description.")
+ .create();
+ String checkerUuid3 =
+ checkerOperations
+ .newChecker()
+ .name("checker-with-url")
+ .url("http://example.com/my-checker")
+ .create();
+ List<CheckerInfo> expectedCheckerInfos =
+ ImmutableList.of(checkerUuid1, checkerUuid2, checkerUuid3)
+ .stream()
+ .sorted()
+ .map(uuid -> checkerOperations.checker(uuid).asInfo())
+ .collect(toList());
+
+ List<CheckerInfo> allCheckers = checkersApi.all();
+ assertThat(allCheckers).isEqualTo(expectedCheckerInfos);
+ }
+
+ @Test
+ public void listWithoutAdministrateCheckersCapabilityFails() throws Exception {
+ checkerOperations.newChecker().name("my-checker").create();
+
+ requestScopeOperations.setApiUser(user.getId());
+
+ try {
+ checkersApi.all();
+ assert_().fail("expected AuthException");
+ } catch (AuthException e) {
+ assertThat(e.getMessage()).isEqualTo("administrateCheckers for plugin checks not permitted");
+ }
+ }
+
+ @Test
+ public void listIgnoresInvalidCheckers() throws Exception {
+ String checkerUuid = checkerOperations.newChecker().name("checker-with-name-only").create();
+ createInvalidChecker();
+
+ List<CheckerInfo> allCheckers = checkersApi.all();
+ assertThat(allCheckers).containsExactly(checkerOperations.checker(checkerUuid).asInfo());
+ }
+
+ private void createInvalidChecker() throws Exception {
+ try (Repository repo = repoManager.openRepository(allProjects)) {
+ new TestRepository<>(repo)
+ .branch(CheckerRef.refsCheckers(CheckerUuid.make("my-checker")))
+ .commit()
+ .add(CheckerConfig.CHECKER_CONFIG_FILE, "invalid-config")
+ .create();
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/api/UpdateCheckerIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/api/UpdateCheckerIT.java
new file mode 100644
index 0000000..f6f3b0e
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/api/UpdateCheckerIT.java
@@ -0,0 +1,437 @@
+// 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.google.gerrit.plugins.checks.acceptance.api;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.testing.CommitSubject.assertCommit;
+
+import com.google.gerrit.acceptance.SkipProjectClone;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.plugins.checks.acceptance.AbstractCheckersTest;
+import com.google.gerrit.plugins.checks.acceptance.testsuite.CheckerOperations.PerCheckerOperations;
+import com.google.gerrit.plugins.checks.acceptance.testsuite.TestChecker;
+import com.google.gerrit.plugins.checks.api.CheckerInfo;
+import com.google.gerrit.plugins.checks.api.CheckerInput;
+import com.google.gerrit.plugins.checks.api.CheckerStatus;
+import com.google.gerrit.plugins.checks.db.CheckersByRepositoryNotes;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.gerrit.testing.TestTimeUtil;
+import com.google.inject.Inject;
+import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.lib.Config;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+@SkipProjectClone
+public class UpdateCheckerIT extends AbstractCheckersTest {
+ @Inject private RequestScopeOperations requestScopeOperations;
+ @Inject private ProjectOperations projectOperations;
+
+ @ConfigSuite.Default
+ public static Config defaultConfig() {
+ Config cfg = new Config();
+ cfg.setBoolean("checks", "api", "enabled", true);
+ return cfg;
+ }
+
+ @Before
+ public void setTimeForTesting() {
+ TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS);
+ }
+
+ @After
+ public void resetTime() {
+ TestTimeUtil.useSystemTime();
+ }
+
+ @Test
+ public void updateMultipleCheckerPropertiesAtOnce() throws Exception {
+ String checkerUuid =
+ checkerOperations.newChecker().name("my-checker").repository(allProjects).create();
+ TestChecker checker = checkerOperations.checker(checkerUuid).get();
+
+ Project.NameKey repositoryName = projectOperations.newProject().create();
+
+ CheckerInput input = new CheckerInput();
+ input.name = "my-renamed-checker";
+ input.description = "A description.";
+ input.url = "http://example.com/my-checker";
+ input.repository = repositoryName.get();
+
+ CheckerInfo info = checkersApi.id(checkerUuid).update(input);
+ assertThat(info.uuid).isEqualTo(checkerUuid);
+ assertThat(info.name).isEqualTo(input.name);
+ assertThat(info.description).isEqualTo(input.description);
+ assertThat(info.url).isEqualTo(input.url);
+ assertThat(info.repository).isEqualTo(input.repository);
+ assertThat(info.createdOn).isEqualTo(checker.createdOn());
+ assertThat(info.createdOn).isLessThan(info.updatedOn);
+
+ PerCheckerOperations perCheckerOps = checkerOperations.checker(checkerUuid);
+ assertCommit(
+ perCheckerOps.commit(),
+ "Update checker\n\nRename from my-checker to my-renamed-checker",
+ info.updatedOn,
+ perCheckerOps.get().refState());
+ assertThat(checkerOperations.sha1sOfRepositoriesWithCheckers())
+ .containsExactly(CheckersByRepositoryNotes.computeRepositorySha1(repositoryName));
+ assertThat(checkerOperations.checkersOf(repositoryName)).containsExactly(info.uuid);
+ }
+
+ @Test
+ public void updateCheckerName() throws Exception {
+ String checkerUuid = checkerOperations.newChecker().name("my-checker").create();
+
+ CheckerInput input = new CheckerInput();
+ input.name = "my-renamed-checker";
+
+ CheckerInfo info = checkersApi.id(checkerUuid).update(input);
+ assertThat(info.name).isEqualTo(input.name);
+
+ PerCheckerOperations perCheckerOps = checkerOperations.checker(checkerUuid);
+ assertCommit(
+ perCheckerOps.commit(),
+ "Update checker\n\nRename from my-checker to my-renamed-checker",
+ info.updatedOn,
+ perCheckerOps.get().refState());
+ }
+
+ @Test
+ public void cannotSetCheckerNameToEmptyString() throws Exception {
+ String checkerUuid = checkerOperations.newChecker().name("my-checker").create();
+
+ CheckerInput checkerInput = new CheckerInput();
+ checkerInput.name = "";
+
+ exception.expect(BadRequestException.class);
+ exception.expectMessage("name cannot be unset");
+ checkersApi.id(checkerUuid).update(checkerInput);
+ }
+
+ @Test
+ public void cannotSetCheckerNameToStringWhichIsEmptyAfterTrim() throws Exception {
+ String checkerUuid = checkerOperations.newChecker().name("my-checker").create();
+
+ CheckerInput checkerInput = new CheckerInput();
+ checkerInput.name = " ";
+
+ exception.expect(BadRequestException.class);
+ exception.expectMessage("name cannot be unset");
+ checkersApi.id(checkerUuid).update(checkerInput);
+ }
+
+ @Test
+ public void updateCheckerNameToNameThatIsAlreadyUsed() throws Exception {
+ checkerOperations.newChecker().name("other-checker").create();
+
+ String checkerUuid = checkerOperations.newChecker().name("my-checker").create();
+
+ CheckerInput input = new CheckerInput();
+ input.name = "other-checker";
+
+ CheckerInfo info = checkersApi.id(checkerUuid).update(input);
+ assertThat(info.name).isEqualTo(input.name);
+
+ PerCheckerOperations perCheckerOps = checkerOperations.checker(checkerUuid);
+ assertCommit(
+ perCheckerOps.commit(),
+ "Update checker\n\nRename from my-checker to other-checker",
+ info.updatedOn,
+ perCheckerOps.get().refState());
+ }
+
+ @Test
+ public void addCheckerDescription() throws Exception {
+ String checkerUuid = checkerOperations.newChecker().name("my-checker").create();
+
+ CheckerInput input = new CheckerInput();
+ input.description = "A description.";
+
+ CheckerInfo info = checkersApi.id(checkerUuid).update(input);
+ assertThat(info.description).isEqualTo(input.description);
+
+ PerCheckerOperations perCheckerOps = checkerOperations.checker(checkerUuid);
+ assertCommit(
+ perCheckerOps.commit(), "Update checker", info.updatedOn, perCheckerOps.get().refState());
+ }
+
+ @Test
+ public void updateCheckerDescription() throws Exception {
+ String checkerUuid =
+ checkerOperations.newChecker().name("my-checker").description("A description.").create();
+
+ CheckerInput input = new CheckerInput();
+ input.description = "A new description.";
+
+ CheckerInfo info = checkersApi.id(checkerUuid).update(input);
+ assertThat(info.description).isEqualTo(input.description);
+
+ PerCheckerOperations perCheckerOps = checkerOperations.checker(checkerUuid);
+ assertCommit(
+ perCheckerOps.commit(), "Update checker", info.updatedOn, perCheckerOps.get().refState());
+ }
+
+ @Test
+ public void unsetCheckerDescription() throws Exception {
+ String checkerUuid =
+ checkerOperations.newChecker().name("my-checker").description("A description.").create();
+
+ CheckerInput checkerInput = new CheckerInput();
+ checkerInput.description = "";
+
+ CheckerInfo info = checkersApi.id(checkerUuid).update(checkerInput);
+ assertThat(info.description).isNull();
+
+ PerCheckerOperations perCheckerOps = checkerOperations.checker(checkerUuid);
+ assertCommit(
+ perCheckerOps.commit(), "Update checker", info.updatedOn, perCheckerOps.get().refState());
+ }
+
+ @Test
+ public void checkerDescriptionIsTrimmed() throws Exception {
+ String checkerUuid = checkerOperations.newChecker().name("my-checker").create();
+
+ CheckerInput input = new CheckerInput();
+ input.description = " A description. ";
+
+ CheckerInfo info = checkersApi.id(checkerUuid).update(input);
+ assertThat(info.description).isEqualTo("A description.");
+
+ PerCheckerOperations perCheckerOps = checkerOperations.checker(checkerUuid);
+ assertCommit(
+ perCheckerOps.commit(), "Update checker", info.updatedOn, perCheckerOps.get().refState());
+ }
+
+ @Test
+ public void addCheckerUrl() throws Exception {
+ String checkerUuid = checkerOperations.newChecker().name("my-checker").create();
+
+ CheckerInput input = new CheckerInput();
+ input.url = "http://example.com/my-checker";
+
+ CheckerInfo info = checkersApi.id(checkerUuid).update(input);
+ assertThat(info.url).isEqualTo(input.url);
+
+ PerCheckerOperations perCheckerOps = checkerOperations.checker(checkerUuid);
+ assertCommit(
+ perCheckerOps.commit(), "Update checker", info.updatedOn, perCheckerOps.get().refState());
+ }
+
+ @Test
+ public void updateCheckerUrl() throws Exception {
+ String checkerUuid =
+ checkerOperations
+ .newChecker()
+ .name("my-checker")
+ .url("http://example.com/my-checker")
+ .create();
+
+ CheckerInput input = new CheckerInput();
+ input.url = "http://example.com/my-checker-foo";
+
+ CheckerInfo info = checkersApi.id(checkerUuid).update(input);
+ assertThat(info.url).isEqualTo(input.url);
+
+ PerCheckerOperations perCheckerOps = checkerOperations.checker(checkerUuid);
+ assertCommit(
+ perCheckerOps.commit(), "Update checker", info.updatedOn, perCheckerOps.get().refState());
+ }
+
+ @Test
+ public void unsetCheckerUrl() throws Exception {
+ String checkerUuid =
+ checkerOperations
+ .newChecker()
+ .name("my-checker")
+ .url("http://example.com/my-checker")
+ .create();
+
+ CheckerInput checkerInput = new CheckerInput();
+ checkerInput.url = "";
+
+ CheckerInfo info = checkersApi.id(checkerUuid).update(checkerInput);
+ assertThat(info.url).isNull();
+
+ PerCheckerOperations perCheckerOps = checkerOperations.checker(checkerUuid);
+ assertCommit(
+ perCheckerOps.commit(), "Update checker", info.updatedOn, perCheckerOps.get().refState());
+ }
+
+ @Test
+ public void checkerUrlIsTrimmed() throws Exception {
+ String checkerUuid = checkerOperations.newChecker().name("my-checker").create();
+
+ CheckerInput input = new CheckerInput();
+ input.url = " http://example.com/my-checker ";
+
+ CheckerInfo info = checkersApi.id(checkerUuid).update(input);
+ assertThat(info.url).isEqualTo("http://example.com/my-checker");
+
+ PerCheckerOperations perCheckerOps = checkerOperations.checker(checkerUuid);
+ assertCommit(
+ perCheckerOps.commit(), "Update checker", info.updatedOn, perCheckerOps.get().refState());
+ }
+
+ @Test
+ public void updateRepository() throws Exception {
+ String checkerUuid =
+ checkerOperations.newChecker().name("my-checker").repository(allProjects).create();
+
+ Project.NameKey repositoryName = projectOperations.newProject().create();
+
+ CheckerInput input = new CheckerInput();
+ input.repository = repositoryName.get();
+
+ CheckerInfo info = checkersApi.id(checkerUuid).update(input);
+ assertThat(info.repository).isEqualTo(input.repository);
+
+ PerCheckerOperations perCheckerOps = checkerOperations.checker(checkerUuid);
+ assertCommit(
+ perCheckerOps.commit(), "Update checker", info.updatedOn, perCheckerOps.get().refState());
+ assertThat(checkerOperations.sha1sOfRepositoriesWithCheckers())
+ .containsExactly(CheckersByRepositoryNotes.computeRepositorySha1(repositoryName));
+ assertThat(checkerOperations.checkersOf(repositoryName)).containsExactly(info.uuid);
+ }
+
+ @Test
+ public void cannotSetRepositoryToEmptyString() throws Exception {
+ String checkerUuid = checkerOperations.newChecker().create();
+
+ CheckerInput checkerInput = new CheckerInput();
+ checkerInput.repository = "";
+
+ exception.expect(BadRequestException.class);
+ exception.expectMessage("repository cannot be unset");
+ checkersApi.id(checkerUuid).update(checkerInput);
+ }
+
+ @Test
+ public void cannotSetRepositoryToStringWhichIsEmptyAfterTrim() throws Exception {
+ String checkerUuid = checkerOperations.newChecker().create();
+
+ CheckerInput checkerInput = new CheckerInput();
+ checkerInput.repository = " ";
+
+ exception.expect(BadRequestException.class);
+ exception.expectMessage("repository cannot be unset");
+ checkersApi.id(checkerUuid).update(checkerInput);
+ }
+
+ @Test
+ public void cannotSetNonExistingRepository() throws Exception {
+ String checkerUuid = checkerOperations.newChecker().create();
+
+ CheckerInput checkerInput = new CheckerInput();
+ checkerInput.repository = "non-existing";
+
+ exception.expect(UnprocessableEntityException.class);
+ exception.expectMessage("repository non-existing not found");
+ checkersApi.id(checkerUuid).update(checkerInput);
+ }
+
+ @Test
+ public void cannotSetUrlToInvalidUrl() throws Exception {
+ String checkerUuid = checkerOperations.newChecker().name("my-checker").create();
+
+ CheckerInput input = new CheckerInput();
+ input.url = "ftp://example.com/my-checker";
+ exception.expect(BadRequestException.class);
+ exception.expectMessage("only http/https URLs supported: ftp://example.com/my-checker");
+ checkersApi.id(checkerUuid).update(input);
+ }
+
+ @Test
+ public void disableAndReenable() throws Exception {
+ String checkerUuid =
+ checkerOperations.newChecker().name("my-checker").repository(allProjects).create();
+ assertThat(checkerOperations.checkersOf(allProjects)).containsExactly(checkerUuid);
+
+ CheckerInput input = new CheckerInput();
+ input.status = CheckerStatus.DISABLED;
+
+ CheckerInfo info = checkersApi.id(checkerUuid).update(input);
+ assertThat(info.status).isEqualTo(CheckerStatus.DISABLED);
+ assertThat(checkerOperations.checkersOf(allProjects)).isEmpty();
+
+ input = new CheckerInput();
+ input.status = CheckerStatus.ENABLED;
+ info = checkersApi.id(checkerUuid).update(input);
+ assertThat(info.status).isEqualTo(CheckerStatus.ENABLED);
+ assertThat(checkerOperations.checkersOf(allProjects)).containsExactly(checkerUuid);
+ }
+
+ @Test
+ public void updateRepositoryDuringDisable() throws Exception {
+ String checkerUuid =
+ checkerOperations.newChecker().name("my-checker").repository(allProjects).create();
+
+ Project.NameKey repositoryName = projectOperations.newProject().create();
+
+ CheckerInput input = new CheckerInput();
+ input.repository = repositoryName.get();
+ input.status = CheckerStatus.DISABLED;
+
+ CheckerInfo info = checkersApi.id(checkerUuid).update(input);
+ assertThat(info.repository).isEqualTo(input.repository);
+ assertThat(info.status).isEqualTo(CheckerStatus.DISABLED);
+ assertThat(checkerOperations.checkersOf(allProjects)).isEmpty();
+ }
+
+ @Test
+ public void updateRepositoryDuringEnable() throws Exception {
+ String checkerUuid =
+ checkerOperations.newChecker().name("my-checker").repository(allProjects).create();
+
+ Project.NameKey repositoryName = projectOperations.newProject().create();
+ assertThat(checkerOperations.checkersOf(allProjects)).containsExactly(checkerUuid);
+ assertThat(checkerOperations.checkersOf(repositoryName)).isEmpty();
+
+ CheckerInput input = new CheckerInput();
+ input.status = CheckerStatus.DISABLED;
+
+ CheckerInfo info = checkersApi.id(checkerUuid).update(input);
+ assertThat(info.status).isEqualTo(CheckerStatus.DISABLED);
+ assertThat(checkerOperations.checkersOf(allProjects)).isEmpty();
+ assertThat(checkerOperations.checkersOf(repositoryName)).isEmpty();
+
+ input = new CheckerInput();
+ input.status = CheckerStatus.ENABLED;
+ input.repository = repositoryName.get();
+ info = checkersApi.id(checkerUuid).update(input);
+ assertThat(info.status).isEqualTo(CheckerStatus.ENABLED);
+ assertThat(checkerOperations.checkersOf(allProjects)).isEmpty();
+ assertThat(checkerOperations.checkersOf(repositoryName)).containsExactly(checkerUuid);
+ }
+
+ @Test
+ public void updateCheckerWithoutAdministrateCheckersCapabilityFails() throws Exception {
+ String checkerUuid = checkerOperations.newChecker().name("my-checker").create();
+
+ requestScopeOperations.setApiUser(user.getId());
+
+ CheckerInput input = new CheckerInput();
+ input.name = "my-renamed-checker";
+
+ exception.expect(AuthException.class);
+ exception.expectMessage("administrateCheckers for plugin checks not permitted");
+ checkersApi.id(checkerUuid).update(input);
+ }
+}
diff --git a/javatests/com/google/gerrit/plugins/checks/db/BUILD b/javatests/com/google/gerrit/plugins/checks/db/BUILD
new file mode 100644
index 0000000..a8d497c
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/checks/db/BUILD
@@ -0,0 +1,28 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+
+junit_tests(
+ name = "db_tests",
+ size = "small",
+ srcs = glob(["*.java"]),
+ deps = [
+ "//java/com/google/gerrit/common:annotations",
+ "//java/com/google/gerrit/common:server",
+ "//java/com/google/gerrit/common/data/testing:common-data-test-util",
+ "//java/com/google/gerrit/extensions:api",
+ "//java/com/google/gerrit/extensions/common/testing:common-test-util",
+ "//java/com/google/gerrit/git",
+ "//java/com/google/gerrit/reviewdb:server",
+ "//java/com/google/gerrit/server",
+ "//java/com/google/gerrit/server/testing",
+ "//java/com/google/gerrit/server/util/time",
+ "//java/com/google/gerrit/testing:gerrit-test-util",
+ "//java/com/google/gerrit/truth",
+ "//lib:guava",
+ "//lib:gwtorm",
+ "//lib/jgit/org.eclipse.jgit:jgit",
+ "//lib/jgit/org.eclipse.jgit.junit:junit",
+ "//lib/truth",
+ "//plugins/checks:checks__plugin",
+ "//plugins/checks/java/com/google/gerrit/plugins/checks/testing",
+ ],
+)
diff --git a/javatests/com/google/gerrit/plugins/checks/db/CheckerConfigTest.java b/javatests/com/google/gerrit/plugins/checks/db/CheckerConfigTest.java
new file mode 100644
index 0000000..1e560e3
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/checks/db/CheckerConfigTest.java
@@ -0,0 +1,550 @@
+// 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.google.gerrit.plugins.checks.db;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.plugins.checks.testing.CheckerConfigSubject.assertThat;
+import static org.hamcrest.CoreMatchers.instanceOf;
+
+import com.google.common.truth.StringSubject;
+import com.google.gerrit.plugins.checks.CheckerCreation;
+import com.google.gerrit.plugins.checks.CheckerRef;
+import com.google.gerrit.plugins.checks.CheckerUpdate;
+import com.google.gerrit.plugins.checks.CheckerUuid;
+import com.google.gerrit.plugins.checks.api.CheckerStatus;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.util.time.TimeUtil;
+import com.google.gerrit.testing.GerritBaseTests;
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.Month;
+import java.time.ZoneId;
+import java.util.TimeZone;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.junit.Before;
+import org.junit.Test;
+
+public class CheckerConfigTest extends GerritBaseTests {
+ private Project.NameKey projectName;
+ private Repository repository;
+ private TestRepository<?> testRepository;
+
+ private final String checkerName = "my-checker";
+ private final String checkerUuid = CheckerUuid.make(checkerName);
+ private final Project.NameKey checkerRepository = new Project.NameKey("my-repo");
+ private final TimeZone timeZone = TimeZone.getTimeZone("America/Los_Angeles");
+
+ @Before
+ public void setUp() throws Exception {
+ projectName = new Project.NameKey("Test Repository");
+ repository = new InMemoryRepository(new DfsRepositoryDescription("Test Repository"));
+ testRepository = new TestRepository<>(repository);
+ }
+
+ @Test
+ public void correctCommitMessageForCheckerCreation() throws Exception {
+ CheckerCreation checkerCreation =
+ getPrefilledCheckerCreationBuilder().setCheckerUuid(checkerUuid).build();
+ createChecker(checkerCreation);
+ assertThatCommitMessage(checkerUuid).isEqualTo("Create checker");
+ }
+
+ @Test
+ public void specifiedCheckerUuidIsRespectedForNewChecker() throws Exception {
+ CheckerCreation checkerCreation =
+ getPrefilledCheckerCreationBuilder().setCheckerUuid(checkerUuid).build();
+ createChecker(checkerCreation);
+
+ CheckerConfig checkerConfig = loadChecker(checkerUuid);
+ assertThat(checkerConfig).hasUuid(checkerUuid);
+ }
+
+ @Test
+ public void invalidCheckerUuidIsRejectedForNewChecker() throws Exception {
+ CheckerCreation checkerCreation =
+ getPrefilledCheckerCreationBuilder().setCheckerUuid("not-a-SHA1").build();
+
+ exception.expect(IllegalStateException.class);
+ exception.expectMessage("invalid checker UUID");
+ createChecker(checkerCreation);
+ }
+
+ @Test
+ public void specifiedNameIsRespectedForNewChecker() throws Exception {
+ CheckerCreation checkerCreation =
+ getPrefilledCheckerCreationBuilder().setName(checkerName).build();
+ createChecker(checkerCreation);
+
+ CheckerConfig checkerConfig = loadChecker(checkerCreation.getCheckerUuid());
+ assertThat(checkerConfig).hasName(checkerName);
+ assertThat(checkerConfig).configStringList("name").containsExactly(checkerName);
+ }
+
+ @Test
+ public void nameOfCheckerUpdateOverridesCheckerCreation() throws Exception {
+ String anotherName = "another-name";
+
+ CheckerCreation checkerCreation =
+ getPrefilledCheckerCreationBuilder().setName(checkerName).build();
+ CheckerUpdate checkerUpdate = CheckerUpdate.builder().setName(anotherName).build();
+ createChecker(checkerCreation, checkerUpdate);
+
+ CheckerConfig checkerConfig = loadChecker(checkerCreation.getCheckerUuid());
+ assertThat(checkerConfig).hasName(anotherName);
+ assertThat(checkerConfig).configStringList("name").containsExactly(anotherName);
+ }
+
+ @Test
+ public void nameOfNewCheckerMustNotBeEmpty() throws Exception {
+ CheckerCreation checkerCreation = getPrefilledCheckerCreationBuilder().setName("").build();
+ CheckerConfig checkerConfig =
+ CheckerConfig.createForNewChecker(projectName, repository, checkerCreation);
+
+ try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
+ exception.expectCause(instanceOf(ConfigInvalidException.class));
+ exception.expectMessage(String.format("Name of the checker %s must be defined", checkerUuid));
+ checkerConfig.commit(metaDataUpdate);
+ }
+ }
+
+ @Test
+ public void descriptionDefaultsToOptionalEmpty() throws Exception {
+ CheckerCreation checkerCreation =
+ CheckerCreation.builder()
+ .setCheckerUuid(checkerUuid)
+ .setName(checkerName)
+ .setRepository(checkerRepository)
+ .build();
+ createChecker(checkerCreation);
+
+ CheckerConfig checkerConfig = loadChecker(checkerCreation.getCheckerUuid());
+ assertThat(checkerConfig).hasDescriptionThat().isEmpty();
+ assertThat(checkerConfig).configStringList("description").isEmpty();
+ }
+
+ @Test
+ public void specifiedDescriptionIsRespectedForNewChecker() throws Exception {
+ String description = "This is a test checker.";
+
+ CheckerCreation checkerCreation = getPrefilledCheckerCreationBuilder().build();
+ CheckerUpdate checkerUpdate = CheckerUpdate.builder().setDescription(description).build();
+ createChecker(checkerCreation, checkerUpdate);
+
+ CheckerConfig checkerConfig = loadChecker(checkerCreation.getCheckerUuid());
+ assertThat(checkerConfig).hasDescriptionThat().value().isEqualTo(description);
+ assertThat(checkerConfig).configStringList("description").containsExactly(description);
+ }
+
+ @Test
+ public void emptyDescriptionForNewCheckerIsIgnored() throws Exception {
+ CheckerCreation checkerCreation = getPrefilledCheckerCreationBuilder().build();
+ CheckerUpdate checkerUpdate = CheckerUpdate.builder().setDescription("").build();
+ createChecker(checkerCreation, checkerUpdate);
+
+ CheckerConfig checkerConfig = loadChecker(checkerCreation.getCheckerUuid());
+ assertThat(checkerConfig).hasDescriptionThat().isEmpty();
+ assertThat(checkerConfig).configStringList("description").isEmpty();
+ }
+
+ @Test
+ public void urlDefaultsToOptionalEmpty() throws Exception {
+ CheckerCreation checkerCreation =
+ CheckerCreation.builder()
+ .setCheckerUuid(checkerUuid)
+ .setName(checkerName)
+ .setRepository(checkerRepository)
+ .build();
+ createChecker(checkerCreation);
+
+ CheckerConfig checkerConfig = loadChecker(checkerCreation.getCheckerUuid());
+ assertThat(checkerConfig).hasUrlThat().isEmpty();
+ assertThat(checkerConfig).configStringList("url").isEmpty();
+ }
+
+ @Test
+ public void specifiedUrlIsRespectedForNewChecker() throws Exception {
+ String url = "http://example.com/my-checker";
+
+ CheckerCreation checkerCreation = getPrefilledCheckerCreationBuilder().build();
+ CheckerUpdate checkerUpdate = CheckerUpdate.builder().setUrl(url).build();
+ createChecker(checkerCreation, checkerUpdate);
+
+ CheckerConfig checkerConfig = loadChecker(checkerCreation.getCheckerUuid());
+ assertThat(checkerConfig).hasUrlThat().value().isEqualTo(url);
+ assertThat(checkerConfig).configStringList("url").containsExactly(url);
+ }
+
+ @Test
+ public void emptyUrlForNewCheckerIsIgnored() throws Exception {
+ CheckerCreation checkerCreation = getPrefilledCheckerCreationBuilder().build();
+ CheckerUpdate checkerUpdate = CheckerUpdate.builder().setUrl("").build();
+ createChecker(checkerCreation, checkerUpdate);
+
+ CheckerConfig checkerConfig = loadChecker(checkerCreation.getCheckerUuid());
+ assertThat(checkerConfig).hasUrlThat().isEmpty();
+ assertThat(checkerConfig).configStringList("url").isEmpty();
+ }
+
+ @Test
+ public void specifiedRepositoryIsRespectedForNewChecker() throws Exception {
+ CheckerCreation checkerCreation =
+ getPrefilledCheckerCreationBuilder().setRepository(checkerRepository).build();
+ createChecker(checkerCreation);
+
+ CheckerConfig checkerConfig = loadChecker(checkerCreation.getCheckerUuid());
+ assertThat(checkerConfig).hasRepository(checkerRepository);
+ assertThat(checkerConfig)
+ .configStringList("repository")
+ .containsExactly(checkerRepository.get());
+ }
+
+ @Test
+ public void repositoryOfCheckerUpdateOverridesCheckerCreation() throws Exception {
+ Project.NameKey anotherRepository = new Project.NameKey("another-repo");
+
+ CheckerCreation checkerCreation =
+ getPrefilledCheckerCreationBuilder().setRepository(checkerRepository).build();
+ CheckerUpdate checkerUpdate = CheckerUpdate.builder().setRepository(anotherRepository).build();
+ createChecker(checkerCreation, checkerUpdate);
+
+ CheckerConfig checkerConfig = loadChecker(checkerCreation.getCheckerUuid());
+ assertThat(checkerConfig).hasRepository(anotherRepository);
+ assertThat(checkerConfig)
+ .configStringList("repository")
+ .containsExactly(anotherRepository.get());
+ }
+
+ @Test
+ public void repositoryOfNewCheckerMustNotBeEmpty() throws Exception {
+ CheckerCreation checkerCreation =
+ getPrefilledCheckerCreationBuilder().setRepository(new Project.NameKey("")).build();
+ CheckerConfig checkerConfig =
+ CheckerConfig.createForNewChecker(projectName, repository, checkerCreation);
+
+ try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
+ exception.expectCause(instanceOf(ConfigInvalidException.class));
+ exception.expectMessage(
+ String.format("Repository of the checker %s must be defined", checkerUuid));
+ checkerConfig.commit(metaDataUpdate);
+ }
+ }
+
+ @Test
+ public void createdOnDefaultsToNow() throws Exception {
+ // Git timestamps are only precise to the second.
+ Timestamp testStart = TimeUtil.truncateToSecond(TimeUtil.nowTs());
+
+ createArbitraryChecker(checkerUuid);
+ CheckerConfig checkerConfig = loadChecker(checkerUuid);
+ assertThat(checkerConfig).hasCreatedOnThat().isAtLeast(testStart);
+ }
+
+ @Test
+ public void specifiedCreatedOnIsRespectedForNewChecker() throws Exception {
+ Timestamp createdOn = toTimestamp(LocalDate.of(2017, Month.DECEMBER, 11).atTime(13, 44, 10));
+
+ CheckerCreation checkerCreation = getPrefilledCheckerCreationBuilder().build();
+ CheckerUpdate checkerUpdate = CheckerUpdate.builder().setUpdatedOn(createdOn).build();
+ createChecker(checkerCreation, checkerUpdate);
+
+ CheckerConfig checkerConfig = loadChecker(checkerCreation.getCheckerUuid());
+ assertThat(checkerConfig).hasCreatedOnThat().isEqualTo(createdOn);
+ }
+
+ @Test
+ public void nameInConfigMayNotBeUndefined() throws Exception {
+ populateCheckerConfig(checkerUuid, "[checker]");
+
+ exception.expect(ConfigInvalidException.class);
+ exception.expectMessage(String.format("name of checker %s not set", checkerUuid));
+ loadChecker(checkerUuid);
+ }
+
+ @Test
+ public void correctCommitMessageForCheckerUpdate() throws Exception {
+ createArbitraryChecker(checkerUuid);
+ assertThatCommitMessage(checkerUuid).isEqualTo("Create checker");
+
+ CheckerUpdate checkerUpdate = CheckerUpdate.builder().setDescription("A description.").build();
+ updateChecker(checkerUuid, checkerUpdate);
+ assertThatCommitMessage(checkerUuid).isEqualTo("Update checker");
+ }
+
+ @Test
+ public void nameCanBeUpdated() throws Exception {
+ CheckerCreation checkerCreation =
+ CheckerCreation.builder()
+ .setCheckerUuid(checkerUuid)
+ .setName(checkerName)
+ .setRepository(checkerRepository)
+ .build();
+ createChecker(checkerCreation);
+
+ String newName = "new-name";
+ CheckerUpdate checkerUpdate = CheckerUpdate.builder().setName(newName).build();
+ updateChecker(checkerUuid, checkerUpdate);
+
+ CheckerConfig checkerConfig = loadChecker(checkerUuid);
+ assertThat(checkerConfig).hasName(newName);
+ assertThat(checkerConfig).configStringList("name").containsExactly(newName);
+
+ assertThatCommitMessage(checkerUuid)
+ .isEqualTo("Update checker\n\nRename from " + checkerName + " to " + newName);
+ }
+
+ @Test
+ public void nameCannotBeRemoved() throws Exception {
+ createArbitraryChecker(checkerUuid);
+
+ CheckerUpdate checkerUpdate = CheckerUpdate.builder().setName("").build();
+
+ exception.expect(IOException.class);
+ exception.expectMessage(String.format("Name of the checker %s must be defined", checkerUuid));
+ updateChecker(checkerUuid, checkerUpdate);
+ }
+
+ @Test
+ public void descriptionCanBeUpdated() throws Exception {
+ createArbitraryChecker(checkerUuid);
+ String newDescription = "New description";
+
+ CheckerUpdate checkerUpdate = CheckerUpdate.builder().setDescription(newDescription).build();
+ updateChecker(checkerUuid, checkerUpdate);
+
+ CheckerConfig checkerConfig = loadChecker(checkerUuid);
+ assertThat(checkerConfig).hasDescriptionThat().value().isEqualTo(newDescription);
+ assertThat(checkerConfig).configStringList("description").containsExactly(newDescription);
+ }
+
+ @Test
+ public void descriptionCanBeRemoved() throws Exception {
+ createArbitraryChecker(checkerUuid);
+
+ CheckerUpdate checkerUpdate = CheckerUpdate.builder().setDescription("").build();
+ CheckerConfig checkerConfig = updateChecker(checkerUuid, checkerUpdate);
+ assertThat(checkerConfig).hasDescriptionThat().isEmpty();
+ assertThat(checkerConfig).configStringList("description").isEmpty();
+ }
+
+ @Test
+ public void urlCanBeUpdated() throws Exception {
+ createArbitraryChecker(checkerUuid);
+ String newUrl = "http://example.com/my-checker";
+
+ CheckerUpdate checkerUpdate = CheckerUpdate.builder().setUrl(newUrl).build();
+ updateChecker(checkerUuid, checkerUpdate);
+
+ CheckerConfig checkerConfig = loadChecker(checkerUuid);
+ assertThat(checkerConfig).hasUrlThat().value().isEqualTo(newUrl);
+ assertThat(checkerConfig).configStringList("url").containsExactly(newUrl);
+ }
+
+ @Test
+ public void urlCanBeRemoved() throws Exception {
+ createArbitraryChecker(checkerUuid);
+
+ CheckerUpdate checkerUpdate = CheckerUpdate.builder().setUrl("").build();
+ CheckerConfig checkerConfig = updateChecker(checkerUuid, checkerUpdate);
+ assertThat(checkerConfig).hasUrlThat().isEmpty();
+ assertThat(checkerConfig).configStringList("url").isEmpty();
+ }
+
+ @Test
+ public void repositoryCanBeUpdated() throws Exception {
+ CheckerCreation checkerCreation =
+ CheckerCreation.builder()
+ .setCheckerUuid(checkerUuid)
+ .setName(checkerName)
+ .setRepository(checkerRepository)
+ .build();
+ createChecker(checkerCreation);
+
+ Project.NameKey newRepository = new Project.NameKey("another-repo");
+ CheckerUpdate checkerUpdate = CheckerUpdate.builder().setRepository(newRepository).build();
+ updateChecker(checkerUuid, checkerUpdate);
+
+ CheckerConfig checkerConfig = loadChecker(checkerUuid);
+ assertThat(checkerConfig).hasRepository(newRepository);
+ assertThat(checkerConfig).configStringList("repository").containsExactly(newRepository.get());
+
+ assertThatCommitMessage(checkerUuid).isEqualTo("Update checker");
+ }
+
+ @Test
+ public void repositoryCannotBeRemoved() throws Exception {
+ createArbitraryChecker(checkerUuid);
+
+ CheckerUpdate checkerUpdate =
+ CheckerUpdate.builder().setRepository(new Project.NameKey("")).build();
+
+ exception.expect(IOException.class);
+ exception.expectMessage(
+ String.format("Repository of the checker %s must be defined", checkerUuid));
+ updateChecker(checkerUuid, checkerUpdate);
+ }
+
+ @Test
+ public void createDisabledChecker() throws Exception {
+ CheckerCreation checkerCreation = getPrefilledCheckerCreationBuilder().build();
+ CheckerUpdate checkerUpdate = CheckerUpdate.builder().setStatus(CheckerStatus.DISABLED).build();
+ CheckerConfig checker = createChecker(checkerCreation, checkerUpdate);
+
+ assertThat(checker).hasStatus(CheckerStatus.DISABLED);
+ assertThat(checker).configStringList("status").containsExactly("disabled");
+ assertThatCommitMessage(checkerUuid).isEqualTo("Create checker");
+ }
+
+ @Test
+ public void updateStatusToSameStatus() throws Exception {
+ CheckerConfig checker = createArbitraryChecker(checkerUuid);
+ assertThat(checker).hasStatus(CheckerStatus.ENABLED);
+ assertThat(checker).configStringList("status").containsExactly("enabled");
+
+ CheckerUpdate checkerUpdate = CheckerUpdate.builder().setStatus(CheckerStatus.ENABLED).build();
+ checker = updateChecker(checkerUuid, checkerUpdate);
+ assertThat(checker).hasStatus(CheckerStatus.ENABLED);
+ assertThat(checker).configStringList("status").containsExactly("enabled");
+ }
+
+ @Test
+ public void disableAndReenable() throws Exception {
+ createArbitraryChecker(checkerUuid);
+
+ CheckerUpdate checkerUpdate = CheckerUpdate.builder().setStatus(CheckerStatus.DISABLED).build();
+ CheckerConfig checker = updateChecker(checkerUuid, checkerUpdate);
+ assertThat(checker).hasStatus(CheckerStatus.DISABLED);
+ assertThat(checker).configStringList("status").containsExactly("disabled");
+
+ checkerUpdate = CheckerUpdate.builder().setStatus(CheckerStatus.ENABLED).build();
+ checker = updateChecker(checkerUuid, checkerUpdate);
+ assertThat(checker).hasStatus(CheckerStatus.ENABLED);
+ assertThat(checker).configStringList("status").containsExactly("enabled");
+ }
+
+ @Test
+ public void refStateIsCorrectlySet() throws Exception {
+ CheckerCreation checkerCreation =
+ getPrefilledCheckerCreationBuilder().setCheckerUuid(checkerUuid).build();
+ CheckerConfig newChecker = createChecker(checkerCreation);
+ ObjectId expectedRefStateAfterCreation = getCheckerRefState(checkerUuid);
+ assertThat(newChecker).hasRefStateThat().isEqualTo(expectedRefStateAfterCreation);
+
+ CheckerConfig checker = loadChecker(checkerUuid);
+ assertThat(checker).hasRefStateThat().isEqualTo(expectedRefStateAfterCreation);
+
+ CheckerUpdate checkerUpdate = CheckerUpdate.builder().setDescription("A description.").build();
+ CheckerConfig updatedChecker = updateChecker(checkerUuid, checkerUpdate);
+ ObjectId expectedRefStateAfterUpdate = getCheckerRefState(checkerUuid);
+ assertThat(expectedRefStateAfterUpdate).isNotEqualTo(expectedRefStateAfterCreation);
+ assertThat(updatedChecker).hasRefStateThat().isEqualTo(expectedRefStateAfterUpdate);
+ }
+
+ private CheckerConfig createArbitraryChecker(String checkerUuid) throws Exception {
+ CheckerCreation checkerCreation =
+ getPrefilledCheckerCreationBuilder().setCheckerUuid(checkerUuid).build();
+ return createChecker(checkerCreation);
+ }
+
+ private CheckerCreation.Builder getPrefilledCheckerCreationBuilder() {
+ return CheckerCreation.builder()
+ .setCheckerUuid(checkerUuid)
+ .setName(checkerName)
+ .setRepository(checkerRepository);
+ }
+
+ private CheckerConfig createChecker(CheckerCreation checkerCreation) throws Exception {
+ CheckerConfig checkerConfig =
+ CheckerConfig.createForNewChecker(projectName, repository, checkerCreation);
+ commit(checkerConfig);
+ return loadChecker(checkerCreation.getCheckerUuid());
+ }
+
+ private CheckerConfig createChecker(CheckerCreation checkerCreation, CheckerUpdate checkerUpdate)
+ throws Exception {
+ CheckerConfig checkerConfig =
+ CheckerConfig.createForNewChecker(projectName, repository, checkerCreation);
+ checkerConfig.setCheckerUpdate(checkerUpdate);
+ commit(checkerConfig);
+ return loadChecker(checkerCreation.getCheckerUuid());
+ }
+
+ private CheckerConfig updateChecker(String checkerUuid, CheckerUpdate checkerUpdate)
+ throws Exception {
+ CheckerConfig checkerConfig =
+ CheckerConfig.loadForChecker(projectName, repository, checkerUuid);
+ checkerConfig.setCheckerUpdate(checkerUpdate);
+ commit(checkerConfig);
+ return loadChecker(checkerUuid);
+ }
+
+ private CheckerConfig loadChecker(String uuid) throws Exception {
+ return CheckerConfig.loadForChecker(projectName, repository, uuid);
+ }
+
+ private void commit(CheckerConfig checkerConfig) throws IOException {
+ try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
+ checkerConfig.commit(metaDataUpdate);
+ }
+ }
+
+ private MetaDataUpdate createMetaDataUpdate() {
+ PersonIdent serverIdent =
+ new PersonIdent(
+ "Gerrit Server", "noreply@gerritcodereview.com", TimeUtil.nowTs(), timeZone);
+
+ MetaDataUpdate metaDataUpdate =
+ new MetaDataUpdate(
+ GitReferenceUpdated.DISABLED, new Project.NameKey("Test Repository"), repository);
+ metaDataUpdate.getCommitBuilder().setCommitter(serverIdent);
+ metaDataUpdate.getCommitBuilder().setAuthor(serverIdent);
+ return metaDataUpdate;
+ }
+
+ private void populateCheckerConfig(String uuid, String fileContent) throws Exception {
+ testRepository
+ .branch(CheckerRef.refsCheckers(uuid))
+ .commit()
+ .message("Prepopulate checker.config")
+ .add(CheckerConfig.CHECKER_CONFIG_FILE, fileContent)
+ .create();
+ }
+
+ private ObjectId getCheckerRefState(String checkerUuid) throws IOException {
+ return repository.exactRef(CheckerRef.refsCheckers(checkerUuid)).getObjectId();
+ }
+
+ private StringSubject assertThatCommitMessage(String checkerUuid) throws IOException {
+ try (RevWalk rw = new RevWalk(repository)) {
+ RevCommit commit = rw.parseCommit(getCheckerRefState(checkerUuid));
+ return assertThat(commit.getFullMessage()).named("commit message");
+ }
+ }
+
+ private static Timestamp toTimestamp(LocalDateTime localDateTime) {
+ return Timestamp.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
+ }
+}