Merge changes I29bc8ee1,Ica77b8f5,I05ceb86c,I3be67af9,I542286a7, ...

* changes:
  CheckerInput: Rename field 'blockingConditions' to 'blocking'
  Test listing pending checks for checker that has an invalid query
  ListChecksIT: Add tests with non-current patch set
  GetCheckIT: Test more special cases
  Fix binding of ChecksSubmitRule
  Test that getting a check with options works if checker is invalid
  GetCheckIT: Only assert things that are relevant to the tests
  ListChecksIT: Let each test care about the test setup itself
  CheckersByRepositoryNotes: Fix loading checkers from old revision
  Add unit test for CheckersByRepositoryNotes
  Test that setting an invalid URL on checks is not allowed
  CreateCheckIT: Add more tests
  CheckInput: Mark all fields as Nullable
  Fix update finished field in check and add more update check tests
  Consistently use the name 'repository' in the checks API
  CheckerConfig: Set new updated timestamp only when config has changed
  CheckerConfig: Don't reload config file on save
  CheckerConfig#getConfigForTesting: Return Optional<Config>
  Rename createdOn/updateOn in CheckerInfo to created/updated
  CheckerUpdate: Remove support for specifying updated timestamp
  Test that creation/last-updated timestamps for checks/checkers are reasonable
diff --git a/java/com/google/gerrit/plugins/checks/CheckJson.java b/java/com/google/gerrit/plugins/checks/CheckJson.java
index 3d55666..4b5e9f3 100644
--- a/java/com/google/gerrit/plugins/checks/CheckJson.java
+++ b/java/com/google/gerrit/plugins/checks/CheckJson.java
@@ -62,7 +62,7 @@
     CheckInfo info = new CheckInfo();
     info.checkerUuid = check.key().checkerUuid().get();
     info.changeNumber = check.key().patchSet().changeId.id;
-    info.project = check.key().project().get();
+    info.repository = check.key().repository().get();
     info.patchSetId = check.key().patchSet().patchSetId;
     info.state = check.state();
 
diff --git a/java/com/google/gerrit/plugins/checks/CheckKey.java b/java/com/google/gerrit/plugins/checks/CheckKey.java
index 3b5bc3a..526f8f2 100644
--- a/java/com/google/gerrit/plugins/checks/CheckKey.java
+++ b/java/com/google/gerrit/plugins/checks/CheckKey.java
@@ -21,14 +21,14 @@
 /** Fields to identify a check. */
 @AutoValue
 public abstract class CheckKey {
-  public abstract Project.NameKey project();
+  public abstract Project.NameKey repository();
 
   public abstract PatchSet.Id patchSet();
 
   public abstract CheckerUuid checkerUuid();
 
   public static CheckKey create(
-      Project.NameKey project, PatchSet.Id patchSet, CheckerUuid checkerUuid) {
-    return new AutoValue_CheckKey(project, patchSet, checkerUuid);
+      Project.NameKey repositoryName, PatchSet.Id patchSetId, CheckerUuid checkerUuid) {
+    return new AutoValue_CheckKey(repositoryName, patchSetId, checkerUuid);
   }
 }
diff --git a/java/com/google/gerrit/plugins/checks/Checker.java b/java/com/google/gerrit/plugins/checks/Checker.java
index 67a29f8..aa43340 100644
--- a/java/com/google/gerrit/plugins/checks/Checker.java
+++ b/java/com/google/gerrit/plugins/checks/Checker.java
@@ -119,14 +119,14 @@
    *
    * @return the creation timestamp
    */
-  public abstract Timestamp getCreatedOn();
+  public abstract Timestamp getCreated();
 
   /**
    * Returns the timestamp of when the checker was last updated.
    *
    * @return the last updated timestamp
    */
-  public abstract Timestamp getUpdatedOn();
+  public abstract Timestamp getUpdated();
 
   /**
    * Returns the ref state of the checker.
@@ -269,9 +269,9 @@
 
     public abstract Builder setQuery(String query);
 
-    public abstract Builder setCreatedOn(Timestamp createdOn);
+    public abstract Builder setCreated(Timestamp created);
 
-    public abstract Builder setUpdatedOn(Timestamp updatedOn);
+    public abstract Builder setUpdated(Timestamp updated);
 
     public abstract Builder setRefState(ObjectId refState);
 
diff --git a/java/com/google/gerrit/plugins/checks/CheckerJson.java b/java/com/google/gerrit/plugins/checks/CheckerJson.java
index 27f96ec..701a169 100644
--- a/java/com/google/gerrit/plugins/checks/CheckerJson.java
+++ b/java/com/google/gerrit/plugins/checks/CheckerJson.java
@@ -30,8 +30,8 @@
     info.status = checker.getStatus();
     info.blocking = checker.getBlockingConditions();
     info.query = checker.getQuery().orElse(null);
-    info.createdOn = checker.getCreatedOn();
-    info.updatedOn = checker.getUpdatedOn();
+    info.created = checker.getCreated();
+    info.updated = checker.getUpdated();
     return info;
   }
 }
diff --git a/java/com/google/gerrit/plugins/checks/CheckerUpdate.java b/java/com/google/gerrit/plugins/checks/CheckerUpdate.java
index e555180..97165f6 100644
--- a/java/com/google/gerrit/plugins/checks/CheckerUpdate.java
+++ b/java/com/google/gerrit/plugins/checks/CheckerUpdate.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.plugins.checks.api.BlockingCondition;
 import com.google.gerrit.plugins.checks.api.CheckerStatus;
 import com.google.gerrit.reviewdb.client.Project;
-import java.sql.Timestamp;
 import java.util.Optional;
 
 @AutoValue
@@ -60,16 +59,6 @@
   /** Defines the new query for the checker. If not specified, the query remains unchanged. */
   public abstract Optional<String> getQuery();
 
-  /**
-   * 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() {
@@ -93,8 +82,6 @@
 
     public abstract Builder setQuery(String query);
 
-    public abstract Builder setUpdatedOn(Timestamp timestamp);
-
     public abstract CheckerUpdate build();
   }
 }
diff --git a/java/com/google/gerrit/plugins/checks/Module.java b/java/com/google/gerrit/plugins/checks/Module.java
index 987a227..102c057 100644
--- a/java/com/google/gerrit/plugins/checks/Module.java
+++ b/java/com/google/gerrit/plugins/checks/Module.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.plugins.checks.api.ChangeCheckAttributeFactory;
 import com.google.gerrit.plugins.checks.api.ChangeCheckAttributeFactory.GetChangeOptions;
 import com.google.gerrit.plugins.checks.db.NoteDbCheckersModule;
+import com.google.gerrit.plugins.checks.rules.ChecksSubmitRule;
 import com.google.gerrit.server.DynamicOptions;
 import com.google.gerrit.server.change.ChangeAttributeFactory;
 import com.google.gerrit.server.git.validators.CommitValidationListener;
@@ -57,5 +58,6 @@
         .to(GetChangeOptions.class);
 
     install(new ApiModule());
+    install(new ChecksSubmitRule.Module());
   }
 }
diff --git a/java/com/google/gerrit/plugins/checks/acceptance/TestModule.java b/java/com/google/gerrit/plugins/checks/acceptance/TestModule.java
index c04a4d3..3348659 100644
--- a/java/com/google/gerrit/plugins/checks/acceptance/TestModule.java
+++ b/java/com/google/gerrit/plugins/checks/acceptance/TestModule.java
@@ -14,14 +14,11 @@
 
 package com.google.gerrit.plugins.checks.acceptance;
 
-import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.plugins.checks.Module;
 import com.google.gerrit.plugins.checks.acceptance.testsuite.CheckOperations;
 import com.google.gerrit.plugins.checks.acceptance.testsuite.CheckOperationsImpl;
 import com.google.gerrit.plugins.checks.acceptance.testsuite.CheckerOperations;
 import com.google.gerrit.plugins.checks.acceptance.testsuite.CheckerOperationsImpl;
-import com.google.gerrit.plugins.checks.rules.ChecksSubmitRule;
-import com.google.gerrit.server.rules.SubmitRule;
 import com.google.inject.AbstractModule;
 
 public class TestModule extends AbstractModule {
@@ -33,8 +30,5 @@
     // setup in tests as realistic as possible by delegating to the original module.
     bind(CheckerOperations.class).to(CheckerOperationsImpl.class);
     bind(CheckOperations.class).to(CheckOperationsImpl.class);
-    bind(SubmitRule.class)
-        .annotatedWith(Exports.named("ChecksSubmitRule"))
-        .to(ChecksSubmitRule.class);
   }
 }
diff --git a/java/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckOperationsImpl.java b/java/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckOperationsImpl.java
index 5d78aae..169fcc7 100644
--- a/java/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckOperationsImpl.java
+++ b/java/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckOperationsImpl.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
@@ -41,7 +42,7 @@
 
 public final class CheckOperationsImpl implements CheckOperations {
   private final Checks checks;
-  private final ChecksUpdate checksUpdate;
+  private final Provider<ChecksUpdate> checksUpdate;
   private final CheckJson.Factory checkJsonFactory;
   private final GitRepositoryManager repoManager;
 
@@ -50,7 +51,7 @@
       Checks checks,
       GitRepositoryManager repoManager,
       CheckJson.Factory checkJsonFactory,
-      @ServerInitiated ChecksUpdate checksUpdate) {
+      @ServerInitiated Provider<ChecksUpdate> checksUpdate) {
     this.checks = checks;
     this.repoManager = repoManager;
     this.checkJsonFactory = checkJsonFactory;
@@ -65,7 +66,7 @@
   @Override
   public TestCheckUpdate.Builder newCheck(CheckKey key) {
     return TestCheckUpdate.builder(key)
-        .setCheckUpdater(u -> checksUpdate.createCheck(key, toCheckUpdate(u)));
+        .setCheckUpdater(u -> checksUpdate.get().createCheck(key, toCheckUpdate(u)));
   }
 
   final class PerCheckOperationsImpl implements PerCheckOperations {
@@ -87,7 +88,7 @@
 
     @Override
     public ImmutableMap<RevId, String> notesAsText() throws Exception {
-      try (Repository repo = repoManager.openRepository(key.project());
+      try (Repository repo = repoManager.openRepository(key.repository());
           RevWalk rw = new RevWalk(repo)) {
         Ref checkRef =
             repo.getRefDatabase().exactRef(CheckerRef.checksRef(key.patchSet().changeId));
@@ -115,7 +116,8 @@
     @Override
     public TestCheckUpdate.Builder forUpdate() {
       return TestCheckUpdate.builder(key)
-          .setCheckUpdater(testUpdate -> checksUpdate.updateCheck(key, toCheckUpdate(testUpdate)));
+          .setCheckUpdater(
+              testUpdate -> checksUpdate.get().updateCheck(key, toCheckUpdate(testUpdate)));
     }
   }
 
diff --git a/java/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckTestData.java b/java/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckTestData.java
new file mode 100644
index 0000000..db977b7
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckTestData.java
@@ -0,0 +1,22 @@
+// 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;
+
+public class CheckTestData {
+  /** An invalid check URL that is not allowed to be set for a check. */
+  public static final String INVALID_URL = "ftp://example.com/my-check";
+
+  private CheckTestData() {}
+}
diff --git a/java/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckerOperationsImpl.java b/java/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckerOperationsImpl.java
index bbc0386..fe4d54f 100644
--- a/java/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckerOperationsImpl.java
+++ b/java/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckerOperationsImpl.java
@@ -39,6 +39,7 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 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 java.util.Optional;
@@ -65,7 +66,7 @@
 @Singleton
 public class CheckerOperationsImpl implements CheckerOperations {
   private final Checkers checkers;
-  private final CheckersUpdate checkersUpdate;
+  private final Provider<CheckersUpdate> checkersUpdate;
   private final GitRepositoryManager repoManager;
   private final AllProjectsName allProjectsName;
   private final CheckerJson checkerJson;
@@ -74,7 +75,7 @@
   @Inject
   public CheckerOperationsImpl(
       Checkers checkers,
-      @ServerInitiated CheckersUpdate checkersUpdate,
+      @ServerInitiated Provider<CheckersUpdate> checkersUpdate,
       GitRepositoryManager repoManager,
       AllProjectsName allProjectsName,
       CheckerJson checkerJson) {
@@ -100,7 +101,7 @@
       throws OrmDuplicateKeyException, ConfigInvalidException, IOException {
     CheckerCreation checkerCreation = toCheckerCreation(testCheckerCreation);
     CheckerUpdate checkerUpdate = toCheckerUpdate(testCheckerCreation);
-    Checker checker = checkersUpdate.createChecker(checkerCreation, checkerUpdate);
+    Checker checker = checkersUpdate.get().createChecker(checkerCreation, checkerUpdate);
     return checker.getUuid();
   }
 
@@ -235,7 +236,7 @@
 
     private void updateChecker(TestCheckerUpdate testCheckerUpdate) throws Exception {
       CheckerUpdate checkerUpdate = toCheckerUpdate(testCheckerUpdate);
-      checkersUpdate.updateChecker(checkerUuid, checkerUpdate);
+      checkersUpdate.get().updateChecker(checkerUuid, checkerUpdate);
 
       if (testCheckerUpdate.forceInvalidConfig()) {
         try (Repository repo = repoManager.openRepository(allProjectsName)) {
diff --git a/java/com/google/gerrit/plugins/checks/api/CheckInfo.java b/java/com/google/gerrit/plugins/checks/api/CheckInfo.java
index be8f34a..3096fec 100644
--- a/java/com/google/gerrit/plugins/checks/api/CheckInfo.java
+++ b/java/com/google/gerrit/plugins/checks/api/CheckInfo.java
@@ -22,8 +22,8 @@
 
 /** REST API representation of a {@link com.google.gerrit.plugins.checks.Check}. */
 public class CheckInfo {
-  /** Project name that this check applies to. */
-  public String project;
+  /** Repository name that this check applies to. */
+  public String repository;
   /** Change number that this check applies to. */
   public int changeNumber;
   /** Patch set ID that this check applies to. */
@@ -60,7 +60,7 @@
       return false;
     }
     CheckInfo other = (CheckInfo) o;
-    return Objects.equals(other.project, project)
+    return Objects.equals(other.repository, repository)
         && Objects.equals(other.changeNumber, changeNumber)
         && Objects.equals(other.patchSetId, patchSetId)
         && Objects.equals(other.checkerUuid, checkerUuid)
@@ -78,7 +78,7 @@
   @Override
   public int hashCode() {
     return Objects.hash(
-        project,
+        repository,
         changeNumber,
         patchSetId,
         checkerUuid,
@@ -96,7 +96,7 @@
   @Override
   public String toString() {
     return MoreObjects.toStringHelper(this)
-        .add("project", project)
+        .add("repository", repository)
         .add("changeNumber", changeNumber)
         .add("patchSetId", patchSetId)
         .add("checkerUuid", checkerUuid)
diff --git a/java/com/google/gerrit/plugins/checks/api/CheckInput.java b/java/com/google/gerrit/plugins/checks/api/CheckInput.java
index 31d8a35..633d2fb 100644
--- a/java/com/google/gerrit/plugins/checks/api/CheckInput.java
+++ b/java/com/google/gerrit/plugins/checks/api/CheckInput.java
@@ -22,9 +22,9 @@
 /** Input to create or update a {@link com.google.gerrit.plugins.checks.Check}. */
 public class CheckInput {
   /** UUID of the checker. */
-  public String checkerUuid;
+  @Nullable public String checkerUuid;
   /** State of the check. */
-  public CheckState state;
+  @Nullable public CheckState state;
   /** Fully qualified URL to detailed result on the Checker's service. */
   @Nullable public String url;
   /** Date/Time at which the checker started processing this check. */
diff --git a/java/com/google/gerrit/plugins/checks/api/CheckablePatchSetInfo.java b/java/com/google/gerrit/plugins/checks/api/CheckablePatchSetInfo.java
index c8747e1..1581585 100644
--- a/java/com/google/gerrit/plugins/checks/api/CheckablePatchSetInfo.java
+++ b/java/com/google/gerrit/plugins/checks/api/CheckablePatchSetInfo.java
@@ -16,8 +16,8 @@
 
 /** REST API representation of a patch set for which checks are pending. */
 public class CheckablePatchSetInfo {
-  /** Project name. */
-  public String project;
+  /** Repository name. */
+  public String repository;
 
   /** Change number. */
   public int changeNumber;
diff --git a/java/com/google/gerrit/plugins/checks/api/CheckerInfo.java b/java/com/google/gerrit/plugins/checks/api/CheckerInfo.java
index d6af4a6..7f99d98 100644
--- a/java/com/google/gerrit/plugins/checks/api/CheckerInfo.java
+++ b/java/com/google/gerrit/plugins/checks/api/CheckerInfo.java
@@ -28,13 +28,13 @@
   public CheckerStatus status;
   public Set<BlockingCondition> blocking;
   public String query;
-  public Timestamp createdOn;
-  public Timestamp updatedOn;
+  public Timestamp created;
+  public Timestamp updated;
 
   @Override
   public int hashCode() {
     return Objects.hash(
-        uuid, name, description, url, repository, status, blocking, query, createdOn, updatedOn);
+        uuid, name, description, url, repository, status, blocking, query, created, updated);
   }
 
   @Override
@@ -51,8 +51,8 @@
         && Objects.equals(status, o.status)
         && Objects.equals(blocking, o.blocking)
         && Objects.equals(query, o.query)
-        && Objects.equals(createdOn, o.createdOn)
-        && Objects.equals(updatedOn, o.updatedOn);
+        && Objects.equals(created, o.created)
+        && Objects.equals(updated, o.updated);
   }
 
   @Override
@@ -66,8 +66,8 @@
         .add("status", status)
         .add("blockingConditions", blocking)
         .add("query", query)
-        .add("createdOn", createdOn)
-        .add("updatedOn", updatedOn)
+        .add("created", created)
+        .add("updated", updated)
         .toString();
   }
 }
diff --git a/java/com/google/gerrit/plugins/checks/api/CheckerInput.java b/java/com/google/gerrit/plugins/checks/api/CheckerInput.java
index 9d609c6..1f4b498 100644
--- a/java/com/google/gerrit/plugins/checks/api/CheckerInput.java
+++ b/java/com/google/gerrit/plugins/checks/api/CheckerInput.java
@@ -23,6 +23,6 @@
   public String url;
   public String repository;
   public CheckerStatus status;
-  public Set<BlockingCondition> blockingConditions;
+  public Set<BlockingCondition> blocking;
   public String query;
 }
diff --git a/java/com/google/gerrit/plugins/checks/api/ChecksCollection.java b/java/com/google/gerrit/plugins/checks/api/ChecksCollection.java
index db02463..7a02911 100644
--- a/java/com/google/gerrit/plugins/checks/api/ChecksCollection.java
+++ b/java/com/google/gerrit/plugins/checks/api/ChecksCollection.java
@@ -70,8 +70,8 @@
             () ->
                 new ResourceNotFoundException(
                     String.format(
-                        "Patch set %s in project %s doesn't have check for checker %s.",
-                        checkKey.patchSet(), checkKey.project(), checkerUuid))));
+                        "Patch set %s in repository %s doesn't have check for checker %s.",
+                        checkKey.patchSet(), checkKey.repository(), checkerUuid))));
   }
 
   @Override
diff --git a/java/com/google/gerrit/plugins/checks/api/CreateChecker.java b/java/com/google/gerrit/plugins/checks/api/CreateChecker.java
index b5743bd..8bea2f3 100644
--- a/java/com/google/gerrit/plugins/checks/api/CreateChecker.java
+++ b/java/com/google/gerrit/plugins/checks/api/CreateChecker.java
@@ -104,9 +104,8 @@
     if (input.status != null) {
       checkerUpdateBuilder.setStatus(input.status);
     }
-    if (input.blockingConditions != null) {
-      checkerUpdateBuilder.setBlockingConditions(
-          ImmutableSortedSet.copyOf(input.blockingConditions));
+    if (input.blocking != null) {
+      checkerUpdateBuilder.setBlockingConditions(ImmutableSortedSet.copyOf(input.blocking));
     }
     if (input.query != null) {
       checkerUpdateBuilder.setQuery(CheckerQuery.clean(input.query));
diff --git a/java/com/google/gerrit/plugins/checks/api/ListPendingChecks.java b/java/com/google/gerrit/plugins/checks/api/ListPendingChecks.java
index e3da451..18e2f8c 100644
--- a/java/com/google/gerrit/plugins/checks/api/ListPendingChecks.java
+++ b/java/com/google/gerrit/plugins/checks/api/ListPendingChecks.java
@@ -120,12 +120,13 @@
   }
 
   private Optional<PendingChecksInfo> getPostFilteredPendingChecks(
-      Project.NameKey project, PatchSet.Id patchSetId) throws OrmException, IOException {
-    CheckState checkState = getCheckState(project, patchSetId);
+      Project.NameKey repositoryName, PatchSet.Id patchSetId) throws OrmException, IOException {
+    CheckState checkState = getCheckState(repositoryName, patchSetId);
     if (!states.contains(checkState)) {
       return Optional.empty();
     }
-    return Optional.of(createPendingChecksInfo(project, patchSetId, checkerUuid, checkState));
+    return Optional.of(
+        createPendingChecksInfo(repositoryName, patchSetId, checkerUuid, checkState));
   }
 
   private CheckState getCheckState(Project.NameKey project, PatchSet.Id patchSetId)
@@ -142,14 +143,14 @@
   }
 
   private static PendingChecksInfo createPendingChecksInfo(
-      Project.NameKey project,
+      Project.NameKey repositoryName,
       PatchSet.Id patchSetId,
       CheckerUuid checkerUuid,
       CheckState checkState) {
     PendingChecksInfo pendingChecksInfo = new PendingChecksInfo();
 
     pendingChecksInfo.patchSet = new CheckablePatchSetInfo();
-    pendingChecksInfo.patchSet.project = project.get();
+    pendingChecksInfo.patchSet.repository = repositoryName.get();
     pendingChecksInfo.patchSet.changeNumber = patchSetId.getParentKey().get();
     pendingChecksInfo.patchSet.patchSetId = patchSetId.get();
 
diff --git a/java/com/google/gerrit/plugins/checks/api/UpdateCheck.java b/java/com/google/gerrit/plugins/checks/api/UpdateCheck.java
index fb5b653..2b24640 100644
--- a/java/com/google/gerrit/plugins/checks/api/UpdateCheck.java
+++ b/java/com/google/gerrit/plugins/checks/api/UpdateCheck.java
@@ -46,7 +46,7 @@
     } else if (!checkResource.getCheckerUuid().get().equals(input.checkerUuid)) {
       throw new BadRequestException(
           String.format(
-              "checkerUuid must either be null or the same as on the resource:\n"
+              "checker UUID in input must either be null or the same as on the resource:\n"
                   + "the check resource belongs to checker %s,"
                   + " but in the input checker %s was specified",
               checkResource.getCheckerUuid(), input.checkerUuid));
diff --git a/java/com/google/gerrit/plugins/checks/api/UpdateChecker.java b/java/com/google/gerrit/plugins/checks/api/UpdateChecker.java
index b20117b..362fde8 100644
--- a/java/com/google/gerrit/plugins/checks/api/UpdateChecker.java
+++ b/java/com/google/gerrit/plugins/checks/api/UpdateChecker.java
@@ -103,9 +103,8 @@
       checkerUpdateBuilder.setStatus(input.status);
     }
 
-    if (input.blockingConditions != null) {
-      checkerUpdateBuilder.setBlockingConditions(
-          ImmutableSortedSet.copyOf(input.blockingConditions));
+    if (input.blocking != null) {
+      checkerUpdateBuilder.setBlockingConditions(ImmutableSortedSet.copyOf(input.blocking));
     }
 
     if (input.query != null) {
diff --git a/java/com/google/gerrit/plugins/checks/db/CheckerConfig.java b/java/com/google/gerrit/plugins/checks/db/CheckerConfig.java
index dde68b0..98738d4 100644
--- a/java/com/google/gerrit/plugins/checks/db/CheckerConfig.java
+++ b/java/com/google/gerrit/plugins/checks/db/CheckerConfig.java
@@ -166,7 +166,7 @@
   private Optional<CheckerCreation> checkerCreation = Optional.empty();
   private Optional<CheckerUpdate> checkerUpdate = Optional.empty();
   private Optional<Checker.Builder> updatedCheckerBuilder = Optional.empty();
-  private Config config;
+  private Optional<Config> loadedConfig = Optional.empty();
   private boolean isLoaded = false;
 
   private CheckerConfig(String ref) {
@@ -242,8 +242,8 @@
   }
 
   @VisibleForTesting
-  public Config getConfigForTesting() {
-    return config;
+  public Optional<Config> getConfigForTesting() {
+    return loadedConfig;
   }
 
   @Override
@@ -253,12 +253,13 @@
       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);
+      Timestamp created = new Timestamp(earliestCommit.getCommitTime() * 1000L);
+      Timestamp updated = new Timestamp(rw.parseCommit(revision).getCommitTime() * 1000L);
 
-      config = readConfig(CHECKER_CONFIG_FILE);
-      Checker checker = createFrom(config, createdOn, updatedOn, revision.toObjectId());
+      Config checkerConfig = readConfig(CHECKER_CONFIG_FILE);
+      Checker checker = createFrom(checkerConfig, created, updated, revision.toObjectId());
       loadedChecker = Optional.of(checker);
+      loadedConfig = Optional.of(checkerConfig);
       checkerUuid = Optional.of(checker.getUuid());
     }
 
@@ -275,23 +276,33 @@
 
     ensureThatMandatoryPropertiesAreSet();
 
-    // Commit timestamps are internally truncated to seconds. To return the correct 'createdOn' time
+    // Commit timestamps are internally truncated to seconds. To return the correct 'created' time
     // for new checkers, we explicitly need to truncate the timestamp here.
     Timestamp commitTimestamp =
-        TimeUtil.truncateToSecond(
-            checkerUpdate.flatMap(CheckerUpdate::getUpdatedOn).orElseGet(TimeUtil::nowTs));
+        TimeUtil.truncateToSecond(new Timestamp(commit.getCommitter().getWhen().getTime()));
     commit.setAuthor(new PersonIdent(commit.getAuthor(), commitTimestamp));
     commit.setCommitter(new PersonIdent(commit.getCommitter(), commitTimestamp));
 
-    Checker.Builder checker = updateChecker(commitTimestamp);
+    Config oldConfig = copyOrElseNew(loadedConfig);
+    Config newConfig = copyOrElseNew(loadedConfig);
+
+    Checker.Builder checker = updateChecker(newConfig, commitTimestamp);
+
+    if (oldConfig.toText().equals(newConfig.toText())) {
+      // Don't create a new commit if nothing was changed.
+      return false;
+    }
+
     updatedCheckerBuilder = Optional.of(checker);
     checkerUuid = Optional.of(checker.getUuid());
+    loadedConfig = Optional.of(newConfig);
 
     String commitMessage = createCommitMessage(loadedChecker);
     commit.setMessage(commitMessage);
 
     checkerCreation = Optional.empty();
     checkerUpdate = Optional.empty();
+
     return true;
   }
 
@@ -325,15 +336,14 @@
     return Optional.empty();
   }
 
-  private Checker.Builder updateChecker(Timestamp commitTimestamp)
+  private Checker.Builder updateChecker(Config config, Timestamp commitTimestamp)
       throws IOException, ConfigInvalidException {
-    Config config = updateCheckerProperties();
-    Timestamp createdOn = loadedChecker.map(Checker::getCreatedOn).orElse(commitTimestamp);
-    return createBuilderFrom(config, createdOn, commitTimestamp);
+    updateCheckerProperties(config);
+    Timestamp created = loadedChecker.map(Checker::getCreated).orElse(commitTimestamp);
+    return createBuilderFrom(config, created, commitTimestamp);
   }
 
-  private Config updateCheckerProperties() throws IOException, ConfigInvalidException {
-    Config config = readConfig(CHECKER_CONFIG_FILE);
+  private void updateCheckerProperties(Config config) throws IOException {
     checkerCreation.ifPresent(
         checkerCreation ->
             Arrays.stream(CheckerConfigEntry.values())
@@ -343,10 +353,9 @@
             Arrays.stream(CheckerConfigEntry.values())
                 .forEach(configEntry -> configEntry.updateConfigValue(config, checkerUpdate)));
     saveConfig(CHECKER_CONFIG_FILE, config);
-    return config;
   }
 
-  private Checker.Builder createBuilderFrom(Config config, Timestamp createdOn, Timestamp updatedOn)
+  private Checker.Builder createBuilderFrom(Config config, Timestamp created, Timestamp updated)
       throws ConfigInvalidException {
     Checker.Builder checker = Checker.builder();
 
@@ -361,16 +370,35 @@
       }
       configEntry.readFromConfig(checker.getUuid(), checker, config);
     }
-    return checker.setCreatedOn(createdOn).setUpdatedOn(updatedOn);
+    return checker.setCreated(created).setUpdated(updated);
   }
 
-  private Checker createFrom(
-      Config config, Timestamp createdOn, Timestamp updatedOn, ObjectId refState)
+  private Checker createFrom(Config config, Timestamp created, Timestamp updated, ObjectId refState)
       throws ConfigInvalidException {
-    return createBuilderFrom(config, createdOn, updatedOn).setRefState(refState).build();
+    return createBuilderFrom(config, created, updated).setRefState(refState).build();
   }
 
   private static String createCommitMessage(Optional<Checker> originalChecker) {
     return originalChecker.isPresent() ? "Update checker" : "Create checker";
   }
+
+  private static Config copyOrElseNew(Optional<Config> config) throws ConfigInvalidException {
+    return config.isPresent() ? copyConfig(config.get()) : new Config();
+  }
+
+  /**
+   * Copies the provided config.
+   *
+   * <p><strong>Note:</strong> This method only works for configs that have no base config.
+   * Unfortunately we cannot check that the provided config has no base config, so callers must take
+   * care to use this method only for configs without base config.
+   *
+   * @param config the config that should be copied
+   * @return the copy of the config
+   */
+  private static Config copyConfig(Config config) throws ConfigInvalidException {
+    Config copiedConfig = new Config();
+    copiedConfig.fromText(config.toText());
+    return copiedConfig;
+  }
 }
diff --git a/java/com/google/gerrit/plugins/checks/db/CheckersByRepositoryNotes.java b/java/com/google/gerrit/plugins/checks/db/CheckersByRepositoryNotes.java
index 65d5719..0150b56 100644
--- a/java/com/google/gerrit/plugins/checks/db/CheckersByRepositoryNotes.java
+++ b/java/com/google/gerrit/plugins/checks/db/CheckersByRepositoryNotes.java
@@ -146,10 +146,10 @@
       return load();
     }
     if (ObjectId.zeroId().equals(rev)) {
-      load(allProjectsName, repo, null);
+      super.load(allProjectsName, repo, null);
       return this;
     }
-    load(allProjectsName, repo, rev);
+    super.load(allProjectsName, repo, rev);
     return this;
   }
 
diff --git a/java/com/google/gerrit/plugins/checks/db/NoteDbCheck.java b/java/com/google/gerrit/plugins/checks/db/NoteDbCheck.java
index 3c9ad40..8f6482a 100644
--- a/java/com/google/gerrit/plugins/checks/db/NoteDbCheck.java
+++ b/java/com/google/gerrit/plugins/checks/db/NoteDbCheck.java
@@ -38,8 +38,8 @@
     return newCheck.build();
   }
 
-  Check toCheck(Project.NameKey project, PatchSet.Id patchSetId, CheckerUuid checkerUuid) {
-    CheckKey key = CheckKey.create(project, patchSetId, checkerUuid);
+  Check toCheck(Project.NameKey repositoryName, PatchSet.Id patchSetId, CheckerUuid checkerUuid) {
+    CheckKey key = CheckKey.create(repositoryName, patchSetId, checkerUuid);
     return toCheck(key);
   }
 
@@ -67,7 +67,7 @@
       started = update.started().get();
       modified = true;
     }
-    if (update.finished().isPresent() && update.finished().get().equals(finished)) {
+    if (update.finished().isPresent() && !update.finished().get().equals(finished)) {
       finished = update.finished().get();
       modified = true;
     }
diff --git a/java/com/google/gerrit/plugins/checks/db/NoteDbCheckersUpdate.java b/java/com/google/gerrit/plugins/checks/db/NoteDbCheckersUpdate.java
index 6da5a2a..5c3ccd6 100644
--- a/java/com/google/gerrit/plugins/checks/db/NoteDbCheckersUpdate.java
+++ b/java/com/google/gerrit/plugins/checks/db/NoteDbCheckersUpdate.java
@@ -33,12 +33,10 @@
 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;
@@ -227,11 +225,6 @@
   @Override
   public Checker updateChecker(CheckerUuid 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);
   }
 
diff --git a/java/com/google/gerrit/plugins/checks/db/NoteDbChecks.java b/java/com/google/gerrit/plugins/checks/db/NoteDbChecks.java
index 1e3a6f9..c73a0f1 100644
--- a/java/com/google/gerrit/plugins/checks/db/NoteDbChecks.java
+++ b/java/com/google/gerrit/plugins/checks/db/NoteDbChecks.java
@@ -85,14 +85,14 @@
       throws OrmException, IOException {
     // TODO(gerrit-team): Instead of reading the complete notes map, read just one note.
     Optional<Check> result =
-        getChecksFromNoteDb(checkKey.project(), checkKey.patchSet(), GetCheckOptions.defaults())
+        getChecksFromNoteDb(checkKey.repository(), checkKey.patchSet(), GetCheckOptions.defaults())
             .stream()
             .filter(c -> c.key().checkerUuid().equals(checkKey.checkerUuid()))
             .findAny();
 
     if (!result.isPresent() && options.backfillChecks()) {
       ChangeData changeData =
-          changeDataFactory.create(checkKey.project(), checkKey.patchSet().getParentKey());
+          changeDataFactory.create(checkKey.repository(), checkKey.patchSet().getParentKey());
       return checkBackfiller.getBackfilledCheckForRelevantChecker(
           checkKey.checkerUuid(), changeData, checkKey.patchSet());
     }
@@ -101,10 +101,10 @@
   }
 
   private ImmutableList<Check> getChecksFromNoteDb(
-      Project.NameKey projectName, PatchSet.Id psId, GetCheckOptions options)
+      Project.NameKey repositoryName, PatchSet.Id psId, GetCheckOptions options)
       throws OrmException, IOException {
     // TODO(gerrit-team): Instead of reading the complete notes map, read just one note.
-    ChangeData changeData = changeDataFactory.create(projectName, psId.getParentKey());
+    ChangeData changeData = changeDataFactory.create(repositoryName, psId.getParentKey());
     PatchSet patchSet = changeData.patchSet(psId);
     CheckNotes checkNotes = checkNotesFactory.create(changeData.change());
     checkNotes.load();
@@ -112,7 +112,7 @@
     ImmutableList<Check> existingChecks =
         checkNotes.getChecks().getOrDefault(patchSet.getRevision(), NoteDbCheckMap.empty()).checks
             .entrySet().stream()
-            .map(e -> e.getValue().toCheck(projectName, psId, CheckerUuid.parse(e.getKey())))
+            .map(e -> e.getValue().toCheck(repositoryName, psId, CheckerUuid.parse(e.getKey())))
             .collect(toImmutableList());
 
     if (!options.backfillChecks()) {
@@ -120,7 +120,7 @@
     }
 
     ImmutableList<Checker> checkersForBackfiller =
-        getCheckersForBackfiller(projectName, existingChecks);
+        getCheckersForBackfiller(repositoryName, existingChecks);
     ImmutableList<Check> backfilledChecks =
         checkBackfiller.getBackfilledChecksForRelevantCheckers(
             checkersForBackfiller, changeData, psId);
diff --git a/java/com/google/gerrit/plugins/checks/db/NoteDbChecksUpdate.java b/java/com/google/gerrit/plugins/checks/db/NoteDbChecksUpdate.java
index de2758e..87fb008 100644
--- a/java/com/google/gerrit/plugins/checks/db/NoteDbChecksUpdate.java
+++ b/java/com/google/gerrit/plugins/checks/db/NoteDbChecksUpdate.java
@@ -160,7 +160,7 @@
       assertCheckerIsPresent(checkKey.checkerUuid());
     }
 
-    try (Repository repo = repoManager.openRepository(checkKey.project());
+    try (Repository repo = repoManager.openRepository(checkKey.repository());
         ObjectInserter objectInserter = repo.newObjectInserter();
         RevWalk rw = new RevWalk(repo)) {
       Ref checkRef = repo.getRefDatabase().exactRef(checksRef(checkKey.patchSet().getParentKey()));
@@ -195,7 +195,7 @@
       RefUpdateUtil.checkResult(refUpdate);
 
       gitRefUpdated.fire(
-          checkKey.project(), refUpdate, currentUser.map(user -> user.state()).orElse(null));
+          checkKey.repository(), refUpdate, currentUser.map(user -> user.state()).orElse(null));
       return readSingleCheck(checkKey, repo, rw, newCommitId);
     }
   }
diff --git a/java/com/google/gerrit/plugins/checks/testing/BUILD b/java/com/google/gerrit/plugins/checks/testing/BUILD
index a613c7b..6e4e2b7 100644
--- a/java/com/google/gerrit/plugins/checks/testing/BUILD
+++ b/java/com/google/gerrit/plugins/checks/testing/BUILD
@@ -10,6 +10,7 @@
         "//java/com/google/gerrit/git/testing",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/truth",
+        "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/truth",
         "//lib/truth:truth-java8-extension",
         "//plugins:plugin-api",
diff --git a/java/com/google/gerrit/plugins/checks/testing/CheckerConfigSubject.java b/java/com/google/gerrit/plugins/checks/testing/CheckerConfigSubject.java
index e8715f7..60ea7cc 100644
--- a/java/com/google/gerrit/plugins/checks/testing/CheckerConfigSubject.java
+++ b/java/com/google/gerrit/plugins/checks/testing/CheckerConfigSubject.java
@@ -34,6 +34,7 @@
 import com.google.gerrit.truth.OptionalSubject;
 import java.sql.Timestamp;
 import java.util.Optional;
+import org.eclipse.jgit.lib.Config;
 
 public class CheckerConfigSubject extends Subject<CheckerConfigSubject, CheckerConfig> {
   public static CheckerConfigSubject assertThat(CheckerConfig checkerConfig) {
@@ -84,8 +85,8 @@
     check("query()").about(optionals()).that(checker().getQuery()).isEmpty();
   }
 
-  public ComparableSubject<?, Timestamp> hasCreatedOnThat() {
-    return check("createdOn()").that(checker().getCreatedOn());
+  public ComparableSubject<?, Timestamp> hasCreatedThat() {
+    return check("created()").that(checker().getCreated());
   }
 
   public ObjectIdSubject hasRefStateThat() {
@@ -94,8 +95,10 @@
 
   public IterableSubject configStringList(String name) {
     isNotNull();
+    Optional<Config> checkerConfig = actual().getConfigForTesting();
+    check("configValueOf(checker.%s)", name).about(optionals()).that(checkerConfig).isPresent();
     return check("configValueOf(checker.%s)", name)
-        .that(actual().getConfigForTesting().getStringList("checker", null, name))
+        .that(checkerConfig.get().getStringList("checker", null, name))
         .asList();
   }
 
diff --git a/java/com/google/gerrit/plugins/checks/testing/PendingChecksInfoSubject.java b/java/com/google/gerrit/plugins/checks/testing/PendingChecksInfoSubject.java
index 72c6269..a8151a0 100644
--- a/java/com/google/gerrit/plugins/checks/testing/PendingChecksInfoSubject.java
+++ b/java/com/google/gerrit/plugins/checks/testing/PendingChecksInfoSubject.java
@@ -35,8 +35,10 @@
     super(metadata, actual);
   }
 
-  public void hasProject(Project.NameKey expectedProject) {
-    check("patchSet().project()").that(patchSet().project).isEqualTo(expectedProject.get());
+  public void hasRepository(Project.NameKey expectedRepositoryName) {
+    check("patchSet().repository()")
+        .that(patchSet().repository)
+        .isEqualTo(expectedRepositoryName.get());
   }
 
   public void hasPatchSet(PatchSet.Id expectedPatchSetId) {
diff --git a/javatests/com/google/gerrit/plugins/checks/CheckerDefinitionTest.java b/javatests/com/google/gerrit/plugins/checks/CheckerDefinitionTest.java
index 7b9481e..1c8c9ab 100644
--- a/javatests/com/google/gerrit/plugins/checks/CheckerDefinitionTest.java
+++ b/javatests/com/google/gerrit/plugins/checks/CheckerDefinitionTest.java
@@ -53,8 +53,8 @@
         .setRepository(new NameKey("test-repo"))
         .setStatus(CheckerStatus.ENABLED)
         .setUuid(CheckerUuid.parse("schema:any-id"))
-        .setCreatedOn(TimeUtil.nowTs())
-        .setUpdatedOn(TimeUtil.nowTs())
+        .setCreated(TimeUtil.nowTs())
+        .setUpdated(TimeUtil.nowTs())
         .setRefState(ObjectId.zeroId());
   }
 }
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/api/CreateCheckIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/api/CreateCheckIT.java
index 8ef5e51..93bbdd4 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/api/CreateCheckIT.java
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/api/CreateCheckIT.java
@@ -15,16 +15,19 @@
 package com.google.gerrit.plugins.checks.acceptance.api;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
 
 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.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.plugins.checks.Check;
 import com.google.gerrit.plugins.checks.CheckKey;
 import com.google.gerrit.plugins.checks.CheckerUuid;
 import com.google.gerrit.plugins.checks.acceptance.AbstractCheckersTest;
 import com.google.gerrit.plugins.checks.acceptance.testsuite.CheckOperations.PerCheckOperations;
+import com.google.gerrit.plugins.checks.acceptance.testsuite.CheckTestData;
 import com.google.gerrit.plugins.checks.acceptance.testsuite.CheckerTestData;
 import com.google.gerrit.plugins.checks.api.CheckInfo;
 import com.google.gerrit.plugins.checks.api.CheckInput;
@@ -33,6 +36,7 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.testing.TestTimeUtil;
 import com.google.inject.Inject;
 import java.sql.Timestamp;
@@ -75,6 +79,7 @@
     CheckInfo info = checksApiFactory.revision(patchSetId).create(input).get();
     assertThat(info.checkerUuid).isEqualTo(checkerUuid.get());
     assertThat(info.state).isEqualTo(CheckState.RUNNING);
+    assertThat(info.url).isNull();
     assertThat(info.started).isNull();
     assertThat(info.finished).isNull();
     assertThat(info.created).isEqualTo(expectedCreationTimestamp);
@@ -92,6 +97,91 @@
   }
 
   @Test
+  public void cannotCreateCheckWithoutCheckerUuid() throws Exception {
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("checker UUID is required");
+    checksApiFactory.revision(patchSetId).create(new CheckInput());
+  }
+
+  @Test
+  public void createCheckWithStateNotStarted() throws Exception {
+    CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
+
+    CheckInput input = new CheckInput();
+    input.checkerUuid = checkerUuid.get();
+    input.state = CheckState.NOT_STARTED;
+
+    CheckInfo info = checksApiFactory.revision(patchSetId).create(input).get();
+    assertThat(info.state).isEqualTo(input.state);
+    assertThat(getCheck(project, patchSetId, checkerUuid).state()).isEqualTo(input.state);
+  }
+
+  @Test
+  public void createCheckWithoutState() throws Exception {
+    CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
+
+    CheckInput input = new CheckInput();
+    input.checkerUuid = checkerUuid.get();
+
+    CheckInfo info = checksApiFactory.revision(patchSetId).create(input).get();
+    assertThat(info.state).isEqualTo(CheckState.NOT_STARTED);
+    assertThat(getCheck(project, patchSetId, checkerUuid).state())
+        .isEqualTo(CheckState.NOT_STARTED);
+  }
+
+  @Test
+  public void createCheckWithUrl() throws Exception {
+    CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
+
+    CheckInput input = new CheckInput();
+    input.checkerUuid = checkerUuid.get();
+    input.url = "http://example.com/my-check";
+
+    CheckInfo info = checksApiFactory.revision(patchSetId).create(input).get();
+    assertThat(info.url).isEqualTo(input.url);
+    assertThat(getCheck(project, patchSetId, checkerUuid).url()).hasValue(input.url);
+  }
+
+  @Test
+  public void cannotCreateCheckWithInvalidUrl() throws Exception {
+    CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
+
+    CheckInput input = new CheckInput();
+    input.checkerUuid = checkerUuid.get();
+    input.url = CheckTestData.INVALID_URL;
+
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("only http/https URLs supported: " + input.url);
+    checksApiFactory.revision(patchSetId).create(input);
+  }
+
+  @Test
+  public void createCheckWithStarted() throws Exception {
+    CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
+
+    CheckInput input = new CheckInput();
+    input.checkerUuid = checkerUuid.get();
+    input.started = TimeUtil.nowTs();
+
+    CheckInfo info = checksApiFactory.revision(patchSetId).create(input).get();
+    assertThat(info.started).isEqualTo(input.started);
+    assertThat(getCheck(project, patchSetId, checkerUuid).started()).hasValue(input.started);
+  }
+
+  @Test
+  public void createCheckWithFinished() throws Exception {
+    CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
+
+    CheckInput input = new CheckInput();
+    input.checkerUuid = checkerUuid.get();
+    input.finished = TimeUtil.nowTs();
+
+    CheckInfo info = checksApiFactory.revision(patchSetId).create(input).get();
+    assertThat(info.finished).isEqualTo(input.finished);
+    assertThat(getCheck(project, patchSetId, checkerUuid).finished()).hasValue(input.finished);
+  }
+
+  @Test
   public void cannotCreateCheckForInvalidCheckerUuid() throws Exception {
     CheckInput input = new CheckInput();
     input.checkerUuid = CheckerTestData.INVALID_UUID;
@@ -227,6 +317,11 @@
 
   // TODO(gerrit-team) More tests, especially for multiple checkers and PS and how commits behave
 
+  private Check getCheck(Project.NameKey project, PatchSet.Id patchSetId, CheckerUuid checkerUuid)
+      throws Exception {
+    return checkOperations.check(CheckKey.create(project, patchSetId, checkerUuid)).get();
+  }
+
   private String noteDbContent(String uuid, Timestamp created, Timestamp updated) {
     return ""
         + "{\n"
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/api/CreateCheckerIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/api/CreateCheckerIT.java
index 5474101..98a04b8 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/api/CreateCheckerIT.java
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/api/CreateCheckerIT.java
@@ -37,6 +37,8 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.testing.TestTimeUtil;
 import com.google.inject.Inject;
+import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.concurrent.TimeUnit;
 import org.junit.After;
 import org.junit.Before;
@@ -49,6 +51,7 @@
   @Before
   public void setTimeForTesting() {
     TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS);
+    TestTimeUtil.setClock(Timestamp.from(Instant.EPOCH));
   }
 
   @After
@@ -60,6 +63,7 @@
   public void createChecker() throws Exception {
     Project.NameKey repositoryName = projectOperations.newProject().create();
 
+    Timestamp expectedCreationTimestamp = TestTimeUtil.getCurrentTimestamp();
     CheckerInput input = new CheckerInput();
     input.uuid = "test:my-checker";
     input.repository = repositoryName.get();
@@ -72,15 +76,12 @@
     assertThat(info.status).isEqualTo(CheckerStatus.ENABLED);
     assertThat(info.blocking).isEmpty();
     assertThat(info.query).isEqualTo("status:open");
-    assertThat(info.createdOn).isNotNull();
-    assertThat(info.updatedOn).isEqualTo(info.createdOn);
+    assertThat(info.created).isEqualTo(expectedCreationTimestamp);
+    assertThat(info.updated).isEqualTo(info.created);
 
     PerCheckerOperations perCheckerOps = checkerOperations.checker(info.uuid);
     assertCommit(
-        perCheckerOps.commit(),
-        "Create checker",
-        info.createdOn,
-        perCheckerOps.get().getRefState());
+        perCheckerOps.commit(), "Create checker", info.created, perCheckerOps.get().getRefState());
     assertThat(checkerOperations.sha1sOfRepositoriesWithCheckers())
         .containsExactly(CheckersByRepositoryNotes.computeRepositorySha1(repositoryName));
     assertThat(checkerOperations.checkersOf(repositoryName))
@@ -98,10 +99,7 @@
 
     PerCheckerOperations perCheckerOps = checkerOperations.checker(info.uuid);
     assertCommit(
-        perCheckerOps.commit(),
-        "Create checker",
-        info.createdOn,
-        perCheckerOps.get().getRefState());
+        perCheckerOps.commit(), "Create checker", info.created, perCheckerOps.get().getRefState());
   }
 
   @Test
@@ -115,10 +113,7 @@
 
     PerCheckerOperations perCheckerOps = checkerOperations.checker(info.uuid);
     assertCommit(
-        perCheckerOps.commit(),
-        "Create checker",
-        info.createdOn,
-        perCheckerOps.get().getRefState());
+        perCheckerOps.commit(), "Create checker", info.created, perCheckerOps.get().getRefState());
   }
 
   @Test
@@ -132,10 +127,7 @@
 
     PerCheckerOperations perCheckerOps = checkerOperations.checker(info.uuid);
     assertCommit(
-        perCheckerOps.commit(),
-        "Create checker",
-        info.createdOn,
-        perCheckerOps.get().getRefState());
+        perCheckerOps.commit(), "Create checker", info.created, perCheckerOps.get().getRefState());
   }
 
   @Test
@@ -149,10 +141,7 @@
 
     PerCheckerOperations perCheckerOps = checkerOperations.checker(info.uuid);
     assertCommit(
-        perCheckerOps.commit(),
-        "Create checker",
-        info.createdOn,
-        perCheckerOps.get().getRefState());
+        perCheckerOps.commit(), "Create checker", info.created, perCheckerOps.get().getRefState());
   }
 
   @Test
@@ -166,10 +155,7 @@
 
     PerCheckerOperations perCheckerOps = checkerOperations.checker(info.uuid);
     assertCommit(
-        perCheckerOps.commit(),
-        "Create checker",
-        info.createdOn,
-        perCheckerOps.get().getRefState());
+        perCheckerOps.commit(), "Create checker", info.created, perCheckerOps.get().getRefState());
   }
 
   @Test
@@ -183,10 +169,7 @@
 
     PerCheckerOperations perCheckerOps = checkerOperations.checker(info.uuid);
     assertCommit(
-        perCheckerOps.commit(),
-        "Create checker",
-        info.createdOn,
-        perCheckerOps.get().getRefState());
+        perCheckerOps.commit(), "Create checker", info.created, perCheckerOps.get().getRefState());
   }
 
   @Test
@@ -199,10 +182,7 @@
 
     PerCheckerOperations perCheckerOps = checkerOperations.checker(info.uuid);
     assertCommit(
-        perCheckerOps.commit(),
-        "Create checker",
-        info.createdOn,
-        perCheckerOps.get().getRefState());
+        perCheckerOps.commit(), "Create checker", info.created, perCheckerOps.get().getRefState());
   }
 
   @Test
@@ -346,7 +326,7 @@
     CheckerInput input = new CheckerInput();
     input.uuid = "test:my-checker";
     input.repository = allProjects.get();
-    input.blockingConditions = ImmutableSet.of(BlockingCondition.STATE_NOT_PASSING);
+    input.blocking = ImmutableSet.of(BlockingCondition.STATE_NOT_PASSING);
 
     CheckerInfo info = checkersApi.create(input).get();
     assertThat(info.blocking).containsExactly(BlockingCondition.STATE_NOT_PASSING);
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/api/GetCheckIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/api/GetCheckIT.java
index 3c9d886..2fe5429 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/api/GetCheckIT.java
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/api/GetCheckIT.java
@@ -19,6 +19,7 @@
 import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
 
+import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -35,31 +36,57 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.testing.TestTimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.concurrent.TimeUnit;
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
 public class GetCheckIT extends AbstractCheckersTest {
   @Inject private RequestScopeOperations requestScopeOperations;
 
+  private String changeId;
   private PatchSet.Id patchSetId;
 
   @Before
   public void setUp() throws Exception {
-    patchSetId = createChange().getPatchSetId();
+    TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS);
+    TestTimeUtil.setClock(Timestamp.from(Instant.EPOCH));
+
+    PushOneCommit.Result r = createChange();
+    changeId = r.getChangeId();
+    patchSetId = r.getPatchSetId();
+  }
+
+  @After
+  public void resetTime() {
+    TestTimeUtil.useSystemTime();
   }
 
   @Test
-  public void getCheckReturnsProject() throws Exception {
+  public void getCheck() throws Exception {
+    CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
+
+    CheckKey checkKey = CheckKey.create(project, patchSetId, checkerUuid);
+    checkOperations.newCheck(checkKey).setState(CheckState.RUNNING).upsert();
+
+    assertThat(checksApiFactory.revision(patchSetId).id(checkerUuid).get())
+        .isEqualTo(checkOperations.check(checkKey).asInfo());
+  }
+
+  @Test
+  public void getCheckReturnsRepository() throws Exception {
     CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
 
     CheckKey checkKey = CheckKey.create(project, patchSetId, checkerUuid);
     checkOperations.newCheck(checkKey).upsert();
 
-    assertThat(getCheckInfo(patchSetId, checkerUuid).project).isEqualTo(project.get());
-    assertThat(getCheckInfo(patchSetId, checkerUuid, ListChecksOption.CHECKER).project)
+    assertThat(getCheckInfo(patchSetId, checkerUuid).repository).isEqualTo(project.get());
+    assertThat(getCheckInfo(patchSetId, checkerUuid, ListChecksOption.CHECKER).repository)
         .isEqualTo(project.get());
   }
 
@@ -128,11 +155,10 @@
   public void getCheckReturnsCreationTimestamp() throws Exception {
     CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
 
+    Timestamp expectedCreationTimestamp = TestTimeUtil.getCurrentTimestamp();
     CheckKey checkKey = CheckKey.create(project, patchSetId, checkerUuid);
     checkOperations.newCheck(checkKey).upsert();
 
-    Timestamp expectedCreationTimestamp = checkOperations.check(checkKey).get().created();
-
     assertThat(getCheckInfo(patchSetId, checkerUuid).created).isEqualTo(expectedCreationTimestamp);
     assertThat(getCheckInfo(patchSetId, checkerUuid, ListChecksOption.CHECKER).created)
         .isEqualTo(expectedCreationTimestamp);
@@ -142,11 +168,10 @@
   public void getCheckReturnsUpdatedTimestamp() throws Exception {
     CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
 
+    Timestamp expectedUpdatedTimestamp = TestTimeUtil.getCurrentTimestamp();
     CheckKey checkKey = CheckKey.create(project, patchSetId, checkerUuid);
     checkOperations.newCheck(checkKey).upsert();
 
-    Timestamp expectedUpdatedTimestamp = checkOperations.check(checkKey).get().updated();
-
     assertThat(getCheckInfo(patchSetId, checkerUuid).created).isEqualTo(expectedUpdatedTimestamp);
     assertThat(getCheckInfo(patchSetId, checkerUuid, ListChecksOption.CHECKER).created)
         .isEqualTo(expectedUpdatedTimestamp);
@@ -195,27 +220,48 @@
   }
 
   @Test
+  public void getCheckWithCheckerOptionReturnsCheckEvenIfCheckerIsInvalid() throws Exception {
+    CheckerUuid checkerUuid =
+        checkerOperations.newChecker().repository(project).name("My Checker").create();
+
+    CheckKey checkKey = CheckKey.create(project, patchSetId, checkerUuid);
+    checkOperations.newCheck(checkKey).setState(CheckState.RUNNING).upsert();
+
+    checkerOperations.checker(checkerUuid).forUpdate().forceInvalidConfig().update();
+
+    CheckInfo check = getCheckInfo(patchSetId, checkerUuid, ListChecksOption.CHECKER);
+    assertThat(check).isNotNull();
+
+    // Checker fields are not set.
+    assertThat(check.checkerName).isNull();
+    assertThat(check.blocking).isNull();
+    assertThat(check.checkerStatus).isNull();
+
+    // Check that at least some non-checker fields are set to ensure that we didn't get a completely
+    // empty CheckInfo.
+    assertThat(check.repository).isEqualTo(project.get());
+    assertThat(check.checkerUuid).isEqualTo(checkerUuid.get());
+    assertThat(check.changeNumber).isEqualTo(patchSetId.getParentKey().get());
+    assertThat(check.patchSetId).isEqualTo(patchSetId.get());
+    assertThat(check.state).isEqualTo(CheckState.RUNNING);
+  }
+
+  @Test
   public void getCheckForCheckerThatDoesNotApplyToTheProject() throws Exception {
     Project.NameKey otherProject = createProjectOverAPI("other", null, true, null);
     CheckerUuid checkerUuid = checkerOperations.newChecker().repository(otherProject).create();
+    checkOperations.newCheck(CheckKey.create(project, patchSetId, checkerUuid)).upsert();
 
-    CheckKey checkKey = CheckKey.create(project, patchSetId, checkerUuid);
-    checkOperations.newCheck(checkKey).upsert();
-
-    assertThat(getCheckInfo(patchSetId, checkerUuid))
-        .isEqualTo(checkOperations.check(checkKey).asInfo());
+    assertThat(getCheckInfo(patchSetId, checkerUuid)).isNotNull();
   }
 
   @Test
   public void getCheckForCheckerThatDoesNotApplyToTheChange() throws Exception {
     CheckerUuid checkerUuid =
         checkerOperations.newChecker().repository(project).query("message:not-matching").create();
+    checkOperations.newCheck(CheckKey.create(project, patchSetId, checkerUuid)).upsert();
 
-    CheckKey checkKey = CheckKey.create(project, patchSetId, checkerUuid);
-    checkOperations.newCheck(checkKey).upsert();
-
-    assertThat(getCheckInfo(patchSetId, checkerUuid))
-        .isEqualTo(checkOperations.check(checkKey).asInfo());
+    assertThat(getCheckInfo(patchSetId, checkerUuid)).isNotNull();
   }
 
   @Test
@@ -226,12 +272,9 @@
             .repository(project)
             .query(CheckerTestData.QUERY_WITH_UNSUPPORTED_OPERATOR)
             .create();
+    checkOperations.newCheck(CheckKey.create(project, patchSetId, checkerUuid)).upsert();
 
-    CheckKey checkKey = CheckKey.create(project, patchSetId, checkerUuid);
-    checkOperations.newCheck(checkKey).upsert();
-
-    CheckInfo checkInfo = checksApiFactory.revision(patchSetId).id(checkerUuid).get();
-    assertThat(checkInfo).isEqualTo(checkOperations.check(checkKey).asInfo());
+    assertThat(getCheckInfo(patchSetId, checkerUuid)).isNotNull();
   }
 
   @Test
@@ -242,48 +285,79 @@
             .repository(project)
             .query(CheckerTestData.INVALID_QUERY)
             .create();
+    checkOperations.newCheck(CheckKey.create(project, patchSetId, checkerUuid)).upsert();
 
-    CheckKey checkKey = CheckKey.create(project, patchSetId, checkerUuid);
-    checkOperations.newCheck(checkKey).upsert();
-
-    CheckInfo checkInfo = checksApiFactory.revision(patchSetId).id(checkerUuid).get();
-    assertThat(checkInfo).isEqualTo(checkOperations.check(checkKey).asInfo());
+    assertThat(getCheckInfo(patchSetId, checkerUuid)).isNotNull();
   }
 
   @Test
   public void getCheckForDisabledChecker() throws Exception {
     CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
-
-    CheckKey checkKey = CheckKey.create(project, patchSetId, checkerUuid);
-    checkOperations.newCheck(checkKey).upsert();
-
+    checkOperations.newCheck(CheckKey.create(project, patchSetId, checkerUuid)).upsert();
     checkerOperations.checker(checkerUuid).forUpdate().disable().update();
 
-    assertThat(getCheckInfo(patchSetId, checkerUuid))
-        .isEqualTo(checkOperations.check(checkKey).asInfo());
+    assertThat(getCheckInfo(patchSetId, checkerUuid)).isNotNull();
   }
 
   @Test
   public void getCheckForInvalidChecker() throws Exception {
     CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
-    CheckKey checkKey = CheckKey.create(project, patchSetId, checkerUuid);
-    checkOperations.newCheck(checkKey).upsert();
-
+    checkOperations.newCheck(CheckKey.create(project, patchSetId, checkerUuid)).upsert();
     checkerOperations.checker(checkerUuid).forUpdate().forceInvalidConfig().update();
 
-    assertThat(getCheckInfo(patchSetId, checkerUuid))
-        .isEqualTo(checkOperations.check(checkKey).asInfo());
+    assertThat(getCheckInfo(patchSetId, checkerUuid)).isNotNull();
+  }
+
+  @Test
+  public void noBackfillForInvalidChecker() throws Exception {
+    CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
+    assertThat(getCheckInfo(patchSetId, checkerUuid)).isNotNull();
+
+    checkerOperations.checker(checkerUuid).forUpdate().forceInvalidConfig().update();
+    assertCheckNotFound(patchSetId, checkerUuid);
+  }
+
+  @Test
+  public void getChecksForMultiplePatchSets() throws Exception {
+    CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
+
+    CheckKey checkKey1 = CheckKey.create(project, patchSetId, checkerUuid);
+    checkOperations.newCheck(checkKey1).setState(CheckState.FAILED).upsert();
+
+    PatchSet.Id currentPatchSetId = createPatchSet();
+    CheckKey checkKey2 = CheckKey.create(project, currentPatchSetId, checkerUuid);
+    checkOperations.newCheck(checkKey2).setState(CheckState.RUNNING).upsert();
+
+    // get check for the old patch set
+    assertThat(checksApiFactory.revision(patchSetId).id(checkerUuid).get())
+        .isEqualTo(checkOperations.check(checkKey1).asInfo());
+
+    // get check for the current patch set (2 ways)
+    assertThat(checksApiFactory.revision(currentPatchSetId).id(checkerUuid).get())
+        .isEqualTo(checkOperations.check(checkKey2).asInfo());
+    assertThat(
+            checksApiFactory
+                .currentRevision(currentPatchSetId.getParentKey())
+                .id(checkerUuid)
+                .get())
+        .isEqualTo(checkOperations.check(checkKey2).asInfo());
+  }
+
+  @Test
+  public void noBackfillForNonCurrentPatchSet() throws Exception {
+    CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
+    PatchSet.Id currentPatchSetId = createPatchSet();
+    assertCheckNotFound(patchSetId, checkerUuid);
+    assertThat(getCheckInfo(currentPatchSetId, checkerUuid)).isNotNull();
   }
 
   @Test
   public void getCheckForNonExistingChecker() throws Exception {
     CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
-    CheckKey checkKey = CheckKey.create(project, patchSetId, checkerUuid);
-    checkOperations.newCheck(checkKey).upsert();
+    checkOperations.newCheck(CheckKey.create(project, patchSetId, checkerUuid)).upsert();
     checkerOperations.checker(checkerUuid).forUpdate().deleteRef().update();
 
-    assertThat(getCheckInfo(patchSetId, checkerUuid))
-        .isEqualTo(checkOperations.check(checkKey).asInfo());
+    assertThat(getCheckInfo(patchSetId, checkerUuid)).isNotNull();
   }
 
   @Test
@@ -300,7 +374,7 @@
 
     Timestamp psCreated = getPatchSetCreated(changeId);
     CheckInfo expected = new CheckInfo();
-    expected.project = checkKey.project().get();
+    expected.repository = checkKey.repository().get();
     expected.changeNumber = checkKey.patchSet().getParentKey().get();
     expected.patchSetId = checkKey.patchSet().get();
     expected.checkerUuid = checkKey.checkerUuid().get();
@@ -318,7 +392,7 @@
 
     Timestamp psCreated = getPatchSetCreated(changeId);
     CheckInfo expected = new CheckInfo();
-    expected.project = checkKey.project().get();
+    expected.repository = checkKey.repository().get();
     expected.changeNumber = checkKey.patchSet().getParentKey().get();
     expected.patchSetId = checkKey.patchSet().get();
     expected.checkerUuid = checkKey.checkerUuid().get();
@@ -344,12 +418,9 @@
     requestScopeOperations.setApiUser(user.getId());
 
     CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
+    checkOperations.newCheck(CheckKey.create(project, patchSetId, checkerUuid)).upsert();
 
-    CheckKey checkKey = CheckKey.create(project, patchSetId, checkerUuid);
-    checkOperations.newCheck(checkKey).upsert();
-
-    assertThat(getCheckInfo(patchSetId, checkerUuid))
-        .isEqualTo(checkOperations.check(checkKey).asInfo());
+    assertThat(getCheckInfo(patchSetId, checkerUuid)).isNotNull();
   }
 
   @Test
@@ -393,8 +464,16 @@
           .hasMessageThat()
           .isEqualTo(
               String.format(
-                  "Patch set %s in project %s doesn't have check for checker %s.",
+                  "Patch set %s in repository %s doesn't have check for checker %s.",
                   patchSetId, project, checkerUuid));
     }
   }
+
+  private PatchSet.Id createPatchSet() throws Exception {
+    PushOneCommit.Result r = amendChange(changeId);
+    PatchSet.Id currentPatchSetId = r.getPatchSetId();
+    assertThat(patchSetId.changeId).isEqualTo(currentPatchSetId.changeId);
+    assertThat(patchSetId.get()).isLessThan(currentPatchSetId.get());
+    return currentPatchSetId;
+  }
 }
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/api/GetCheckerIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/api/GetCheckerIT.java
index 960e3bc..de28631 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/api/GetCheckerIT.java
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/api/GetCheckerIT.java
@@ -30,15 +30,31 @@
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.restapi.config.ListCapabilities;
 import com.google.gerrit.server.restapi.config.ListCapabilities.CapabilityInfo;
+import com.google.gerrit.testing.TestTimeUtil;
 import com.google.inject.Inject;
 import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import org.junit.After;
+import org.junit.Before;
 import org.junit.Test;
 
 public class GetCheckerIT extends AbstractCheckersTest {
   @Inject private RequestScopeOperations requestScopeOperations;
   @Inject private ListCapabilities listCapabilities;
 
+  @Before
+  public void setUp() throws Exception {
+    TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS);
+    TestTimeUtil.setClock(Timestamp.from(Instant.EPOCH));
+  }
+
+  @After
+  public void resetTime() {
+    TestTimeUtil.useSystemTime();
+  }
+
   @Test
   public void getCheckerReturnsUuid() throws Exception {
     CheckerUuid checkerUuid = checkerOperations.newChecker().create();
@@ -141,20 +157,18 @@
 
   @Test
   public void getCheckerReturnsCreationTimestamp() throws Exception {
+    Timestamp expectedCreationTimestamp = TestTimeUtil.getCurrentTimestamp();
     CheckerUuid checkerUuid = checkerOperations.newChecker().create();
 
-    Timestamp expectedCreationTimestamp =
-        checkerOperations.checker(checkerUuid).get().getCreatedOn();
-    assertThat(getCheckerInfo(checkerUuid).createdOn).isEqualTo(expectedCreationTimestamp);
+    assertThat(getCheckerInfo(checkerUuid).created).isEqualTo(expectedCreationTimestamp);
   }
 
   @Test
   public void getCheckerReturnsUpdatedTimestamp() throws Exception {
+    Timestamp expectedUpdatedTimestamp = TestTimeUtil.getCurrentTimestamp();
     CheckerUuid checkerUuid = checkerOperations.newChecker().create();
 
-    Timestamp expectedUpdatedTimestamp =
-        checkerOperations.checker(checkerUuid).get().getUpdatedOn();
-    assertThat(getCheckerInfo(checkerUuid).updatedOn).isEqualTo(expectedUpdatedTimestamp);
+    assertThat(getCheckerInfo(checkerUuid).updated).isEqualTo(expectedUpdatedTimestamp);
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/api/ListChecksIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/api/ListChecksIT.java
index cdd5f29..3b3012a 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/api/ListChecksIT.java
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/api/ListChecksIT.java
@@ -20,6 +20,7 @@
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.plugins.checks.CheckKey;
 import com.google.gerrit.plugins.checks.CheckerUuid;
@@ -39,44 +40,44 @@
 import org.junit.Test;
 
 public class ListChecksIT extends AbstractCheckersTest {
+  private String changeId;
   private PatchSet.Id patchSetId;
-  private CheckKey checkKey1;
-  private CheckKey checkKey2;
 
   @Before
   public void setUp() throws Exception {
-    patchSetId = createChange().getPatchSetId();
-
-    CheckerUuid checker1Uuid = checkerOperations.newChecker().repository(project).create();
-    CheckerUuid checker2Uuid =
-        checkerOperations.newChecker().name("Checker Two").repository(project).create();
-
-    checkKey1 = CheckKey.create(project, patchSetId, checker1Uuid);
-    checkOperations.newCheck(checkKey1).setState(CheckState.RUNNING).upsert();
-
-    checkKey2 = CheckKey.create(project, patchSetId, checker2Uuid);
-    checkOperations.newCheck(checkKey2).setState(CheckState.RUNNING).upsert();
+    PushOneCommit.Result r = createChange();
+    changeId = r.getChangeId();
+    patchSetId = r.getPatchSetId();
   }
 
   @Test
   public void listAll() throws Exception {
+    CheckerUuid checkerUuid1 = checkerOperations.newChecker().repository(project).create();
+    CheckerUuid checkerUuid2 = checkerOperations.newChecker().repository(project).create();
+
+    CheckKey checkKey1 = CheckKey.create(project, patchSetId, checkerUuid1);
+    checkOperations.newCheck(checkKey1).setState(CheckState.RUNNING).upsert();
+
+    CheckKey checkKey2 = CheckKey.create(project, patchSetId, checkerUuid2);
+    checkOperations.newCheck(checkKey2).setState(CheckState.FAILED).upsert();
+
     Collection<CheckInfo> checks = checksApiFactory.revision(patchSetId).list();
 
     CheckInfo expected1 = new CheckInfo();
-    expected1.project = checkKey1.project().get();
-    expected1.changeNumber = checkKey1.patchSet().getParentKey().get();
-    expected1.patchSetId = checkKey1.patchSet().get();
-    expected1.checkerUuid = checkKey1.checkerUuid().get();
+    expected1.repository = project.get();
+    expected1.changeNumber = patchSetId.getParentKey().get();
+    expected1.patchSetId = patchSetId.get();
+    expected1.checkerUuid = checkerUuid1.get();
     expected1.state = CheckState.RUNNING;
     expected1.created = checkOperations.check(checkKey1).get().created();
     expected1.updated = expected1.created;
 
     CheckInfo expected2 = new CheckInfo();
-    expected2.project = checkKey2.project().get();
-    expected2.changeNumber = checkKey2.patchSet().getParentKey().get();
-    expected2.patchSetId = checkKey2.patchSet().get();
-    expected2.checkerUuid = checkKey2.checkerUuid().get();
-    expected2.state = CheckState.RUNNING;
+    expected2.repository = project.get();
+    expected2.changeNumber = patchSetId.getParentKey().get();
+    expected2.patchSetId = patchSetId.get();
+    expected2.checkerUuid = checkerUuid2.get();
+    expected2.state = CheckState.FAILED;
     expected2.created = checkOperations.check(checkKey2).get().created();
     expected2.updated = expected2.created;
 
@@ -85,29 +86,40 @@
 
   @Test
   public void listAllWithOptions() throws Exception {
+    String checkerName1 = "Checker One";
+    CheckerUuid checkerUuid1 =
+        checkerOperations.newChecker().name(checkerName1).repository(project).create();
+    CheckerUuid checkerUuid2 = checkerOperations.newChecker().repository(project).create();
+
+    CheckKey checkKey1 = CheckKey.create(project, patchSetId, checkerUuid1);
+    checkOperations.newCheck(checkKey1).setState(CheckState.RUNNING).upsert();
+
+    CheckKey checkKey2 = CheckKey.create(project, patchSetId, checkerUuid2);
+    checkOperations.newCheck(checkKey2).setState(CheckState.FAILED).upsert();
+
     Collection<CheckInfo> checks =
         checksApiFactory.revision(patchSetId).list(ListChecksOption.CHECKER);
 
     CheckInfo expected1 = new CheckInfo();
-    expected1.project = checkKey1.project().get();
-    expected1.changeNumber = checkKey1.patchSet().getParentKey().get();
-    expected1.patchSetId = checkKey1.patchSet().get();
-    expected1.checkerUuid = checkKey1.checkerUuid().get();
+    expected1.repository = project.get();
+    expected1.changeNumber = patchSetId.getParentKey().get();
+    expected1.patchSetId = patchSetId.get();
+    expected1.checkerUuid = checkerUuid1.get();
     expected1.state = CheckState.RUNNING;
     expected1.created = checkOperations.check(checkKey1).get().created();
     expected1.updated = expected1.created;
+    expected1.checkerName = checkerName1;
     expected1.blocking = ImmutableSet.of();
     expected1.checkerStatus = CheckerStatus.ENABLED;
 
     CheckInfo expected2 = new CheckInfo();
-    expected2.project = checkKey2.project().get();
-    expected2.changeNumber = checkKey2.patchSet().getParentKey().get();
-    expected2.patchSetId = checkKey2.patchSet().get();
-    expected2.checkerUuid = checkKey2.checkerUuid().get();
-    expected2.state = CheckState.RUNNING;
+    expected2.repository = project.get();
+    expected2.changeNumber = patchSetId.getParentKey().get();
+    expected2.patchSetId = patchSetId.get();
+    expected2.checkerUuid = checkerUuid2.get();
+    expected2.state = CheckState.FAILED;
     expected2.created = checkOperations.check(checkKey2).get().created();
     expected2.updated = expected2.created;
-    expected2.checkerName = "Checker Two";
     expected2.blocking = ImmutableSet.of();
     expected2.checkerStatus = CheckerStatus.ENABLED;
 
@@ -116,141 +128,164 @@
 
   @Test
   public void listAllWithOptionsSkipsPopulatingCheckerFieldsForInvalidCheckers() throws Exception {
-    checkerOperations.checker(checkKey1.checkerUuid()).forUpdate().forceInvalidConfig().update();
+    String checkerName1 = "Checker One";
+    CheckerUuid checkerUuid1 =
+        checkerOperations.newChecker().name(checkerName1).repository(project).create();
+    CheckerUuid checkerUuid2 = checkerOperations.newChecker().repository(project).create();
+    checkOperations.newCheck(CheckKey.create(project, patchSetId, checkerUuid1)).upsert();
+    checkOperations.newCheck(CheckKey.create(project, patchSetId, checkerUuid2)).upsert();
+    checkerOperations.checker(checkerUuid2).forUpdate().forceInvalidConfig().update();
 
     List<CheckInfo> checks = checksApiFactory.revision(patchSetId).list(ListChecksOption.CHECKER);
 
     assertThat(checks).hasSize(2);
 
     Optional<CheckInfo> maybeCheck1 =
-        checks.stream().filter(c -> c.checkerUuid.equals(checkKey1.checkerUuid().get())).findAny();
+        checks.stream().filter(c -> c.checkerUuid.equals(checkerUuid1.get())).findAny();
     assertThat(maybeCheck1).isPresent();
     CheckInfo check1 = maybeCheck1.get();
-    assertThat(check1.checkerName).isNull();
-    assertThat(check1.blocking).isNull();
-    assertThat(check1.checkerStatus).isNull();
+    assertThat(check1.checkerName).isEqualTo(checkerName1);
+    assertThat(check1.blocking).isEmpty();
+    assertThat(check1.checkerStatus).isEqualTo(CheckerStatus.ENABLED);
 
     Optional<CheckInfo> maybeCheck2 =
-        checks.stream().filter(c -> c.checkerUuid.equals(checkKey2.checkerUuid().get())).findAny();
+        checks.stream().filter(c -> c.checkerUuid.equals(checkerUuid2.get())).findAny();
     assertThat(maybeCheck2).isPresent();
     CheckInfo check2 = maybeCheck2.get();
-    assertThat(check2.checkerName).isEqualTo("Checker Two");
-    assertThat(check2.blocking).isEmpty();
-    assertThat(check2.checkerStatus).isEqualTo(CheckerStatus.ENABLED);
+    assertThat(check2.checkerName).isNull();
+    assertThat(check2.blocking).isNull();
+    assertThat(check2.checkerStatus).isNull();
   }
 
   @Test
   public void listIncludesCheckFromCheckerThatDoesNotApplyToTheProject() throws Exception {
+    CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
+    checkOperations.newCheck(CheckKey.create(project, patchSetId, checkerUuid)).upsert();
     Project.NameKey otherProject = createProjectOverAPI("other", null, true, null);
-    checkerOperations
-        .checker(checkKey2.checkerUuid())
-        .forUpdate()
-        .repository(otherProject)
-        .update();
+    checkerOperations.checker(checkerUuid).forUpdate().repository(otherProject).update();
 
-    Collection<CheckInfo> checks = checksApiFactory.revision(patchSetId).list();
-    assertThat(checks)
-        .containsExactly(
-            checkOperations.check(checkKey1).asInfo(), checkOperations.check(checkKey2).asInfo());
+    assertThat(checksApiFactory.revision(patchSetId).list()).hasSize(1);
   }
 
   @Test
   public void listIncludesCheckFromCheckerThatDoesNotApplyToTheChange() throws Exception {
-    checkerOperations
-        .checker(checkKey2.checkerUuid())
-        .forUpdate()
-        .query("message:not-matching")
-        .update();
+    CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
 
-    Collection<CheckInfo> checks = checksApiFactory.revision(patchSetId).list();
-    assertThat(checks)
-        .containsExactly(
-            checkOperations.check(checkKey1).asInfo(), checkOperations.check(checkKey2).asInfo());
+    CheckKey checkKey = CheckKey.create(project, patchSetId, checkerUuid);
+    checkOperations.newCheck(checkKey).upsert();
+
+    checkerOperations.checker(checkerUuid).forUpdate().query("message:not-matching").update();
+
+    assertThat(checksApiFactory.revision(patchSetId).list()).hasSize(1);
   }
 
   @Test
   public void listIncludesCheckFromDisabledChecker() throws Exception {
-    checkerOperations.checker(checkKey2.checkerUuid()).forUpdate().disable().update();
+    CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
+    checkOperations.newCheck(CheckKey.create(project, patchSetId, checkerUuid)).upsert();
+    checkerOperations.checker(checkerUuid).forUpdate().disable().update();
 
-    Collection<CheckInfo> checks = checksApiFactory.revision(patchSetId).list();
-    assertThat(checks)
-        .containsExactly(
-            checkOperations.check(checkKey1).asInfo(), checkOperations.check(checkKey2).asInfo());
+    assertThat(checksApiFactory.revision(patchSetId).list()).hasSize(1);
   }
 
   @Test
   public void listIncludesCheckFromInvalidChecker() throws Exception {
-    checkerOperations.checker(checkKey2.checkerUuid()).forUpdate().forceInvalidConfig().update();
+    CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
+    checkOperations.newCheck(CheckKey.create(project, patchSetId, checkerUuid)).upsert();
+    checkerOperations.checker(checkerUuid).forUpdate().forceInvalidConfig().update();
 
-    Collection<CheckInfo> checks = checksApiFactory.revision(patchSetId).list();
-    assertThat(checks)
-        .containsExactly(
-            checkOperations.check(checkKey1).asInfo(), checkOperations.check(checkKey2).asInfo());
+    assertThat(checksApiFactory.revision(patchSetId).list()).hasSize(1);
   }
 
   @Test
   public void listIncludesCheckFromNonExistingChecker() throws Exception {
-    checkerOperations.checker(checkKey2.checkerUuid()).forUpdate().deleteRef().update();
+    CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
+    checkOperations.newCheck(CheckKey.create(project, patchSetId, checkerUuid)).upsert();
+    checkerOperations.checker(checkerUuid).forUpdate().deleteRef().update();
 
-    Collection<CheckInfo> checks = checksApiFactory.revision(patchSetId).list();
-    assertThat(checks)
-        .containsExactly(
-            checkOperations.check(checkKey1).asInfo(), checkOperations.check(checkKey2).asInfo());
+    assertThat(checksApiFactory.revision(patchSetId).list()).hasSize(1);
   }
 
   @Test
   public void listBackfillsForRelevantChecker() throws Exception {
     String topic = name("topic");
-    Change.Id changeId = patchSetId.getParentKey();
-    CheckerUuid checker3Uuid =
+    CheckerUuid checkerUuid =
         checkerOperations.newChecker().repository(project).query("topic:" + topic).create();
-    CheckKey checkKey3 = CheckKey.create(project, patchSetId, checker3Uuid);
 
-    CheckInfo checkInfo1 = checkOperations.check(checkKey1).asInfo();
-    CheckInfo checkInfo2 = checkOperations.check(checkKey2).asInfo();
-    assertThat(checksApiFactory.revision(patchSetId).list())
-        .containsExactly(checkInfo1, checkInfo2);
+    assertThat(checksApiFactory.revision(patchSetId).list()).isEmpty();
 
-    // Update change to match checker3's query.
-    gApi.changes().id(changeId.get()).topic(topic);
+    // Update change to match checker's query.
+    gApi.changes().id(patchSetId.getParentKey().get()).topic(topic);
 
     Timestamp psCreated = getPatchSetCreated(patchSetId.getParentKey());
-    CheckInfo checkInfo3 = new CheckInfo();
-    checkInfo3.project = checkKey3.project().get();
-    checkInfo3.changeNumber = checkKey3.patchSet().getParentKey().get();
-    checkInfo3.patchSetId = checkKey3.patchSet().get();
-    checkInfo3.checkerUuid = checkKey3.checkerUuid().get();
-    checkInfo3.state = CheckState.NOT_STARTED;
-    checkInfo3.created = psCreated;
-    checkInfo3.updated = psCreated;
-    assertThat(checksApiFactory.revision(patchSetId).list())
-        .containsExactly(checkInfo1, checkInfo2, checkInfo3);
+    CheckInfo checkInfo = new CheckInfo();
+    checkInfo.repository = project.get();
+    checkInfo.changeNumber = patchSetId.getParentKey().get();
+    checkInfo.patchSetId = patchSetId.get();
+    checkInfo.checkerUuid = checkerUuid.get();
+    checkInfo.state = CheckState.NOT_STARTED;
+    checkInfo.created = psCreated;
+    checkInfo.updated = psCreated;
+    assertThat(checksApiFactory.revision(patchSetId).list()).containsExactly(checkInfo);
   }
 
   @Test
   public void listDoesntBackfillForDisabledChecker() throws Exception {
-    CheckerUuid checker3Uuid = checkerOperations.newChecker().repository(project).create();
-    CheckKey checkKey3 = CheckKey.create(project, patchSetId, checker3Uuid);
+    CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
+    CheckKey checkKey = CheckKey.create(project, patchSetId, checkerUuid);
 
-    CheckInfo checkInfo1 = checkOperations.check(checkKey1).asInfo();
-    CheckInfo checkInfo2 = checkOperations.check(checkKey2).asInfo();
     Timestamp psCreated = getPatchSetCreated(patchSetId.getParentKey());
-    CheckInfo checkInfo3 = new CheckInfo();
-    checkInfo3.project = checkKey3.project().get();
-    checkInfo3.changeNumber = checkKey3.patchSet().getParentKey().get();
-    checkInfo3.patchSetId = checkKey3.patchSet().get();
-    checkInfo3.checkerUuid = checkKey3.checkerUuid().get();
-    checkInfo3.state = CheckState.NOT_STARTED;
-    checkInfo3.created = psCreated;
-    checkInfo3.updated = psCreated;
-    assertThat(checksApiFactory.revision(patchSetId).list())
-        .containsExactly(checkInfo1, checkInfo2, checkInfo3);
+    CheckInfo checkInfo = new CheckInfo();
+    checkInfo.repository = checkKey.repository().get();
+    checkInfo.changeNumber = checkKey.patchSet().getParentKey().get();
+    checkInfo.patchSetId = checkKey.patchSet().get();
+    checkInfo.checkerUuid = checkKey.checkerUuid().get();
+    checkInfo.state = CheckState.NOT_STARTED;
+    checkInfo.created = psCreated;
+    checkInfo.updated = psCreated;
+    assertThat(checksApiFactory.revision(patchSetId).list()).containsExactly(checkInfo);
 
-    // Disable checker3.
-    checkerOperations.checker(checker3Uuid).forUpdate().disable().update();
+    // Disable checker.
+    checkerOperations.checker(checkerUuid).forUpdate().disable().update();
 
+    assertThat(checksApiFactory.revision(patchSetId).list()).isEmpty();
+  }
+
+  @Test
+  public void listForMultiplePatchSets() throws Exception {
+    CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
+
+    // create check on the first patch set
+    CheckKey checkKey1 = CheckKey.create(project, patchSetId, checkerUuid);
+    checkOperations.newCheck(checkKey1).setState(CheckState.SUCCESSFUL).upsert();
+
+    // create a second patch set
+    PatchSet.Id currentPatchSet = createPatchSet();
+
+    // create check on the second patch set, we expect that this check is not returned for the first
+    // patch set
+    CheckKey checkKey2 = CheckKey.create(project, currentPatchSet, checkerUuid);
+    checkOperations.newCheck(checkKey2).setState(CheckState.RUNNING).upsert();
+
+    // list checks for the old patch set
     assertThat(checksApiFactory.revision(patchSetId).list())
-        .containsExactly(checkInfo1, checkInfo2);
+        .containsExactly(checkOperations.check(checkKey1).asInfo());
+
+    // list checks for the new patch set (2 ways)
+    assertThat(checksApiFactory.currentRevision(currentPatchSet.getParentKey()).list())
+        .containsExactly(checkOperations.check(checkKey2).asInfo());
+    assertThat(checksApiFactory.revision(currentPatchSet).list())
+        .containsExactly(checkOperations.check(checkKey2).asInfo());
+  }
+
+  @Test
+  public void noBackfillForNonCurrentPatchSet() throws Exception {
+    checkerOperations.newChecker().repository(project).create();
+
+    PatchSet.Id currentPatchSet = createPatchSet();
+
+    assertThat(checksApiFactory.revision(patchSetId).list()).isEmpty();
+    assertThat(checksApiFactory.revision(currentPatchSet).list()).hasSize(1);
   }
 
   private Timestamp getPatchSetCreated(Change.Id changeId) throws RestApiException {
@@ -258,4 +293,12 @@
             gApi.changes().id(changeId.get()).get(CURRENT_REVISION).revisions.values())
         .created;
   }
+
+  private PatchSet.Id createPatchSet() throws Exception {
+    PushOneCommit.Result r = amendChange(changeId);
+    PatchSet.Id currentPatchSetId = r.getPatchSetId();
+    assertThat(patchSetId.changeId).isEqualTo(currentPatchSetId.changeId);
+    assertThat(patchSetId.get()).isLessThan(currentPatchSetId.get());
+    return currentPatchSetId;
+  }
 }
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/api/ListPendingChecksIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/api/ListPendingChecksIT.java
index 60fe8f9..41ddec3 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/api/ListPendingChecksIT.java
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/api/ListPendingChecksIT.java
@@ -114,7 +114,7 @@
     List<PendingChecksInfo> pendingChecksList = pendingChecksApi.list(checkerUuid);
     assertThat(pendingChecksList).hasSize(1);
     PendingChecksInfo pendingChecks = Iterables.getOnlyElement(pendingChecksList);
-    assertThat(pendingChecks).hasProject(project);
+    assertThat(pendingChecks).hasRepository(project);
     assertThat(pendingChecks).hasPatchSet(patchSetId);
     assertThat(pendingChecks)
         .hasPendingChecksMapThat()
@@ -149,7 +149,7 @@
         pendingChecksApi.list(checkerUuid, CheckState.FAILED);
     assertThat(pendingChecksList).hasSize(1);
     PendingChecksInfo pendingChecks = Iterables.getOnlyElement(pendingChecksList);
-    assertThat(pendingChecks).hasProject(project);
+    assertThat(pendingChecks).hasRepository(project);
     assertThat(pendingChecks).hasPatchSet(patchSetId);
     assertThat(pendingChecks)
         .hasPendingChecksMapThat()
@@ -195,14 +195,14 @@
     // returned from the change index, which is by last updated timestamp. Use this knowledge here
     // to do the assertions although the REST endpoint doesn't document a guaranteed sort order.
     PendingChecksInfo pendingChecksChange2 = pendingChecksList.get(0);
-    assertThat(pendingChecksChange2).hasProject(project);
+    assertThat(pendingChecksChange2).hasRepository(project);
     assertThat(pendingChecksChange2).hasPatchSet(patchSetId2);
     assertThat(pendingChecksChange2)
         .hasPendingChecksMapThat()
         .containsExactly(checkerUuid.get(), new PendingCheckInfo(CheckState.SCHEDULED));
 
     PendingChecksInfo pendingChecksChange1 = pendingChecksList.get(1);
-    assertThat(pendingChecksChange1).hasProject(project);
+    assertThat(pendingChecksChange1).hasRepository(project);
     assertThat(pendingChecksChange1).hasPatchSet(patchSetId);
     assertThat(pendingChecksChange1)
         .hasPendingChecksMapThat()
@@ -215,7 +215,7 @@
     List<PendingChecksInfo> pendingChecksList = pendingChecksApi.list(checkerUuid);
     assertThat(pendingChecksList).hasSize(1);
     PendingChecksInfo pendingChecks = Iterables.getOnlyElement(pendingChecksList);
-    assertThat(pendingChecks).hasProject(project);
+    assertThat(pendingChecks).hasRepository(project);
     assertThat(pendingChecks).hasPatchSet(patchSetId);
     assertThat(pendingChecks)
         .hasPendingChecksMapThat()
@@ -318,6 +318,21 @@
   }
 
   @Test
+  public void listPendingChecksForCheckerWithInvalidQueryFails() throws Exception {
+    CheckerUuid checkerUuid =
+        checkerOperations
+            .newChecker()
+            .repository(project)
+            .query(CheckerTestData.INVALID_QUERY)
+            .create();
+
+    exception.expect(RestApiException.class);
+    exception.expectMessage("Cannot list pending checks");
+    exception.expectCause(instanceOf(ConfigInvalidException.class));
+    pendingChecksApi.list(checkerUuid);
+  }
+
+  @Test
   public void listPendingChecksWithoutAdministrateCheckersCapabilityWorks() throws Exception {
     CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
     checkOperations
@@ -330,7 +345,7 @@
         pendingChecksApi.list(checkerUuid, CheckState.NOT_STARTED);
     assertThat(pendingChecksList).hasSize(1);
     PendingChecksInfo pendingChecks = Iterables.getOnlyElement(pendingChecksList);
-    assertThat(pendingChecks).hasProject(project);
+    assertThat(pendingChecks).hasRepository(project);
     assertThat(pendingChecks).hasPatchSet(patchSetId);
     assertThat(pendingChecks)
         .hasPendingChecksMapThat()
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/api/UpdateCheckIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/api/UpdateCheckIT.java
index 7ae4fac..debef5f 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/api/UpdateCheckIT.java
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/api/UpdateCheckIT.java
@@ -18,16 +18,24 @@
 
 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.plugins.checks.CheckKey;
 import com.google.gerrit.plugins.checks.CheckerUuid;
 import com.google.gerrit.plugins.checks.acceptance.AbstractCheckersTest;
+import com.google.gerrit.plugins.checks.acceptance.testsuite.CheckTestData;
 import com.google.gerrit.plugins.checks.acceptance.testsuite.CheckerTestData;
 import com.google.gerrit.plugins.checks.api.CheckInfo;
 import com.google.gerrit.plugins.checks.api.CheckInput;
 import com.google.gerrit.plugins.checks.api.CheckState;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.util.time.TimeUtil;
+import com.google.gerrit.testing.TestTimeUtil;
 import com.google.inject.Inject;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.concurrent.TimeUnit;
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -39,6 +47,9 @@
 
   @Before
   public void setUp() throws Exception {
+    TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS);
+    TestTimeUtil.setClock(Timestamp.from(Instant.EPOCH));
+
     patchSetId = createChange().getPatchSetId();
 
     CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
@@ -46,6 +57,11 @@
     checkOperations.newCheck(checkKey).upsert();
   }
 
+  @After
+  public void resetTime() {
+    TestTimeUtil.useSystemTime();
+  }
+
   @Test
   public void updateCheckState() throws Exception {
     CheckInput input = new CheckInput();
@@ -56,6 +72,93 @@
   }
 
   @Test
+  public void cannotUpdateCheckerUuid() throws Exception {
+    CheckInput input = new CheckInput();
+    input.checkerUuid = "foo:bar";
+
+    exception.expect(BadRequestException.class);
+    exception.expectMessage(
+        "checker UUID in input must either be null or the same as on the resource");
+    checksApiFactory.revision(patchSetId).id(checkKey.checkerUuid()).update(input);
+  }
+
+  @Test
+  public void specifyingCheckerUuidInInputThatMatchesTheCheckerUuidInTheUrlIsOkay()
+      throws Exception {
+    CheckInput input = new CheckInput();
+    input.checkerUuid = checkKey.checkerUuid().get();
+    checksApiFactory.revision(patchSetId).id(checkKey.checkerUuid()).update(input);
+  }
+
+  @Test
+  public void updateUrl() throws Exception {
+    CheckInput input = new CheckInput();
+    input.url = "http://example.com/my-check";
+
+    CheckInfo info = checksApiFactory.revision(patchSetId).id(checkKey.checkerUuid()).update(input);
+    assertThat(info.url).isEqualTo(input.url);
+  }
+
+  @Test
+  public void cannotSetInvalidUrl() throws Exception {
+    CheckInput input = new CheckInput();
+    input.url = CheckTestData.INVALID_URL;
+
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("only http/https URLs supported: " + input.url);
+    checksApiFactory.revision(patchSetId).id(checkKey.checkerUuid()).update(input);
+  }
+
+  @Test
+  public void updateStarted() throws Exception {
+    CheckInput input = new CheckInput();
+    input.started = TimeUtil.nowTs();
+
+    CheckInfo info = checksApiFactory.revision(patchSetId).id(checkKey.checkerUuid()).update(input);
+    assertThat(info.started).isEqualTo(input.started);
+  }
+
+  @Test
+  public void updateFinished() throws Exception {
+    CheckInput input = new CheckInput();
+    input.finished = TimeUtil.nowTs();
+
+    CheckInfo info = checksApiFactory.revision(patchSetId).id(checkKey.checkerUuid()).update(input);
+    assertThat(info.finished).isEqualTo(input.finished);
+  }
+
+  @Test
+  public void updateWithEmptyInput() throws Exception {
+    assertThat(
+            checksApiFactory
+                .revision(patchSetId)
+                .id(checkKey.checkerUuid())
+                .update(new CheckInput()))
+        .isNotNull();
+  }
+
+  @Test
+  public void updateResultsInNewUpdatedTimestamp() throws Exception {
+    CheckInput input = new CheckInput();
+    input.state = CheckState.FAILED;
+
+    Timestamp expectedUpdateTimestamp = TestTimeUtil.getCurrentTimestamp();
+    CheckInfo info = checksApiFactory.revision(patchSetId).id(checkKey.checkerUuid()).update(input);
+    assertThat(info.updated).isEqualTo(expectedUpdateTimestamp);
+  }
+
+  @Test
+  public void noOpUpdateDoesntResultInNewUpdatedTimestamp() throws Exception {
+    CheckInput input = new CheckInput();
+    input.state = CheckState.FAILED;
+    CheckInfo info = checksApiFactory.revision(patchSetId).id(checkKey.checkerUuid()).update(input);
+    Timestamp expectedUpdateTimestamp = info.updated;
+
+    info = checksApiFactory.revision(patchSetId).id(checkKey.checkerUuid()).update(input);
+    assertThat(info.updated).isEqualTo(expectedUpdateTimestamp);
+  }
+
+  @Test
   public void canUpdateCheckForDisabledChecker() throws Exception {
     checkerOperations.checker(checkKey.checkerUuid()).forUpdate().disable().update();
 
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/api/UpdateCheckerIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/api/UpdateCheckerIT.java
index c7ab750..da9025a 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/api/UpdateCheckerIT.java
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/api/UpdateCheckerIT.java
@@ -40,6 +40,8 @@
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.TestTimeUtil;
 import com.google.inject.Inject;
+import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Optional;
 import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.lib.Config;
@@ -62,6 +64,7 @@
   @Before
   public void setTimeForTesting() {
     TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS);
+    TestTimeUtil.setClock(Timestamp.from(Instant.EPOCH));
   }
 
   @After
@@ -89,15 +92,12 @@
     assertThat(info.description).isEqualTo(input.description);
     assertThat(info.url).isEqualTo(input.url);
     assertThat(info.repository).isEqualTo(input.repository);
-    assertThat(info.createdOn).isEqualTo(checker.getCreatedOn());
-    assertThat(info.createdOn).isLessThan(info.updatedOn);
+    assertThat(info.created).isEqualTo(checker.getCreated());
+    assertThat(info.created).isLessThan(info.updated);
 
     PerCheckerOperations perCheckerOps = checkerOperations.checker(checkerUuid);
     assertCommit(
-        perCheckerOps.commit(),
-        "Update checker",
-        info.updatedOn,
-        perCheckerOps.get().getRefState());
+        perCheckerOps.commit(), "Update checker", info.updated, perCheckerOps.get().getRefState());
     assertThat(checkerOperations.sha1sOfRepositoriesWithCheckers())
         .containsExactly(CheckersByRepositoryNotes.computeRepositorySha1(repositoryName));
     assertThat(checkerOperations.checkersOf(repositoryName))
@@ -141,10 +141,7 @@
 
     PerCheckerOperations perCheckerOps = checkerOperations.checker(checkerUuid);
     assertCommit(
-        perCheckerOps.commit(),
-        "Update checker",
-        info.updatedOn,
-        perCheckerOps.get().getRefState());
+        perCheckerOps.commit(), "Update checker", info.updated, perCheckerOps.get().getRefState());
   }
 
   @Test
@@ -185,10 +182,7 @@
 
     PerCheckerOperations perCheckerOps = checkerOperations.checker(checkerUuid);
     assertCommit(
-        perCheckerOps.commit(),
-        "Update checker",
-        info.updatedOn,
-        perCheckerOps.get().getRefState());
+        perCheckerOps.commit(), "Update checker", info.updated, perCheckerOps.get().getRefState());
   }
 
   @Test
@@ -203,10 +197,7 @@
 
     PerCheckerOperations perCheckerOps = checkerOperations.checker(checkerUuid);
     assertCommit(
-        perCheckerOps.commit(),
-        "Update checker",
-        info.updatedOn,
-        perCheckerOps.get().getRefState());
+        perCheckerOps.commit(), "Update checker", info.updated, perCheckerOps.get().getRefState());
   }
 
   @Test
@@ -221,10 +212,7 @@
 
     PerCheckerOperations perCheckerOps = checkerOperations.checker(checkerUuid);
     assertCommit(
-        perCheckerOps.commit(),
-        "Update checker",
-        info.updatedOn,
-        perCheckerOps.get().getRefState());
+        perCheckerOps.commit(), "Update checker", info.updated, perCheckerOps.get().getRefState());
   }
 
   @Test
@@ -239,10 +227,7 @@
 
     PerCheckerOperations perCheckerOps = checkerOperations.checker(checkerUuid);
     assertCommit(
-        perCheckerOps.commit(),
-        "Update checker",
-        info.updatedOn,
-        perCheckerOps.get().getRefState());
+        perCheckerOps.commit(), "Update checker", info.updated, perCheckerOps.get().getRefState());
   }
 
   @Test
@@ -257,10 +242,7 @@
 
     PerCheckerOperations perCheckerOps = checkerOperations.checker(checkerUuid);
     assertCommit(
-        perCheckerOps.commit(),
-        "Update checker",
-        info.updatedOn,
-        perCheckerOps.get().getRefState());
+        perCheckerOps.commit(), "Update checker", info.updated, perCheckerOps.get().getRefState());
   }
 
   @Test
@@ -275,10 +257,7 @@
 
     PerCheckerOperations perCheckerOps = checkerOperations.checker(checkerUuid);
     assertCommit(
-        perCheckerOps.commit(),
-        "Update checker",
-        info.updatedOn,
-        perCheckerOps.get().getRefState());
+        perCheckerOps.commit(), "Update checker", info.updated, perCheckerOps.get().getRefState());
   }
 
   @Test
@@ -294,10 +273,7 @@
 
     PerCheckerOperations perCheckerOps = checkerOperations.checker(checkerUuid);
     assertCommit(
-        perCheckerOps.commit(),
-        "Update checker",
-        info.updatedOn,
-        perCheckerOps.get().getRefState());
+        perCheckerOps.commit(), "Update checker", info.updated, perCheckerOps.get().getRefState());
   }
 
   @Test
@@ -313,10 +289,7 @@
 
     PerCheckerOperations perCheckerOps = checkerOperations.checker(checkerUuid);
     assertCommit(
-        perCheckerOps.commit(),
-        "Update checker",
-        info.updatedOn,
-        perCheckerOps.get().getRefState());
+        perCheckerOps.commit(), "Update checker", info.updated, perCheckerOps.get().getRefState());
   }
 
   @Test
@@ -331,10 +304,7 @@
 
     PerCheckerOperations perCheckerOps = checkerOperations.checker(checkerUuid);
     assertCommit(
-        perCheckerOps.commit(),
-        "Update checker",
-        info.updatedOn,
-        perCheckerOps.get().getRefState());
+        perCheckerOps.commit(), "Update checker", info.updated, perCheckerOps.get().getRefState());
   }
 
   @Test
@@ -351,10 +321,7 @@
 
     PerCheckerOperations perCheckerOps = checkerOperations.checker(checkerUuid);
     assertCommit(
-        perCheckerOps.commit(),
-        "Update checker",
-        info.updatedOn,
-        perCheckerOps.get().getRefState());
+        perCheckerOps.commit(), "Update checker", info.updated, perCheckerOps.get().getRefState());
     assertThat(checkerOperations.sha1sOfRepositoriesWithCheckers())
         .containsExactly(CheckersByRepositoryNotes.computeRepositorySha1(repositoryName));
     assertThat(checkerOperations.checkersOf(repositoryName))
@@ -473,7 +440,7 @@
     CheckerUuid checkerUuid = checkerOperations.newChecker().create();
 
     CheckerInput input = new CheckerInput();
-    input.blockingConditions = ImmutableSet.of(BlockingCondition.STATE_NOT_PASSING);
+    input.blocking = ImmutableSet.of(BlockingCondition.STATE_NOT_PASSING);
     CheckerInfo info = checkersApi.id(checkerUuid).update(input);
     assertThat(info.blocking).containsExactly(BlockingCondition.STATE_NOT_PASSING);
   }
@@ -549,6 +516,29 @@
   }
 
   @Test
+  public void updateResultsInNewUpdatedTimestamp() throws Exception {
+    CheckerUuid checkerUuid = checkerOperations.newChecker().create();
+
+    Timestamp expectedUpdateTimestamp = TestTimeUtil.getCurrentTimestamp();
+    CheckerInput input = new CheckerInput();
+    input.name = "My Checker";
+    CheckerInfo info = checkersApi.id(checkerUuid).update(input);
+    assertThat(info.updated).isEqualTo(expectedUpdateTimestamp);
+  }
+
+  @Test
+  public void noOpUpdateDoesntResultInNewUpdatedTimestamp() throws Exception {
+    CheckerUuid checkerUuid = checkerOperations.newChecker().name("My Checker").create();
+
+    Timestamp expectedUpdateTimestamp = checkerOperations.checker(checkerUuid).get().getUpdated();
+
+    CheckerInput input = new CheckerInput();
+    input.name = "My Checker";
+    CheckerInfo info = checkersApi.id(checkerUuid).update(input);
+    assertThat(info.updated).isEqualTo(expectedUpdateTimestamp);
+  }
+
+  @Test
   public void updateCheckerWithoutAdministrateCheckersCapabilityFails() throws Exception {
     CheckerUuid checkerUuid = checkerOperations.newChecker().create();
 
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckTestDataTest.java b/javatests/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckTestDataTest.java
new file mode 100644
index 0000000..e81c171
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckTestDataTest.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.acceptance.testsuite;
+
+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.plugins.checks.UrlValidator;
+import com.google.gerrit.plugins.checks.acceptance.AbstractCheckersTest;
+import org.junit.Test;
+
+public class CheckTestDataTest extends AbstractCheckersTest {
+  @Test
+  public void verifyTestUrls() throws Exception {
+    try {
+      UrlValidator.clean(CheckTestData.INVALID_URL);
+      assert_().fail("expected BadRequestException");
+    } catch (BadRequestException e) {
+      assertMessage(e, "only http/https URLs supported", CheckTestData.INVALID_URL);
+    }
+  }
+
+  private static void assertMessage(Exception e, String... expectedMessageParts) {
+    for (String expectedMessagePart : expectedMessageParts) {
+      assertThat(e).hasMessageThat().ignoringCase().contains(expectedMessagePart);
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckerOperationsImplTest.java b/javatests/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckerOperationsImplTest.java
index 86c5469..4060c82 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckerOperationsImplTest.java
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckerOperationsImplTest.java
@@ -76,8 +76,8 @@
     assertThat(foundChecker.blocking).isEmpty();
     assertThat(foundChecker.url).isNull();
     assertThat(foundChecker.description).isNull();
-    assertThat(foundChecker.createdOn).isNotNull();
-    assertThat(foundChecker.updatedOn).isNotNull();
+    assertThat(foundChecker.created).isNotNull();
+    assertThat(foundChecker.updated).isNotNull();
   }
 
   @Test
@@ -274,21 +274,21 @@
   }
 
   @Test
-  public void createdOnOfExistingCheckerCanBeRetrieved() throws Exception {
+  public void createdOfExistingCheckerCanBeRetrieved() throws Exception {
     CheckerInfo checker = checkersApi.create(createArbitraryCheckerInput()).get();
 
-    Timestamp createdOn = checkerOperations.checker(checker.uuid).get().getCreatedOn();
+    Timestamp created = checkerOperations.checker(checker.uuid).get().getCreated();
 
-    assertThat(createdOn).isEqualTo(checker.createdOn);
+    assertThat(created).isEqualTo(checker.created);
   }
 
   @Test
-  public void updatedOnOfExistingCheckerCanBeRetrieved() throws Exception {
+  public void updatedOfExistingCheckerCanBeRetrieved() throws Exception {
     CheckerInfo checker = checkersApi.create(createArbitraryCheckerInput()).get();
 
-    Timestamp updatedOn = checkerOperations.checker(checker.uuid).get().getUpdatedOn();
+    Timestamp updated = checkerOperations.checker(checker.uuid).get().getUpdated();
 
-    assertThat(updatedOn).isEqualTo(checker.updatedOn);
+    assertThat(updated).isEqualTo(checker.updated);
   }
 
   @Test
@@ -573,8 +573,8 @@
     assertThat(checkerInfo.name).isEqualTo(checker.getName().get());
     assertThat(checkerInfo.description).isEqualTo(checker.getDescription().get());
     assertThat(checkerInfo.url).isEqualTo(checker.getUrl().get());
-    assertThat(checkerInfo.createdOn).isEqualTo(checker.getCreatedOn());
-    assertThat(checkerInfo.updatedOn).isEqualTo(checker.getUpdatedOn());
+    assertThat(checkerInfo.created).isEqualTo(checker.getCreated());
+    assertThat(checkerInfo.updated).isEqualTo(checker.getUpdated());
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/plugins/checks/db/CheckerConfigTest.java b/javatests/com/google/gerrit/plugins/checks/db/CheckerConfigTest.java
index a473eef..05ba663 100644
--- a/javatests/com/google/gerrit/plugins/checks/db/CheckerConfigTest.java
+++ b/javatests/com/google/gerrit/plugins/checks/db/CheckerConfigTest.java
@@ -35,10 +35,6 @@
 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;
@@ -221,25 +217,13 @@
   }
 
   @Test
-  public void createdOnDefaultsToNow() throws Exception {
+  public void createdDefaultsToNow() 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);
+    assertThat(checkerConfig).hasCreatedThat().isAtLeast(testStart);
   }
 
   @Test
@@ -508,6 +492,24 @@
     assertThat(updatedChecker).hasRefStateThat().isEqualTo(expectedRefStateAfterUpdate);
   }
 
+  @Test
+  public void noNewCommitOnNoOpUpdate() throws Exception {
+    CheckerCreation checkerCreation =
+        getPrefilledCheckerCreationBuilder().setCheckerUuid(checkerUuid).build();
+    createChecker(checkerCreation);
+    ObjectId refState = getCheckerRefState(checkerUuid);
+
+    // Setting a description updates the ref.
+    CheckerUpdate checkerUpdate = CheckerUpdate.builder().setDescription("A description.").build();
+    updateChecker(checkerUuid, checkerUpdate);
+    ObjectId refState2 = getCheckerRefState(checkerUuid);
+    assertThat(refState2).isNotEqualTo(refState);
+
+    // Setting the same description again is a no-op and the ref is not updated.
+    updateChecker(checkerUuid, checkerUpdate);
+    assertThat(getCheckerRefState(checkerUuid)).isEqualTo(refState2);
+  }
+
   private CheckerConfig createArbitraryChecker(CheckerUuid checkerUuid) throws Exception {
     CheckerCreation checkerCreation =
         getPrefilledCheckerCreationBuilder().setCheckerUuid(checkerUuid).build();
@@ -585,8 +587,4 @@
       return assertThat(commit.getFullMessage()).named("commit message");
     }
   }
-
-  private static Timestamp toTimestamp(LocalDateTime localDateTime) {
-    return Timestamp.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
-  }
 }
diff --git a/javatests/com/google/gerrit/plugins/checks/db/CheckersByRepositoryNotesTest.java b/javatests/com/google/gerrit/plugins/checks/db/CheckersByRepositoryNotesTest.java
new file mode 100644
index 0000000..59eca92
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/checks/db/CheckersByRepositoryNotesTest.java
@@ -0,0 +1,334 @@
+// 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 com.google.common.truth.StringSubject;
+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.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.util.time.TimeUtil;
+import java.io.IOException;
+import java.util.TimeZone;
+import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+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 CheckersByRepositoryNotesTest {
+  private final TimeZone timeZone = TimeZone.getTimeZone("America/Los_Angeles");
+
+  private AllProjectsName projectName;
+  private Repository repository;
+
+  @Before
+  public void setUp() throws Exception {
+    projectName = new AllProjectsName("Test Repository");
+    repository = new InMemoryRepository(new DfsRepositoryDescription("Test Repository"));
+  }
+
+  @Test
+  public void getEmptyCheckersList() throws Exception {
+    CheckersByRepositoryNotes checkersByRepositoryNotes = loadCheckersByRepositoryNotes();
+    assertThat(checkersByRepositoryNotes.get(new Project.NameKey("some-project"))).isEmpty();
+  }
+
+  @Test
+  public void insertCheckers() throws Exception {
+    CheckersByRepositoryNotes checkersByRepositoryNotes = loadCheckersByRepositoryNotes();
+
+    Project.NameKey project1 = new Project.NameKey("some-project");
+    Project.NameKey project2 = new Project.NameKey("other-project");
+
+    CheckerUuid checkerUuid1 = CheckerUuid.parse("foo:bar");
+    checkersByRepositoryNotes.insert(checkerUuid1, project1);
+    commit(checkersByRepositoryNotes);
+    assertThat(checkersByRepositoryNotes.get(project1)).containsExactly(checkerUuid1);
+    assertThat(checkersByRepositoryNotes.get(project2)).isEmpty();
+
+    CheckerUuid checkerUuid2 = CheckerUuid.parse("foo:baz");
+    checkersByRepositoryNotes.insert(checkerUuid2, project1);
+    commit(checkersByRepositoryNotes);
+    assertThat(checkersByRepositoryNotes.get(project1))
+        .containsExactly(checkerUuid1, checkerUuid2)
+        .inOrder();
+    assertThat(checkersByRepositoryNotes.get(project2)).isEmpty();
+
+    CheckerUuid checkerUuid3 = CheckerUuid.parse("bar:baz");
+    checkersByRepositoryNotes.insert(checkerUuid3, project2);
+    commit(checkersByRepositoryNotes);
+    assertThat(checkersByRepositoryNotes.get(project1))
+        .containsExactly(checkerUuid1, checkerUuid2)
+        .inOrder();
+    assertThat(checkersByRepositoryNotes.get(project2)).containsExactly(checkerUuid3);
+  }
+
+  @Test
+  public void insertCheckerTwice() throws Exception {
+    CheckersByRepositoryNotes checkersByRepositoryNotes = loadCheckersByRepositoryNotes();
+
+    CheckerUuid checkerUuid = CheckerUuid.parse("foo:bar");
+    Project.NameKey project = new Project.NameKey("some-project");
+
+    checkersByRepositoryNotes.insert(checkerUuid, project);
+    commit(checkersByRepositoryNotes);
+    assertThat(checkersByRepositoryNotes.get(project)).containsExactly(checkerUuid);
+
+    ObjectId commitId = getRefsMetaCheckersState();
+    checkersByRepositoryNotes.insert(checkerUuid, project);
+    commit(checkersByRepositoryNotes);
+    assertThat(checkersByRepositoryNotes.get(project)).containsExactly(checkerUuid);
+    assertThat(getRefsMetaCheckersState()).isEqualTo(commitId);
+  }
+
+  @Test
+  public void removeCheckers() throws Exception {
+    CheckersByRepositoryNotes checkersByRepositoryNotes = loadCheckersByRepositoryNotes();
+
+    Project.NameKey project = new Project.NameKey("some-project");
+
+    CheckerUuid checkerUuid1 = CheckerUuid.parse("bar:baz");
+    CheckerUuid checkerUuid2 = CheckerUuid.parse("foo:bar");
+    CheckerUuid checkerUuid3 = CheckerUuid.parse("foo:baz");
+
+    checkersByRepositoryNotes.insert(checkerUuid1, project);
+    checkersByRepositoryNotes.insert(checkerUuid2, project);
+    checkersByRepositoryNotes.insert(checkerUuid3, project);
+    commit(checkersByRepositoryNotes);
+    assertThat(checkersByRepositoryNotes.get(project))
+        .containsExactly(checkerUuid1, checkerUuid2, checkerUuid3)
+        .inOrder();
+
+    checkersByRepositoryNotes.remove(checkerUuid2, project);
+    commit(checkersByRepositoryNotes);
+    assertThat(checkersByRepositoryNotes.get(project))
+        .containsExactly(checkerUuid1, checkerUuid3)
+        .inOrder();
+
+    checkersByRepositoryNotes.remove(checkerUuid1, project);
+    commit(checkersByRepositoryNotes);
+    assertThat(checkersByRepositoryNotes.get(project)).containsExactly(checkerUuid3).inOrder();
+
+    checkersByRepositoryNotes.remove(checkerUuid3, project);
+    commit(checkersByRepositoryNotes);
+    assertThat(checkersByRepositoryNotes.get(project)).isEmpty();
+  }
+
+  @Test
+  public void removeNonExistingChecker() throws Exception {
+    CheckersByRepositoryNotes checkersByRepositoryNotes = loadCheckersByRepositoryNotes();
+
+    Project.NameKey project1 = new Project.NameKey("some-project");
+    Project.NameKey project2 = new Project.NameKey("other-project");
+
+    CheckerUuid checkerUuid1 = CheckerUuid.parse("foo:bar");
+    CheckerUuid checkerUuid2 = CheckerUuid.parse("foo:baz");
+
+    checkersByRepositoryNotes.insert(checkerUuid1, project1);
+    commit(checkersByRepositoryNotes);
+    assertThat(checkersByRepositoryNotes.get(project1)).containsExactly(checkerUuid1);
+    assertThat(checkersByRepositoryNotes.get(project2)).isEmpty();
+
+    ObjectId commitId = getRefsMetaCheckersState();
+    checkersByRepositoryNotes.remove(checkerUuid2, project1);
+    checkersByRepositoryNotes.remove(checkerUuid1, project2);
+    commit(checkersByRepositoryNotes);
+    assertThat(checkersByRepositoryNotes.get(project1)).containsExactly(checkerUuid1);
+    assertThat(checkersByRepositoryNotes.get(project2)).isEmpty();
+    assertThat(getRefsMetaCheckersState()).isEqualTo(commitId);
+  }
+
+  @Test
+  public void updateCheckers() throws Exception {
+    CheckersByRepositoryNotes checkersByRepositoryNotes = loadCheckersByRepositoryNotes();
+
+    Project.NameKey project1 = new Project.NameKey("some-project");
+    Project.NameKey project2 = new Project.NameKey("other-project");
+
+    CheckerUuid checkerUuid1 = CheckerUuid.parse("foo:bar");
+    CheckerUuid checkerUuid2 = CheckerUuid.parse("foo:baz");
+
+    checkersByRepositoryNotes.insert(checkerUuid1, project1);
+    checkersByRepositoryNotes.insert(checkerUuid2, project1);
+    commit(checkersByRepositoryNotes);
+    assertThat(checkersByRepositoryNotes.get(project1))
+        .containsExactly(checkerUuid1, checkerUuid2)
+        .inOrder();
+    assertThat(checkersByRepositoryNotes.get(project2)).isEmpty();
+
+    checkersByRepositoryNotes.update(checkerUuid1, project1, project2);
+    commit(checkersByRepositoryNotes);
+    assertThat(checkersByRepositoryNotes.get(project1)).containsExactly(checkerUuid2);
+    assertThat(checkersByRepositoryNotes.get(project2)).containsExactly(checkerUuid1);
+
+    checkersByRepositoryNotes.update(checkerUuid2, project1, project2);
+    commit(checkersByRepositoryNotes);
+    assertThat(checkersByRepositoryNotes.get(project1)).isEmpty();
+    assertThat(checkersByRepositoryNotes.get(project2))
+        .containsExactly(checkerUuid1, checkerUuid2)
+        .inOrder();
+  }
+
+  @Test
+  public void sortOrder() throws Exception {
+    CheckersByRepositoryNotes checkersByRepositoryNotes = loadCheckersByRepositoryNotes();
+
+    Project.NameKey project = new Project.NameKey("some-project");
+
+    CheckerUuid checkerUuid1 = CheckerUuid.parse("foo:bar");
+    CheckerUuid checkerUuid2 = CheckerUuid.parse("foo:baz");
+    checkersByRepositoryNotes.insert(checkerUuid1, project);
+    checkersByRepositoryNotes.insert(checkerUuid2, project);
+    commit(checkersByRepositoryNotes);
+    assertThat(checkersByRepositoryNotes.get(project))
+        .containsExactly(checkerUuid1, checkerUuid2)
+        .inOrder();
+
+    CheckerUuid checkerUuid3 = CheckerUuid.parse("abc:xyz");
+    CheckerUuid checkerUuid4 = CheckerUuid.parse("xyz:abc");
+
+    checkersByRepositoryNotes.insert(checkerUuid3, project);
+    checkersByRepositoryNotes.insert(checkerUuid4, project);
+    commit(checkersByRepositoryNotes);
+    assertThat(checkersByRepositoryNotes.get(project))
+        .containsExactly(checkerUuid3, checkerUuid1, checkerUuid2, checkerUuid4)
+        .inOrder();
+  }
+
+  @Test
+  public void commitMessage() throws Exception {
+    CheckersByRepositoryNotes checkersByRepositoryNotes = loadCheckersByRepositoryNotes();
+
+    CheckerUuid checkerUuid = CheckerUuid.parse("foo:bar");
+    Project.NameKey project = new Project.NameKey("some-project");
+
+    checkersByRepositoryNotes.insert(checkerUuid, project);
+    commit(checkersByRepositoryNotes);
+    assertThatCommitMessage()
+        .isEqualTo(
+            "Update checkers by repository\n\nChecker: "
+                + checkerUuid.toString()
+                + "\nRepository: "
+                + project.get());
+  }
+
+  @Test
+  public void noOpUpdate() throws Exception {
+    CheckersByRepositoryNotes checkersByRepositoryNotes = loadCheckersByRepositoryNotes();
+
+    Project.NameKey project = new Project.NameKey("some-project");
+
+    CheckerUuid checkerUuid1 = CheckerUuid.parse("foo:bar");
+    checkersByRepositoryNotes.insert(checkerUuid1, project);
+    commit(checkersByRepositoryNotes);
+
+    ObjectId commitId = getRefsMetaCheckersState();
+
+    // Set the repository of the checker to the same value that it currently has.
+    checkersByRepositoryNotes.update(checkerUuid1, project, project);
+    commit(checkersByRepositoryNotes);
+    assertThat(getRefsMetaCheckersState()).isEqualTo(commitId);
+
+    // Insert a new checker and remove it before commit.
+    CheckerUuid checkerUuid2 = CheckerUuid.parse("foo:baz");
+    checkersByRepositoryNotes.insert(checkerUuid2, project);
+    checkersByRepositoryNotes.remove(checkerUuid2, project);
+    commit(checkersByRepositoryNotes);
+    assertThat(getRefsMetaCheckersState()).isEqualTo(commitId);
+  }
+
+  @Test
+  public void getCheckersFromOldRevision() throws Exception {
+    CheckersByRepositoryNotes checkersByRepositoryNotes = loadCheckersByRepositoryNotes();
+
+    Project.NameKey project = new Project.NameKey("some-project");
+
+    CheckerUuid checkerUuid1 = CheckerUuid.parse("foo:bar");
+    checkersByRepositoryNotes.insert(checkerUuid1, project);
+    commit(checkersByRepositoryNotes);
+    assertThat(checkersByRepositoryNotes.get(project)).containsExactly(checkerUuid1);
+
+    ObjectId oldRevision = getRefsMetaCheckersState();
+
+    CheckerUuid checkerUuid2 = CheckerUuid.parse("foo:baz");
+    checkersByRepositoryNotes.insert(checkerUuid2, project);
+    commit(checkersByRepositoryNotes);
+    assertThat(checkersByRepositoryNotes.get(project))
+        .containsExactly(checkerUuid1, checkerUuid2)
+        .inOrder();
+
+    assertThat(CheckersByRepositoryNotes.load(projectName, repository, oldRevision).get(project))
+        .containsExactly(checkerUuid1);
+  }
+
+  @Test
+  public void getCheckersFromZeroIdRevision() throws Exception {
+    assertThat(
+            CheckersByRepositoryNotes.load(projectName, repository, ObjectId.zeroId())
+                .get(new Project.NameKey("some-project")))
+        .isEmpty();
+  }
+
+  @Test
+  public void getCheckersFromNullRevision() throws Exception {
+    assertThat(
+            CheckersByRepositoryNotes.load(projectName, repository, null)
+                .get(new Project.NameKey("some-project")))
+        .isEmpty();
+  }
+
+  private CheckersByRepositoryNotes loadCheckersByRepositoryNotes() throws IOException {
+    return CheckersByRepositoryNotes.load(projectName, repository);
+  }
+
+  private void commit(CheckersByRepositoryNotes checkersByRepositoryNotes) throws IOException {
+    try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
+      checkersByRepositoryNotes.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 ObjectId getRefsMetaCheckersState() throws IOException {
+    return repository.exactRef(CheckerRef.REFS_META_CHECKERS).getObjectId();
+  }
+
+  private StringSubject assertThatCommitMessage() throws IOException {
+    try (RevWalk rw = new RevWalk(repository)) {
+      RevCommit commit = rw.parseCommit(getRefsMetaCheckersState());
+      return assertThat(commit.getFullMessage()).named("commit message");
+    }
+  }
+}
diff --git a/src/main/resources/Documentation/rest-api-checkers.md b/src/main/resources/Documentation/rest-api-checkers.md
index dfa4240..40e1cf3 100644
--- a/src/main/resources/Documentation/rest-api-checkers.md
+++ b/src/main/resources/Documentation/rest-api-checkers.md
@@ -39,7 +39,7 @@
     "repository": "examples/Foo",
     "blocking": [],
     "description": "A simple checker.",
-    "created_on": "2019-01-31 09:59:32.126000000"
+    "created": "2019-01-31 09:59:32.126000000"
   }
 ```
 
@@ -83,8 +83,8 @@
     "name": "MyChecker",
     "description": "A simple checker.",
     "repository": "examples/Foo",
-    "created_on": "2019-01-31 09:59:32.126000000",
-    "updated_on": "2019-01-31 09:59:32.126000000"
+    "created": "2019-01-31 09:59:32.126000000",
+    "updated": "2019-01-31 09:59:32.126000000"
   }
 ```
 
@@ -144,8 +144,8 @@
     "description": "A simple checker.",
     "repository": "examples/Foo",
     "status": "ENABLED",
-    "created_on": "2019-01-31 09:59:32.126000000",
-    "updated_on": "2019-02-01 07:23:44.158000000"
+    "created": "2019-01-31 09:59:32.126000000",
+    "updated": "2019-02-01 07:23:44.158000000"
   }
 ```
 
@@ -187,8 +187,8 @@
 | `status`        |          | The status of the checker; one of `ENABLED` or `DISABLED`.
 | `blocking`      |          | A list of [conditions](#blocking-conditions) that describe when the checker should block change submission.
 | `query`         | optional | A [query](#query) that limits changes for which the checker is relevant.
-| `created_on`    |          | The [timestamp](../../../Documentation/rest-api.html#timestamp) of when the checker was created.
-| `updated_on`    |          | The [timestamp](../../../Documentation/rest-api.html#timestamp) of when the checker was last updated.
+| `created`       |          | The [timestamp](../../../Documentation/rest-api.html#timestamp) of when the checker was created.
+| `updated`       |          | The [timestamp](../../../Documentation/rest-api.html#timestamp) of when the checker was last updated.
 
 ### <a id="checker-input"> CheckerInput
 The `CheckerInput` entity contains information for creating a checker.
@@ -233,8 +233,8 @@
 the query for each checker in sequence. Because of this algorithm, there are
 restrictions on which operators can be supported in checker queries:
 
-* Operators related to projects are not supported, since the algorithm already
-  only considers checkers with a matching repository.
+* Operators related to projects/repositories are not supported, since the
+  algorithm already only considers checkers with a matching repository.
 * Full-text operators (e.g. `message`) are not supported, since these cannot be
   efficiently evaluated sequentially.
 * Operators with an explicit or implied `self` user are not supported, since the
diff --git a/src/main/resources/Documentation/rest-api-checks.md b/src/main/resources/Documentation/rest-api-checks.md
index 3aeabef..c785e75 100644
--- a/src/main/resources/Documentation/rest-api-checks.md
+++ b/src/main/resources/Documentation/rest-api-checks.md
@@ -34,7 +34,7 @@
   )]}'
   [
     {
-      "project": "test-project",
+      "repository": "test-repo",
       "change_number": 1,
       "patch_set_id": 1,
       "checker_uuid": "test:my-checker",
@@ -44,7 +44,7 @@
       "updated": "2019-01-31 09:59:32.126000000"
     },
     {
-      "project": "test-project",
+      "repository": "test-repo",
       "change_number": 1,
       "patch_set_id": 1,
       "checker_uuid": "foo:foo-checker",
@@ -82,7 +82,7 @@
   Content-Type: application/json; charset=UTF-8
   )]}'
   {
-    "project": "test-project",
+    "repository": "test-repo",
     "change_number": 1,
     "patch_set_id": 1,
     "checker_uuid": "test:my-checker",
@@ -129,7 +129,7 @@
   Content-Type: application/json; charset=UTF-8
   )]}'
   {
-    "project": "test-project",
+    "repository": "test-repo",
     "change_number": 1,
     "patch_set_id": 1,
     "checker_uuid": "test:my-checker",
@@ -163,7 +163,7 @@
 
 | Field Name        |          | Description |
 | ----------------- | -------- | ----------- |
-| `project`         |          | The project name that this check applies to.
+| `repository`      |          | The repository name that this check applies to.
 | `change_number`   |          | The change number that this check applies to.
 | `patch_set_id`    |          | The patch set that this check applies to.
 | `checker_uuid`    |          | The [UUID](./rest-api-checkers.md#checker-id) of the checker that reported this check.
diff --git a/src/main/resources/Documentation/rest-api-pending-checks.md b/src/main/resources/Documentation/rest-api-pending-checks.md
index f972cdc..37d7bc5 100644
--- a/src/main/resources/Documentation/rest-api-pending-checks.md
+++ b/src/main/resources/Documentation/rest-api-pending-checks.md
@@ -54,7 +54,7 @@
   [
     {
       "patch_set": {
-        "project": "test-project",
+        "repository": "test-repo",
         "change_number": 1,
         "patch_set_id": 1,
       }
@@ -66,7 +66,7 @@
     },
     {
       "patch_set": {
-        "project": "test-project",
+        "repository": "test-repo",
         "change_number": 5,
         "patch_set_id": 2,
       }
@@ -87,7 +87,7 @@
 
 | Field Name      | Description |
 | --------------- | ----------- |
-| `project`       | The project name that this pending check applies to.
+| `repository`    | The repository name that this pending check applies to.
 | `change_number` | The change number that this pending check applies to.
 | `patch_set_id`  | The ID of the patch set that this pending check applies to.