Merge changes I92fa8ce1,I08822edb

* changes:
  Validate checker properties in CheckerCreation and CheckerUpdate
  Make name of checker mandatory
diff --git a/gr-checks/gr-checkers-list.html b/gr-checks/gr-checkers-list.html
new file mode 100644
index 0000000..36e8f85
--- /dev/null
+++ b/gr-checks/gr-checkers-list.html
@@ -0,0 +1,163 @@
+<link rel="import" href="gr-create-checkers-dialog.html">
+
+<!-- Expects core to import these functionalities
+     gr-list-view/gr-list-view.html
+     styles/gr-form-styles.html
+     /gr-icons/gr-icons.html
+     /iron-input/iron-input.html -->
+
+<dom-module id="gr-checkers-list">
+  <template>
+    <style include="shared-styles"></style>
+    <style include="gr-table-styles"></style>
+    <style>
+      iron-icon {
+        cursor: pointer;
+      }
+      #filter {
+        font-size: var(--font-size-normal);
+        max-width: 25em;
+      }
+      #filter:focus {
+        outline: none;
+      }
+      #topContainer {
+        align-items: center;
+        display: flex;
+        height: 3rem;
+        justify-content: space-between;
+        margin: 0 1em;
+      }
+      #createNewContainer:not(.show) {
+        display: none;
+      }
+      a {
+        color: var(--primary-text-color);
+        text-decoration: none;
+      }
+      nav {
+        align-items: center;
+        display: flex;
+        height: 3rem;
+        justify-content: flex-end;
+        margin-right: 20px;
+      }
+      nav,
+      iron-icon {
+        color: var(--deemphasized-text-color);
+      }
+      .nav-iron-icon {
+        height: 1.85rem;
+        margin-left: 16px;
+        width: 1.85rem;
+      }
+      .nav-buttons:hover {
+        text-decoration: underline;
+        cursor: pointer;
+      }
+    </style>
+
+
+    <div id="topContainer">
+      <div>
+        <label>Filter:</label>
+        <iron-input
+            type="text"
+            bind-value="{{_filter}}">
+          <input
+              is="iron-input"
+              type="text"
+              id="filter"
+              bind-value="{{_filter}}">
+        </iron-input>
+      </div>
+      <div id="createNewContainer"
+          class$="[[_computeCreateClass(_createNewCapability)]]">
+        <gr-button primary link id="createNew" on-tap="_handleCreateClicked">
+          Create New
+        </gr-button>
+      </div>
+    </div>
+
+    <table id="list" class="genericList">
+      <tr class="headerRow">
+        <th class="name topHeader">Checker Name</th>
+        <th class="name topHeader">Repository</th>
+        <th class="name topHeader">Status</th>
+        <th class="name topHeader">Required</th>
+        <th class="topHeader description">Checker Description</th>
+        <th class="name topHeader"> Edit </th>
+      </tr>
+      <tbody class$="[[computeLoadingClass(_loading)]]">
+        <template is="dom-repeat" items="[[_visibleCheckers]]">
+          <tr class="table">
+            <td class="name">
+              <a>[[item.name]]</a>
+            </td>
+            <td class="name">[[item.repository]]</td>
+            <td class="name">[[item.status]]</td>
+            <td class="name">[[_computeBlocking(item)]]</td>
+            <td class="description">[[item.description]]</td>
+            <td on-tap="_handleEditIconClicked">
+              <iron-icon icon="gr-icons:edit"></iron-icon>
+            </td>
+          </tr>
+        </template>
+      </tbody>
+    </table>
+
+    <nav>
+      <template is="dom-if" if="[[_showPrevButton]]">
+        <a class="nav-buttons" id="prevArrow"
+          on-tap="_handlePrevClicked">
+          <iron-icon class="nav-iron-icon" icon="gr-icons:chevron-left"></iron-icon>
+        </a>
+      </template>
+      <template is="dom-if" if="[[_showNextButton]]">
+        <a class="nav-buttons" id="nextArrow"
+          on-tap="_handleNextClicked">
+          <iron-icon icon="gr-icons:chevron-right"></iron-icon>
+        </a>
+      </template>
+    </nav>
+
+    <gr-overlay id="createOverlay" with-backdrop>
+      <gr-dialog
+          id="createDialog"
+          confirm-label="Create"
+          on-confirm="_handleCreateConfirm"
+          on-cancel="_handleCreateCancel">
+        <div class="header" slot="header">
+          Create Checkers
+        </div>
+        <div slot="main">
+          <gr-create-checkers-dialog
+            id="createNewModal"
+            plugin-rest-api="[[pluginRestApi]]">
+          </gr-create-checkers-dialog>
+        </div>
+      </gr-dialog>
+    </gr-overlay>
+    <gr-overlay id="editOverlay" with-backdrop>
+      <gr-dialog
+          id="editDialog"
+          confirm-label="Save"
+          on-confirm="_handleEditConfirm"
+          on-cancel="_handleEditCancel">
+        <div class="header" slot="header">
+          Edit Checker
+        </div>
+        <div slot="main">
+          <gr-create-checkers-dialog
+              checker="[[checker]]"
+              plugin-rest-api="[[pluginRestApi]]"
+              on-cancel="_handleEditCancel"
+              id="editModal">
+          </gr-create-checkers-dialog>
+        </div>
+      </gr-dialog>
+    </gr-overlay>
+      <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+  </template>
+  <script src="gr-checkers-list.js"></script>
+</dom-module>
diff --git a/gr-checks/gr-checkers-list.js b/gr-checks/gr-checkers-list.js
new file mode 100644
index 0000000..0967c03
--- /dev/null
+++ b/gr-checks/gr-checkers-list.js
@@ -0,0 +1,174 @@
+(function() {
+  'use strict';
+  const CHECKERS_PER_PAGE = 15;
+  const GET_CHECKERS_URL = "/a/plugins/checks/checkers/";
+
+  /**
+   * Show a list of all checkers along with creating/editing them
+   */
+  Polymer({
+    is: 'gr-checkers-list',
+    properties: {
+      pluginRestApi: Object,
+      // Checker that will be passed to the editOverlay modal
+      checker: Object,
+      _checkers: Array,
+      // List of checkers that contain the filtered text
+      _filteredCheckers: Array,
+      _loading: {
+        type: Boolean,
+        value: true,
+      },
+      _filter: {
+        type: String,
+        value: '',
+      },
+      _visibleCheckers: {
+        type: Array,
+        computed: '_computeVisibleCheckers(_startingIndex, _filteredCheckers)',
+        observer: '_visibleCheckersChanged'
+      },
+      _createNewCapability: {
+        type: Boolean,
+        value: true,
+      },
+      _startingIndex: {
+        type: Number,
+        value: 0
+      },
+      _showNextButton: {
+        type: Boolean,
+        value: true,
+        computed: '_computeShowNextButton(_startingIndex, _filteredCheckers)'
+      },
+      _showPrevButton: {
+        type: Boolean,
+        value: true,
+        computed: '_computeShowPrevButton(_startingIndex, _filteredCheckers)'
+      },
+    },
+    observers: [
+      '_showCheckers(_checkers, _filter)',
+    ],
+
+    attached() {
+      this._getCheckers();
+    },
+
+    _contains(target, keyword) {
+      return target.toLowerCase().includes(keyword.toLowerCase().trim());
+    },
+
+    _visibleCheckersChanged(currentVisibleCheckers, previousVisibleCheckers) {
+      if (!currentVisibleCheckers || !previousVisibleCheckers) {
+        return;
+      }
+      if (currentVisibleCheckers.length !== previousVisibleCheckers.length) {
+        this.fire('resize', {bubbles: false});
+      }
+    },
+
+    _showCheckers(_checkers, _filter) {
+      if (!_checkers) return;
+      if (!_filter) _filter = '';
+      // TODO(dhruvsri): highlight matching part
+      this._filteredCheckers = this._checkers.filter(checker =>
+        this._contains(checker.name, this._filter) ||
+        this._contains(checker.repository, this._filter))
+      this._startingIndex = 0;
+    },
+
+    computeLoadingClass(loading) {
+      return loading ? 'loading' : '';
+    },
+
+    _computeVisibleCheckers(_startingIndex, _filteredCheckers) {
+      if (!_filteredCheckers) {
+        return [];
+      }
+      return this._filteredCheckers.slice(this._startingIndex,
+        this._startingIndex + CHECKERS_PER_PAGE);
+    },
+
+    _computeShowNextButton(_startingIndex, _filteredCheckers) {
+      if (!_filteredCheckers) {
+        return false;
+      }
+      return _startingIndex + CHECKERS_PER_PAGE < _filteredCheckers.length;
+    },
+
+    _computeShowPrevButton(_startingIndex, _filteredCheckers) {
+      if (!_filteredCheckers) {
+        return false;
+      }
+      return _startingIndex >= CHECKERS_PER_PAGE;
+    },
+
+    _handleNextClicked() {
+      if (this._startingIndex + CHECKERS_PER_PAGE <
+        this._filteredCheckers.length) {
+          this._startingIndex += CHECKERS_PER_PAGE;
+      }
+    },
+
+    _handlePrevClicked() {
+      if (this._startingIndex >= CHECKERS_PER_PAGE) {
+        this._startingIndex -= CHECKERS_PER_PAGE;
+      }
+    },
+
+    _getCheckers() {
+      this.pluginRestApi.fetchJSON({
+        method: 'GET',
+        url: GET_CHECKERS_URL,
+      }).then(checkers => {
+        if (!checkers) { return; }
+        this._checkers = checkers;
+        this._startingIndex = 0;
+        this._loading = false;
+      });
+    },
+
+    _handleEditConfirm() {
+      this.$.editModal.handleEditChecker();
+    },
+
+    _handleEditIconClicked(e) {
+      let checker = e.model.item;
+      this.checker = checker;
+      this.$.editOverlay.open();
+    },
+
+    _handleEditCancel(e) {
+      if (e.detail.reload) {
+        this._getCheckers();
+      }
+      this.$.editOverlay.close();
+    },
+
+    _computeCreateClass(createNew) {
+      return createNew ? 'show' : '';
+    },
+
+    _computeBlocking(checker) {
+      return (checker && checker.blocking && checker.blocking.length > 0)
+        ? "YES": "NO";
+    },
+
+    _handleCreateConfirm() {
+      this.$.createNewModal.handleCreateChecker();
+    },
+
+    _handleCreateClicked() {
+      this.$.createOverlay.open();
+    },
+
+    _handleCreateCancel(e) {
+      if (e.detail.reload) {
+        this._getCheckers();
+      }
+      this.$.createOverlay.close();
+    },
+
+  })
+})();
\ No newline at end of file
diff --git a/gr-checks/gr-checks-all-statuses.js b/gr-checks/gr-checks-all-statuses.js
index 17a7bee..c4be5bc 100644
--- a/gr-checks/gr-checks-all-statuses.js
+++ b/gr-checks/gr-checks-all-statuses.js
@@ -17,6 +17,8 @@
     SUCCESSFUL: 'SUCCESSFUL',
     FAILED: 'FAILED',
     NOT_RELEVANT: 'NOT_RELEVANT',
+
+    STATUS_UNKNOWN: 'UNKNOWN'
   };
 
   function isStatus(status, includedStatuses) {
@@ -27,6 +29,14 @@
     return isStatus(status, [Statuses.NOT_STARTED, Statuses.NOT_RELEVANT]);
   }
 
+  function isScheduled(status) {
+    return isStatus(status, [Statuses.SCHEDULED]);
+  }
+
+  function isRunning(status) {
+    return isStatus(status, [Statuses.RUNNING]);
+  }
+
   function isInProgress(status) {
     return isStatus(status, [Statuses.SCHEDULED, Statuses.RUNNING]);
   }
@@ -57,6 +67,8 @@
 
   window.Gerrit.Checks.Statuses = Statuses;
   window.Gerrit.Checks.isUnevaluated = isUnevaluated;
+  window.Gerrit.Checks.isScheduled = isScheduled;
+  window.Gerrit.Checks.isRunning = isRunning;
   window.Gerrit.Checks.isInProgress = isInProgress;
   window.Gerrit.Checks.isSuccessful = isSuccessful;
   window.Gerrit.Checks.isFailed = isFailed;
diff --git a/gr-checks/gr-checks-chip-view.js b/gr-checks/gr-checks-chip-view.js
index cd07acb..a2933ef 100644
--- a/gr-checks/gr-checks-chip-view.js
+++ b/gr-checks/gr-checks-chip-view.js
@@ -53,13 +53,14 @@
         return check.state == Statuses.FAILED;
       }
     )
-    if (!hasFailedCheck) return false;
+    if (!hasFailedCheck) return '';
     const hasRequiredFailedCheck = checks.some(
       (check) => {
-        return check.state == Statuses.FAILED && check.blocking && check.blocking.length > 0;
+        return check.state == Statuses.FAILED &&
+               check.blocking && check.blocking.length > 0;
       }
     )
-    return !hasRequiredFailedCheck;
+    return hasRequiredFailedCheck ? '' : 'set';
   }
 
 
@@ -74,15 +75,18 @@
       getChecks: Function,
       _checkStatuses: Object,
       _hasChecks: Boolean,
+      _failedRequiredChecksCount: Number,
       _status: {type: String, computed: '_computeStatus(_checkStatuses)'},
       _statusString: {
         type: String,
-        computed: '_computeStatusString(_status, _checkStatuses)',
+        computed: '_computeStatusString(_status, _checkStatuses, _failedRequiredChecksCount)',
       },
       _chipClasses: {type: String, computed: '_computeChipClass(_status)'},
+      // Type is set as string so that it reflects on changes
+      // Polymer does not support reflecting changes in Boolean property
       _downgradeFailureToWarning: {
-        type: Boolean,
-        value: false
+        type: String,
+        value: ''
       },
       pollChecksInterval: Object,
       visibilityChangeListenerAdded: {
@@ -127,8 +131,11 @@
       getChecks(change._number, revision._number).then(checks => {
         this.set('_hasChecks', checks.length > 0);
         if (checks.length > 0) {
-          this.set('_checkStatuses', computeCheckStatuses(checks));
-          this.set('_downgradeFailureToWarning', downgradeFailureToWarning(checks));
+          this._downgradeFailureToWarning =
+            downgradeFailureToWarning(checks);
+          this._failedRequiredChecksCount =
+            this.computeFailedRequiredChecksCount(checks);
+          this._checkStatuses = computeCheckStatuses(checks);
         }
       });
     },
@@ -164,15 +171,30 @@
           Statuses.STATUS_UNKNOWN;
     },
 
+    computeFailedRequiredChecksCount(checks) {
+      const failedRequiredChecks = checks.filter(
+        check => {
+          return check.state == Statuses.FAILED &&
+            check.blocking && check.blocking.length > 0;
+        }
+      );
+      return failedRequiredChecks.length;
+    },
+
     /**
      * @param {string} status The overall status of the checks.
      * @param {!Object} checkStatuses The number of checks in each status.
      * @return {string}
      */
-    _computeStatusString(status, checkStatuses) {
+    _computeStatusString(status, checkStatuses, failedRequiredChecksCount) {
+      if (!checkStatuses) return;
       if (checkStatuses.total === 0) return 'No checks';
-      return `${checkStatuses[status]} of ${
+      let statusString = `${checkStatuses[status]} of ${
           checkStatuses.total} checks ${HumanizedStatuses[status]}`;
+      if (status === Statuses.FAILED && failedRequiredChecksCount > 0) {
+        statusString += ` (${failedRequiredChecksCount} required)`;
+      }
+      return statusString;
     },
 
     /**
diff --git a/gr-checks/gr-checks-item.html b/gr-checks/gr-checks-item.html
index 64a061b..602cffb 100644
--- a/gr-checks/gr-checks-item.html
+++ b/gr-checks/gr-checks-item.html
@@ -18,8 +18,17 @@
         margin-right: 16px;
         display: inline-block;
       }
+      .nav-icon {
+        cursor: pointer;
+      }
     </style>
-
+    <td>
+      <template is="dom-if" if="[[check.message]]">
+          <iron-icon class="nav-icon" on-tap="_toggleMessageShown"
+            icon="[[_computeExpandIcon(showCheckMessage)]]">
+          </iron-icon>
+      </template>
+    </td>
     <td>[[check.checker_name]]</td>
     <td>[[_requiredForMerge]]</td>
     <td>
@@ -42,6 +51,7 @@
 <!--          Re-run-->
 <!--        </gr-button>-->
       </td>
+      <td>[[check.checker_description]]</td>
   </template>
   <script src="gr-checks-item.js"></script>
 </dom-module>
diff --git a/gr-checks/gr-checks-item.js b/gr-checks/gr-checks-item.js
index 199fc92..2c0d98b 100644
--- a/gr-checks/gr-checks-item.js
+++ b/gr-checks/gr-checks-item.js
@@ -41,6 +41,10 @@
       _requiredForMerge: {
         type: String,
         computed: '_computeRequiredForMerge(check)'
+      },
+      showCheckMessage: {
+        type: Boolean,
+        value: false,
       }
     },
 
@@ -53,6 +57,15 @@
       return moment(check.started).format('LTS');
     },
 
+    _toggleMessageShown() {
+      this.showCheckMessage = !this.showCheckMessage;
+      this.fire('toggle-check-message', {uuid: this.check.checker_uuid})
+    },
+
+    _computeExpandIcon(showCheckMessage) {
+      return showCheckMessage ? "gr-icons:expand-less": "gr-icons:expand-more";
+    },
+
     /**
      * @param {!Defs.Check} check
      * @return {string}
diff --git a/gr-checks/gr-checks-status.html b/gr-checks/gr-checks-status.html
index 591d228..0341a3f 100644
--- a/gr-checks/gr-checks-status.html
+++ b/gr-checks/gr-checks-status.html
@@ -33,7 +33,7 @@
           </span>
         </template>
       </template>
-      <template is="dom-if" if="[[_isInProgress(status)]]">
+      <template is="dom-if" if="[[_isScheduled(status)]]">
         <svg width="18" height="18" xmlns="http://www.w3.org/2000/svg">
           <g fill="none" fill-rule="evenodd">
             <path d="M12.184 9.293c-.86 0-1.641-.39-2.15-.976L9.8 9.489l.82.78V13.2h-.78v-2.344l-.821-.781-.39 1.719-2.736-.547.157-.782 1.914.391.625-3.164-.703.273v1.328h-.782V7.457l2.032-.86c.117 0 .196-.039.313-.039.273 0 .508.157.664.391l.39.625c.313.547.939.938 1.68.938v.781zM10.034 4.8c.43 0 .782.352.782.782 0 .43-.352.781-.781.781a.783.783 0 0 1-.782-.781c0-.43.352-.782.782-.782zM9 2a7 7 0 1 0 .002 14.002A7 7 0 0 0 9 2z" fill="#9C27B0"/>
@@ -42,7 +42,20 @@
         </svg>
         <template is="dom-if" if="[[showText]]">
           <span>
-            In progress
+            Scheduled
+          </span>
+        </template>
+      </template>
+      <template is="dom-if" if="[[_isRunning(status)]]">
+        <svg width="18" height="18" xmlns="http://www.w3.org/2000/svg">
+          <g fill="none" fill-rule="evenodd">
+            <path d="M12.184 9.293c-.86 0-1.641-.39-2.15-.976L9.8 9.489l.82.78V13.2h-.78v-2.344l-.821-.781-.39 1.719-2.736-.547.157-.782 1.914.391.625-3.164-.703.273v1.328h-.782V7.457l2.032-.86c.117 0 .196-.039.313-.039.273 0 .508.157.664.391l.39.625c.313.547.939.938 1.68.938v.781zM10.034 4.8c.43 0 .782.352.782.782 0 .43-.352.781-.781.781a.783.783 0 0 1-.782-.781c0-.43.352-.782.782-.782zM9 2a7 7 0 1 0 .002 14.002A7 7 0 0 0 9 2z" fill="#9C27B0"/>
+            <path d="M0 0h18v18H0z"/>
+          </g>
+        </svg>
+        <template is="dom-if" if="[[showText]]">
+          <span>
+            Running
           </span>
         </template>
       </template>
diff --git a/gr-checks/gr-checks-status.js b/gr-checks/gr-checks-status.js
index 0f18339..67ba4e4 100644
--- a/gr-checks/gr-checks-status.js
+++ b/gr-checks/gr-checks-status.js
@@ -12,7 +12,7 @@
         reflectToAttribute: true,
       },
       status: String,
-      downgradeFailureToWarning: Boolean
+      downgradeFailureToWarning: String
     },
 
     _isUnevaluated(status) {
@@ -23,6 +23,14 @@
       return window.Gerrit.Checks.isInProgress(status);
     },
 
+    _isRunning(status) {
+      return window.Gerrit.Checks.isRunning(status);
+    },
+
+    _isScheduled(status) {
+      return window.Gerrit.Checks.isScheduled(status);
+    },
+
     _isSuccessful(status) {
       return window.Gerrit.Checks.isSuccessful(status);
     },
diff --git a/gr-checks/gr-checks-view.html b/gr-checks/gr-checks-view.html
index 67129ee..ee22cbb 100644
--- a/gr-checks/gr-checks-view.html
+++ b/gr-checks/gr-checks-view.html
@@ -1,4 +1,6 @@
 <link rel="import" href="gr-checks-status.html">
+<link rel="import" href="gr-checkers-list.html">
+
 <dom-module id="gr-checks-view">
   <template>
     <style>
@@ -61,8 +63,43 @@
         padding: 24px 0;
         text-align: center;
       }
+
+      /* Add max-width 1px to check-message make sure content doesn't
+         expand beyond the table width */
+      .check-message {
+        padding-left: 1rem;
+        background: var(--table-subheader-background-color);
+        max-width: 1px;
+      }
+
+      .message {
+        white-space: pre-wrap;
+        word-wrap: break-word;
+      }
+
+      .message-heading {
+        font-weight: bold;
+        margin-top: 1em;
+      }
+
+      .check-message-heading {
+        padding-left: 1em;
+      }
+
+      .configure-button {
+        float:right;
+        margin-top: 10px;
+      }
+
+      #listOverlay {
+        width: 75%;
+      }
     </style>
 
+    <template is="dom-if" if="[[_createCheckerCapability]]">
+      <gr-button class="configure-button" on-tap="_handleConfigureClicked"> Configure </gr-button>
+    </template>
+
     <template is="dom-if" if="[[_isLoading(_status)]]">
       <div class="no-content">
         <p>Loading...</p>
@@ -91,6 +128,7 @@
       <table>
         <thead>
           <tr class="headerRow">
+            <th class="topHeader"></th>
             <th class="topHeader">Name</th>
             <th class="topHeader">For submit</th>
             <th class="topHeader">Status</th>
@@ -98,16 +136,39 @@
             <th class="topHeader">Started</th>
             <th class="topHeader">Duration</th>
             <th class="topHeader"><!-- actions --></th>
+            <th class="topHeader">Description</th>
           </tr>
         </thead>
         <tbody>
           <template is="dom-repeat" items="[[_checks]]" as="check">
-            <gr-checks-item check="[[check]]" retry-check="[[retryCheck]]"></gr-checks-item>
+            <tr>
+              <gr-checks-item on-toggle-check-message="_toggleCheckMessage"
+                check="[[check]]" retry-check="[[retryCheck]]">
+              </gr-checks-item>
+            </tr>
+            <template is="dom-if" if="[[check.showCheckMessage]]">
+              <tr>
+                <td colspan="9" class="check-message-heading">
+                  <span class="message-heading">
+                    Message
+                  </span>
+                </td>
+              </tr>
+              <tr>
+                <td colspan="9" class="check-message">
+                  <span class="message"> [[check.message]] </span>
+                </td>
+              </tr>
+            </template>
           </template>
         </tbody>
       </table>
     </template>
-  </template>
 
+    <gr-overlay id="listOverlay" with-backdrop>
+      <gr-checkers-list on-resize="_handleCheckersListResize" plugin-rest-api="[[pluginRestApi]]"></gr-checkers-list>
+    </gr-overlay>
+
+  </template>
   <script src="gr-checks-view.js"></script>
 </dom-module>
diff --git a/gr-checks/gr-checks-view.js b/gr-checks/gr-checks-view.js
index f5d083f..c312163 100644
--- a/gr-checks/gr-checks-view.js
+++ b/gr-checks/gr-checks-view.js
@@ -48,27 +48,60 @@
       isConfigured: Function,
       /** @type {function(string, string): !Promise<!Object>} */
       retryCheck: Function,
+      pluginRestApi: Object,
       _checks: Object,
       _status: {
         type: Object,
         value: LoadingStatus.LOADING,
       },
-      pollChecksInterval: Object,
+      pollChecksInterval: Number,
       visibilityChangeListenerAdded: {
         type: Boolean,
         value: false
-      }
+      },
+      _createCheckerCapability: {
+        type: Boolean,
+        value: false
+      },
     },
 
     observers: [
       '_pollChecksRegularly(change, revision, getChecks)',
     ],
 
+    attached() {
+      this.pluginRestApi = this.plugin.restApi();
+      this._initCreateCheckerCapability();
+    },
+
     detached() {
       clearInterval(this.pollChecksInterval);
       this.unlisten(document, 'visibilitychange', '_onVisibililityChange');
     },
 
+    _handleCheckersListResize() {
+      // Force polymer to recalculate position of overlay when length of
+      // checkers changes
+      this.$.listOverlay.refit();
+    },
+
+    _initCreateCheckerCapability() {
+      return this.pluginRestApi.getAccount().then(account => {
+        if (!account) { return; }
+        return this.pluginRestApi
+          .getAccountCapabilities(['checks-administrateCheckers'])
+          .then(capabilities => {
+            if (capabilities['checks-administrateCheckers']) {
+              this._createCheckerCapability = true;
+            }
+          });
+      });
+    },
+
+    _handleConfigureClicked() {
+      this.$.listOverlay.open();
+    },
+
     _orderChecks(a, b) {
       if (a.state != b.state) {
         let indexA = StatusPriorityOrder.indexOf(a.state);
@@ -95,7 +128,20 @@
       getChecks(change._number, revision._number).then(checks => {
         if (checks && checks.length) {
           checks.sort(this._orderChecks);
-          this.set('_checks', checks);
+          if (!this._checks) {
+            this._checks = checks;
+          } else {
+            // Merge checks & this_checks to keep showCheckMessage property
+            this._checks = checks.map(
+              (check) => {
+                const prevCheck = this._checks.find(
+                  (c) => { return c.checker_uuid === check.checker_uuid }
+                )
+                if (!prevCheck) return check;
+                return Object.assign({}, check, prevCheck)
+              }
+            )
+          }
           this.set('_status', LoadingStatus.RESULTS);
         } else {
           this._checkConfigured();
@@ -111,6 +157,22 @@
       this._pollChecksRegularly(this.change, this.revision, this.getChecks);
     },
 
+    _toggleCheckMessage(e) {
+      const uuid = e.detail.uuid;
+      if (!uuid) {
+        console.warn("uuid not found");
+        return;
+      }
+      const idx = this._checks.findIndex(check => check.checker_uuid === uuid);
+      if (idx == -1) {
+        console.warn("check not found");
+        return;
+      }
+      // Update subproperty of _checks[idx] so that it reflects to polymer
+      this.set(`_checks.${idx}.showCheckMessage`,
+        !this._checks[idx].showCheckMessage)
+    },
+
     _pollChecksRegularly(change, revision, getChecks) {
       if (this.pollChecksInterval) {
         clearInterval(this.pollChecksInterval);
diff --git a/gr-checks/gr-checks.html b/gr-checks/gr-checks.html
index cc4f6ca..2108121 100644
--- a/gr-checks/gr-checks.html
+++ b/gr-checks/gr-checks.html
@@ -12,7 +12,7 @@
 
       const getChecks = (change, revision) => {
         return plugin.restApi().get(
-            '/changes/' + change + '/revisions/' + revision + '/checks?o=CHECKER');
+          '/changes/' + change + '/revisions/' + revision + '/checks?o=CHECKER');
       };
 
       // TODO(brohlfs): Enable this dashboard column when search queries start
@@ -24,23 +24,25 @@
       //     'change-list-item-cell',
       //     'gr-checks-change-list-item-cell-view');
       plugin.registerCustomComponent(
-          'commit-container',
-          'gr-checks-chip-view').onAttached(
-          view => {
-            view['getChecks'] = getChecks;
-          });
+        'commit-container',
+        'gr-checks-chip-view').onAttached(
+        view => {
+          view['getChecks'] = getChecks;
+        }
+      );
       plugin.registerDynamicCustomComponent(
-          'change-view-tab-header',
-          'gr-checks-change-view-tab-header-view');
+        'change-view-tab-header',
+        'gr-checks-change-view-tab-header-view'
+      );
       plugin.registerDynamicCustomComponent(
-          'change-view-tab-content',
-          'gr-checks-view').onAttached(
-          view => {
-            view['isConfigured'] = (repository) => Promise.resolve(true);
-            // TODO(brohlfs): Implement retry.
-            view['retryCheck'] = (buildId) => undefined;
-            view['getChecks'] = getChecks;
-          });
-    });
+        'change-view-tab-content',
+        'gr-checks-view').onAttached(
+        view => {
+          view['isConfigured'] = (repository) => Promise.resolve(true);
+          // TODO(brohlfs): Implement retry.
+          view['retryCheck'] = (buildId) => undefined;
+          view['getChecks'] = getChecks;
+        });
+      });
   </script>
 </dom-module>
diff --git a/gr-checks/gr-create-checkers-dialog.html b/gr-checks/gr-create-checkers-dialog.html
new file mode 100644
index 0000000..e9aad47
--- /dev/null
+++ b/gr-checks/gr-create-checkers-dialog.html
@@ -0,0 +1,157 @@
+<link rel="import" href="gr-repo-chip.html">
+<dom-module id="gr-create-checkers-dialog">
+  <template>
+    <style include="gr-form-styles">
+      :host {
+        display: inline-block;
+      }
+      input {
+        width: 20em;
+      }
+      gr-autocomplete {
+        border: none;
+        --gr-autocomplete: {
+          border: 1px solid var(--border-color);
+          border-radius: 2px;
+          font-size: var(--font-size-normal);
+          height: 2em;
+          padding: 0 .15em;
+          width: 20em;
+        }
+      }
+      .error {
+        color: red;
+      }
+      #checkerSchemaInput[disabled] {
+        background-color: var(--table-subheader-background-color);
+      }
+      #checkerIdInput[disabled] {
+        background-color: var(--table-subheader-background-color);
+      }
+    </style>
+
+    <div class="gr-form-styles">
+      <div id="form">
+        <section hidden$="[[_errorMsg.length > 0]]">
+          <span class="error"> {{_errorMsg}} </span>
+        </section>
+        <section>
+          <span class="title">Name*</span>
+          <iron-input autocomplete="on"
+                      bind-value="{{_name}}">
+            <input is="iron-input"
+                   id="checkerNameInput"
+                   autocomplete="on"
+                   bind-value="{{_name}}">
+          </iron-input>
+        </section>
+        <section>
+          <span class="title">Description</span>
+          <iron-input autocomplete="on"
+                      bind-value="{{_description}}">
+            <input is="iron-input"
+                   id="checkerDescriptionInput"
+                   autocomplete="on"
+                   bind-value="{{_description}}">
+          </iron-input>
+        </section>
+        <section>
+          <span class="title">Repository*</span>
+          <div class="list">
+            <template id="chips" is="dom-repeat" items="[[_repos]]" as="repo">
+              <gr-repo-chip
+                  repo="[[repo]]"
+                  on-keydown="_handleChipKeydown"
+                  on-remove="_handleOnRemove"
+                  tabindex="-1">
+              </gr-repo-chip>
+            </template>
+          </div>
+          <div hidden$="[[_repositorySelected]]">
+            <gr-autocomplete
+              id="input"
+              threshold="[[suggestFrom]]"
+              query="[[_getRepoSuggestions]]"
+              on-commit="_handleRepositorySelected"
+              clear-on-commit
+              warn-uncommitted
+              text="{{_inputText}}">
+           </gr-autocomplete>
+          </div>
+        </section>
+
+        <section>
+          <span class="title">Scheme*</span>
+          <iron-input autocomplete="on"
+                      bind-value="{{_scheme}}">
+            <input is="iron-input"
+                   id="checkerSchemaInput"
+                   disabled$="[[_edit]]"
+                   autocomplete="on"
+                   bind-value="{{_scheme}}">
+          </iron-input>
+        </section>
+
+        <section>
+          <span class="title">ID*</span>
+          <iron-input autocomplete="on"
+                      bind-value="{{_id}}">
+            <input is="iron-input"
+                   id="checkerIdInput"
+                   disabled$="[[_edit]]"
+                   autocomplete="on"
+                   bind-value="{{_id}}">
+          </iron-input>
+        </section>
+
+        <section>
+          <span class="title">Url</span>
+          <iron-input autocomplete="on"
+                      bind-value="{{_url}}">
+            <input is="iron-input"
+                    id="checkerUrlInput"
+                    autocomplete="on"
+                    bind-value="{{_url}}">
+          </iron-input>
+        </section>
+
+        <section>
+          <span class="title"> UUID {{_uuid}}</span>
+        </section>
+
+        <section>
+          <span class="title">Status</span>
+          <gr-dropdown-list
+            items="[[_statuses]]"
+            on-value-change="_handleStatusChange"
+            text="Status"
+            value="[[_status]]">
+          </gr-dropdown-list>
+        </section>
+
+        <section>
+          <span class="title">Required</span>
+          <input
+          on-click = "_handleRequiredCheckBoxClicked"
+          type="checkbox"
+          id="privateChangeCheckBox"
+          checked$="[[_required]]">
+        </section>
+
+        <section>
+          <span class="title">Query</span>
+          <iron-input autocomplete="on"
+                      bind-value="{{_query}}">
+            <input is="iron-input"
+                    id="checkerQueryInput"
+                    autocomplete="on"
+                    bind-value="{{_query}}">
+          </iron-input>
+        </section>
+
+      </div>
+    </div>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+  </template>
+  <script src="gr-create-checkers-dialog.js"></script>
+</dom-module>
diff --git a/gr-checks/gr-create-checkers-dialog.js b/gr-checks/gr-create-checkers-dialog.js
new file mode 100644
index 0000000..cdb4127
--- /dev/null
+++ b/gr-checks/gr-create-checkers-dialog.js
@@ -0,0 +1,258 @@
+(function() {
+  'use strict';
+
+  const REPOS_PER_PAGE = 6;
+  const CREATE_CHECKER_URL = '/plugins/checks/checkers/';
+  const SCHEME_PATTERN = /^[\w-_.]*$/;
+
+  Polymer({
+    is: 'gr-create-checkers-dialog',
+    _legacyUndefinedCheck: true,
+
+    properties: {
+      checker: {
+        type: Object,
+        observer: '_checkerChanged'
+      },
+      _name: String,
+      _scheme: String,
+      _id: String,
+      _uuid: {
+        type: String,
+        value: ""
+      },
+      pluginRestApi: Object,
+      _url: String,
+      _description: String,
+      _getRepoSuggestions: {
+        type: Function,
+        value() {
+          return this._repoSuggestions.bind(this)
+        }
+      },
+      // The backend might support multiple repos in the future
+      // which is why I decided to keep it as an array.
+      _repos: {
+        type: Array,
+        value: [],
+        notify: true,
+      },
+      _repositorySelected: {
+        type: Boolean,
+        value: false
+      },
+      _handleOnRemove: Function,
+      _errorMsg: {
+        type: String,
+        value: ''
+      },
+      _statuses: {
+        type: Array,
+        value: [
+          {
+            "text": "ENABLED",
+            "value": "ENABLED"
+          },
+          {
+            "text": "DISABLED",
+            "value": "DISABLED"
+          }
+        ],
+        readOnly: true
+      },
+      _required: {
+        type: Boolean,
+        value: false
+      },
+      _status: String,
+      _edit: {
+        type: Boolean,
+        value: false
+      },
+      _query: String
+    },
+
+    behaviours: [
+      Gerrit.FireBehavior,
+    ],
+    /**
+    * Fired when the cancel button is pressed.
+    *
+    * @event cancel
+    */
+
+
+    observers: [
+      '_updateUUID(_scheme, _id)',
+    ],
+
+    _checkerChanged() {
+      if (!this.checker) {
+        console.warn("checker not set");
+        return;
+      }
+      this._edit = true;
+      this._scheme = this.checker.uuid.split(':')[0];
+      this._id = this.checker.uuid.split(':')[1];
+      this._name = this.checker.name;
+      this._description = this.checker.description || '';
+      this._url = this.checker.url || '';
+      this._query = this.checker.query || '';
+      this._required = this.checker.blocking &&
+                        this.checker.blocking.length > 0;
+      if (this.checker.repository) {
+        this._repositorySelected = true;
+        this.set('_repos', [{name: this.checker.repository}]);
+      }
+      this._status = this.checker.status;
+    },
+
+    _updateUUID(_scheme, _id) {
+      this._uuid = _scheme + ":" + _id;
+    },
+
+    _handleStatusChange(e) {
+      this._status = e.detail.value;
+    },
+
+    _validateRequest() {
+      if (!this._name) {
+        this._errorMsg = 'Name cannot be empty';
+        return false;
+      }
+      if (this._description && this._description.length > 1000) {
+        this._errorMsg = 'Description should be less than 1000 characters';
+        return false;
+      }
+      if (!this._repositorySelected) {
+        this._errorMsg = 'Select a repository';
+        return false;
+      }
+      if (!this._scheme) {
+        this._errorMsg = 'Scheme cannot be empty.';
+        return false;
+      }
+      if (this._scheme.match(SCHEME_PATTERN) == null) {
+        this._errorMsg =
+          'Scheme must contain [A-Z], [a-z], [0-9] or {"-" , "_" , "."}';
+        return false;
+      }
+      if (this._scheme.length > 100) {
+        this._errorMsg = 'Scheme must be shorter than 100 characters';
+        return false;
+      }
+      if (!this._id) {
+        this._errorMsg = 'ID cannot be empty.';
+        return false;
+      }
+      if (this._id.match(SCHEME_PATTERN) == null) {
+        this._errorMsg =
+          'ID must contain [A-Z], [a-z], [0-9] or {"-" , "_" , "."}';
+        return false;
+      }
+      return true;
+    },
+
+    // TODO(dhruvsri): make sure dialog is scrollable.
+
+    _createChecker(checker) {
+      return this.pluginRestApi.send(
+        'POST',
+        CREATE_CHECKER_URL,
+        checker,
+      )
+    },
+
+    _editChecker(checker) {
+      const url = CREATE_CHECKER_URL + checker.uuid;
+      return this.pluginRestApi.send(
+        'POST',
+        url,
+        checker
+      )
+    },
+
+    handleEditChecker() {
+      if (!this._validateRequest()) return;
+      this._editChecker(this._getCheckerRequestObject()).then(
+        res => {
+          if (res) {
+            this._errorMsg = '';
+            this.fire('cancel', {reload: true}, {bubbles: true});
+          }
+        },
+        error => {
+          this._errorMsg = error;
+        }
+      )
+    },
+
+    _getCheckerRequestObject() {
+      return {
+        "name" : this._name,
+        "description" : this._description || '',
+        "uuid" : this._uuid,
+        "repository": this._repos[0].name,
+        "url" : this._url,
+        "status": this._status,
+        "blocking": this._required ? ["STATE_NOT_PASSING"] : [],
+        "query": this._query
+      }
+    },
+
+    handleCreateChecker() {
+      if (!this._validateRequest()) return;
+      // Currently after creating checker there is no reload happening (as
+      // this would result in the user exiting the screen).
+      this._createChecker(this._getCheckerRequestObject()).then(
+        res => {
+          if (res) this._cleanUp();
+        },
+        error => {
+          this._errorMsg = error;
+        }
+      )
+    },
+
+    _cleanUp() {
+      this._name = '';
+      this._scheme = '';
+      this._id = '';
+      this._uuid = '';
+      this._description = '';
+      this._repos = [];
+      this._repositorySelected = false;
+      this._errorMsg = '';
+      this._required = false;
+      this._query = '';
+      this._status = '';
+      this.fire('cancel', {reload: true}, {bubbles: true});
+    },
+
+    _repoSuggestions(filter) {
+      const _makeSuggestion = repo => {return {name: repo.name, value: repo}};
+      return this.pluginRestApi.getRepos(filter, REPOS_PER_PAGE).then(
+        repos => repos.map(repo =>  _makeSuggestion(repo))
+      )
+    },
+
+    _handleRepositorySelected(e) {
+      this.push('_repos', e.detail.value);
+      this._repositorySelected = true;
+    },
+
+    _handleRequiredCheckBoxClicked() {
+      this._required = !this._required;
+    },
+
+    _handleOnRemove(e) {
+      let idx = this._repos.indexOf(e.detail.repo);
+      if (idx == -1) return;
+      this.splice('_repos', idx, 1);
+      if (this._repos.length == 0) {
+        this._repositorySelected = false;
+      }
+    },
+
+  });
+})();
diff --git a/gr-checks/gr-repo-chip.html b/gr-checks/gr-repo-chip.html
new file mode 100644
index 0000000..b695fa9
--- /dev/null
+++ b/gr-checks/gr-repo-chip.html
@@ -0,0 +1,25 @@
+<dom-module id="gr-repo-chip">
+  <template>
+    <style>
+      iron-icon {
+        height: 1.2rem;
+        width: 1.2rem;
+      }
+      :host {
+        display: inline-block;
+      }
+    </style>
+    <span> {{repo.name}} </span>
+    <gr-button
+      id="remove"
+      link
+      hidden$="[[!removable]]"
+      tabindex="-1"
+      aria-label="Remove"
+      class="remove"
+      on-tap="_handleRemoveTap">
+      <iron-icon icon="gr-icons:close"></iron-icon>
+    </gr-button>
+  </template>
+  <script src="gr-repo-chip.js"></script>
+</dom-module>
\ No newline at end of file
diff --git a/gr-checks/gr-repo-chip.js b/gr-checks/gr-repo-chip.js
new file mode 100644
index 0000000..d63a476
--- /dev/null
+++ b/gr-checks/gr-repo-chip.js
@@ -0,0 +1,24 @@
+(function() {
+  'use strict';
+
+  /**
+   * autocomplete chip for getting repository suggestions
+   */
+  Polymer({
+    is: 'gr-repo-chip',
+    _legacyUndefinedCheck: true,
+    properties: {
+      // repo type is ProjectInfo
+      repo: Object,
+      removable: {
+        type: Boolean,
+        value: true,
+      },
+    },
+    _handleRemoveTap(e) {
+      e.preventDefault();
+      this.fire('remove', {repo: this.repo});
+    },
+  })
+
+})();
\ No newline at end of file
diff --git a/java/com/google/gerrit/plugins/checks/CheckJson.java b/java/com/google/gerrit/plugins/checks/CheckJson.java
index f36c79f..535fbc4 100644
--- a/java/com/google/gerrit/plugins/checks/CheckJson.java
+++ b/java/com/google/gerrit/plugins/checks/CheckJson.java
@@ -89,6 +89,7 @@
                 info.checkerName = checker.getName();
                 info.checkerStatus = checker.getStatus();
                 info.blocking = checker.getBlockingConditions();
+                info.checkerDescription = checker.getDescription().orElse(null);
               });
     } catch (ConfigInvalidException e) {
       logger.atWarning().withCause(e).log("skipping invalid checker %s", checkerUuid);
diff --git a/java/com/google/gerrit/plugins/checks/CheckUpdate.java b/java/com/google/gerrit/plugins/checks/CheckUpdate.java
index 315160d..0b86796 100644
--- a/java/com/google/gerrit/plugins/checks/CheckUpdate.java
+++ b/java/com/google/gerrit/plugins/checks/CheckUpdate.java
@@ -16,6 +16,7 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.gerrit.plugins.checks.api.CheckState;
+import com.google.gerrit.server.util.time.TimeUtil;
 import java.sql.Timestamp;
 import java.util.Optional;
 
@@ -49,6 +50,14 @@
 
     public abstract Builder setFinished(Timestamp finished);
 
+    public Builder unsetStarted() {
+      return setStarted(TimeUtil.never());
+    }
+
+    public Builder unsetFinished() {
+      return setFinished(TimeUtil.never());
+    }
+
     public abstract CheckUpdate build();
   }
 }
diff --git a/java/com/google/gerrit/plugins/checks/Checks.java b/java/com/google/gerrit/plugins/checks/Checks.java
index c4527e5..fa15c0e 100644
--- a/java/com/google/gerrit/plugins/checks/Checks.java
+++ b/java/com/google/gerrit/plugins/checks/Checks.java
@@ -18,6 +18,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.plugins.checks.api.CombinedCheckState;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import java.io.IOException;
@@ -90,6 +91,16 @@
   boolean areAllRequiredCheckersPassing(Project.NameKey projectName, PatchSet.Id patchSetId)
       throws IOException, StorageException;
 
+  /**
+   * Computes an ETag for the checks of the given change.
+   *
+   * @param projectName the name of the project that contains the change
+   * @param changeId ID of the change for which the ETag should be computed
+   * @return ETag for the checks of the given change
+   * @throws IOException if failed to access the checks data
+   */
+  String getETag(Project.NameKey projectName, Change.Id changeId) throws IOException;
+
   @AutoValue
   abstract class GetCheckOptions {
     public static GetCheckOptions defaults() {
diff --git a/java/com/google/gerrit/plugins/checks/ChecksETagComputation.java b/java/com/google/gerrit/plugins/checks/ChecksETagComputation.java
new file mode 100644
index 0000000..ff66eff
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/ChecksETagComputation.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.plugins.checks;
+
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.change.ChangeETagComputation;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+
+/**
+ * Ensures that the ETag of a change changes when there was any check update for the change.
+ *
+ * <p>This prevents callers using ETags from potentially seeing outdated submittability and combined
+ * check state information in {@link com.google.gerrit.extensions.common.ChangeInfo} when a check
+ * for the change was created or updated.
+ */
+@Singleton
+public class ChecksETagComputation implements ChangeETagComputation {
+  private final Checks checks;
+
+  @Inject
+  ChecksETagComputation(Checks checks) {
+    this.checks = checks;
+  }
+
+  @Override
+  public String getETag(Project.NameKey projectName, Change.Id changeId) {
+    try {
+      return checks.getETag(projectName, changeId);
+    } catch (IOException e) {
+      // The change ETag will be invalidated if the checks can be accessed the next time.
+      throw new StorageException(
+          String.format(
+              "Failed to compute ETag for checks of change %s in project %s",
+              changeId, projectName),
+          e);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/plugins/checks/Module.java b/java/com/google/gerrit/plugins/checks/Module.java
index 8440501..140497b 100644
--- a/java/com/google/gerrit/plugins/checks/Module.java
+++ b/java/com/google/gerrit/plugins/checks/Module.java
@@ -28,6 +28,7 @@
 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.change.ChangeETagComputation;
 import com.google.gerrit.server.git.validators.CommitValidationListener;
 import com.google.gerrit.server.git.validators.MergeValidationListener;
 import com.google.gerrit.server.git.validators.RefOperationValidationListener;
@@ -56,6 +57,10 @@
         .to(CheckerRefOperationValidator.class)
         .in(SINGLETON);
 
+    DynamicSet.bind(binder(), ChangeETagComputation.class)
+        .to(ChecksETagComputation.class)
+        .in(SINGLETON);
+
     DynamicSet.bind(binder(), ChangeAttributeFactory.class).to(ChangeCheckAttributeFactory.class);
     bind(DynamicOptions.DynamicBean.class)
         .annotatedWith(Exports.named(GetChange.class))
diff --git a/java/com/google/gerrit/plugins/checks/acceptance/AbstractCheckersTest.java b/java/com/google/gerrit/plugins/checks/acceptance/AbstractCheckersTest.java
index e476537..51fcc41 100644
--- a/java/com/google/gerrit/plugins/checks/acceptance/AbstractCheckersTest.java
+++ b/java/com/google/gerrit/plugins/checks/acceptance/AbstractCheckersTest.java
@@ -38,7 +38,7 @@
     sysModule = "com.google.gerrit.plugins.checks.acceptance.TestModule",
     httpModule = "com.google.gerrit.plugins.checks.HttpModule")
 public class AbstractCheckersTest extends LightweightPluginDaemonTest {
-  @Inject protected ProjectOperations projectOperations;
+  @Inject private ProjectOperations projectOperations;
 
   protected CheckerOperations checkerOperations;
   protected CheckOperations checkOperations;
diff --git a/java/com/google/gerrit/plugins/checks/acceptance/testsuite/BUILD b/java/com/google/gerrit/plugins/checks/acceptance/testsuite/BUILD
index 62a5299..577bfd4 100644
--- a/java/com/google/gerrit/plugins/checks/acceptance/testsuite/BUILD
+++ b/java/com/google/gerrit/plugins/checks/acceptance/testsuite/BUILD
@@ -8,6 +8,7 @@
     srcs = glob(["*.java"]),
     deps = [
         "//java/com/google/gerrit/acceptance:lib",
+        "//java/com/google/gerrit/server/util/time",
         "//plugins/checks:checks__plugin",
     ],
 )
diff --git a/java/com/google/gerrit/plugins/checks/acceptance/testsuite/TestCheckUpdate.java b/java/com/google/gerrit/plugins/checks/acceptance/testsuite/TestCheckUpdate.java
index 90864a6..16b4316 100644
--- a/java/com/google/gerrit/plugins/checks/acceptance/testsuite/TestCheckUpdate.java
+++ b/java/com/google/gerrit/plugins/checks/acceptance/testsuite/TestCheckUpdate.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.acceptance.testsuite.ThrowingConsumer;
 import com.google.gerrit.plugins.checks.CheckKey;
 import com.google.gerrit.plugins.checks.api.CheckState;
+import com.google.gerrit.server.util.time.TimeUtil;
 import java.sql.Timestamp;
 import java.util.Optional;
 
@@ -65,6 +66,14 @@
 
     public abstract Builder finished(Timestamp finished);
 
+    public Builder clearStarted() {
+      return started(TimeUtil.never());
+    }
+
+    public Builder clearFinished() {
+      return finished(TimeUtil.never());
+    }
+
     abstract Builder checkUpdater(ThrowingConsumer<TestCheckUpdate> checkUpdate);
 
     abstract TestCheckUpdate autoBuild();
diff --git a/java/com/google/gerrit/plugins/checks/api/ApiModule.java b/java/com/google/gerrit/plugins/checks/api/ApiModule.java
index ad1e7e3..88fb5cf 100644
--- a/java/com/google/gerrit/plugins/checks/api/ApiModule.java
+++ b/java/com/google/gerrit/plugins/checks/api/ApiModule.java
@@ -50,7 +50,7 @@
             postOnCollection(CHECK_KIND).to(PostCheck.class);
             get(CHECK_KIND).to(GetCheck.class);
             post(CHECK_KIND).to(UpdateCheck.class);
-
+            post(CHECK_KIND, "rerun").to(RerunCheck.class);
             DynamicMap.mapOf(binder(), PENDING_CHECK_KIND);
           }
         });
diff --git a/java/com/google/gerrit/plugins/checks/api/CheckApi.java b/java/com/google/gerrit/plugins/checks/api/CheckApi.java
index d137645..14acb41 100644
--- a/java/com/google/gerrit/plugins/checks/api/CheckApi.java
+++ b/java/com/google/gerrit/plugins/checks/api/CheckApi.java
@@ -26,6 +26,8 @@
   /** Updates a check and returns the {@link CheckInfo} for the updated resource. */
   CheckInfo update(CheckInput input) throws RestApiException;
 
+  /** Reruns the check and returns the {@link CheckInfo} for the updated check. */
+  CheckInfo rerun() throws RestApiException;
   /**
    * A default implementation which allows source compatibility when adding new methods to the
    * interface.
@@ -40,5 +42,10 @@
     public CheckInfo update(CheckInput input) throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public CheckInfo rerun() throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/java/com/google/gerrit/plugins/checks/api/CheckApiImpl.java b/java/com/google/gerrit/plugins/checks/api/CheckApiImpl.java
index 378ce64..3be0a74 100644
--- a/java/com/google/gerrit/plugins/checks/api/CheckApiImpl.java
+++ b/java/com/google/gerrit/plugins/checks/api/CheckApiImpl.java
@@ -30,19 +30,25 @@
   private final GetCheck getCheck;
   private final UpdateCheck updateCheck;
   private final CheckResource checkResource;
+  private final RerunCheck rerunCheck;
 
   @Inject
-  CheckApiImpl(GetCheck getCheck, UpdateCheck updateCheck, @Assisted CheckResource checkResource) {
+  CheckApiImpl(
+      GetCheck getCheck,
+      UpdateCheck updateCheck,
+      @Assisted CheckResource checkResource,
+      RerunCheck rerunCheck) {
     this.getCheck = getCheck;
     this.updateCheck = updateCheck;
     this.checkResource = checkResource;
+    this.rerunCheck = rerunCheck;
   }
 
   @Override
   public CheckInfo get(ListChecksOption... options) throws RestApiException {
     try {
       Arrays.stream(options).forEach(getCheck::addOption);
-      return getCheck.apply(checkResource);
+      return getCheck.apply(checkResource).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot retrieve check", e);
     }
@@ -51,9 +57,18 @@
   @Override
   public CheckInfo update(CheckInput input) throws RestApiException {
     try {
-      return updateCheck.apply(checkResource, input);
+      return updateCheck.apply(checkResource, input).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot update check", e);
     }
   }
+
+  @Override
+  public CheckInfo rerun() throws RestApiException {
+    try {
+      return rerunCheck.apply(checkResource, null).value();
+    } catch (Exception e) {
+      throw asRestApiException("Cannot rerun check", e);
+    }
+  }
 }
diff --git a/java/com/google/gerrit/plugins/checks/api/CheckInfo.java b/java/com/google/gerrit/plugins/checks/api/CheckInfo.java
index 77cbdef..d0d5c38 100644
--- a/java/com/google/gerrit/plugins/checks/api/CheckInfo.java
+++ b/java/com/google/gerrit/plugins/checks/api/CheckInfo.java
@@ -56,6 +56,9 @@
   /** Blocking conditions that apply to this check. */
   public Set<BlockingCondition> blocking;
 
+  /** Description of the checker that produced this check */
+  public String checkerDescription;
+
   @Override
   public boolean equals(Object o) {
     if (!(o instanceof CheckInfo)) {
@@ -75,7 +78,8 @@
         && Objects.equals(other.updated, updated)
         && Objects.equals(other.checkerName, checkerName)
         && Objects.equals(other.checkerStatus, checkerStatus)
-        && Objects.equals(other.blocking, blocking);
+        && Objects.equals(other.blocking, blocking)
+        && Objects.equals(other.checkerDescription, checkerDescription);
   }
 
   @Override
@@ -94,7 +98,8 @@
         updated,
         checkerName,
         checkerStatus,
-        blocking);
+        blocking,
+        checkerDescription);
   }
 
   @Override
@@ -114,6 +119,7 @@
         .add("checkerName", checkerName)
         .add("checkerStatus", checkerStatus)
         .add("blocking", blocking)
+        .add("checkerDescription", checkerDescription)
         .toString();
   }
 }
diff --git a/java/com/google/gerrit/plugins/checks/api/CheckerApiImpl.java b/java/com/google/gerrit/plugins/checks/api/CheckerApiImpl.java
index db74c00..1a63eac 100644
--- a/java/com/google/gerrit/plugins/checks/api/CheckerApiImpl.java
+++ b/java/com/google/gerrit/plugins/checks/api/CheckerApiImpl.java
@@ -41,7 +41,7 @@
   @Override
   public CheckerInfo get() throws RestApiException {
     try {
-      return getChecker.apply(rsrc);
+      return getChecker.apply(rsrc).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot retrieve checker", e);
     }
@@ -50,7 +50,7 @@
   @Override
   public CheckerInfo update(CheckerInput input) throws RestApiException {
     try {
-      return updateChecker.apply(rsrc, input);
+      return updateChecker.apply(rsrc, input).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot update checker", e);
     }
diff --git a/java/com/google/gerrit/plugins/checks/api/CheckersImpl.java b/java/com/google/gerrit/plugins/checks/api/CheckersImpl.java
index 73e0f5d..a5d101b 100644
--- a/java/com/google/gerrit/plugins/checks/api/CheckersImpl.java
+++ b/java/com/google/gerrit/plugins/checks/api/CheckersImpl.java
@@ -64,7 +64,7 @@
   @Override
   public List<CheckerInfo> all() throws RestApiException {
     try {
-      return listCheckers.apply(TopLevelResource.INSTANCE);
+      return listCheckers.apply(TopLevelResource.INSTANCE).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot list all checkers ", e);
     }
diff --git a/java/com/google/gerrit/plugins/checks/api/ChecksImpl.java b/java/com/google/gerrit/plugins/checks/api/ChecksImpl.java
index 206e7b0..36d1550 100644
--- a/java/com/google/gerrit/plugins/checks/api/ChecksImpl.java
+++ b/java/com/google/gerrit/plugins/checks/api/ChecksImpl.java
@@ -65,7 +65,7 @@
   @Override
   public CheckApi create(CheckInput input) throws RestApiException {
     try {
-      CheckInfo checkInfo = postCheck.apply(revisionResource, input);
+      CheckInfo checkInfo = postCheck.apply(revisionResource, input).value();
       return id(CheckerUuid.parse(checkInfo.checkerUuid));
     } catch (Exception e) {
       throw asRestApiException("Cannot create check", e);
@@ -76,7 +76,7 @@
   public ImmutableList<CheckInfo> list(ListChecksOption... options) throws RestApiException {
     try {
       Arrays.stream(options).forEach(listChecks::addOption);
-      return listChecks.apply(revisionResource);
+      return listChecks.apply(revisionResource).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot list checks", e);
     }
diff --git a/java/com/google/gerrit/plugins/checks/api/GetCheck.java b/java/com/google/gerrit/plugins/checks/api/GetCheck.java
index ab2abde..2c35307 100644
--- a/java/com/google/gerrit/plugins/checks/api/GetCheck.java
+++ b/java/com/google/gerrit/plugins/checks/api/GetCheck.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.plugins.checks.CheckJson;
 import com.google.gerrit.plugins.checks.ListChecksOption;
@@ -47,8 +48,8 @@
   }
 
   @Override
-  public CheckInfo apply(CheckResource resource)
+  public Response<CheckInfo> apply(CheckResource resource)
       throws AuthException, BadRequestException, ResourceConflictException, IOException {
-    return checkJsonFactory.create(options).format(resource.getCheck());
+    return Response.ok(checkJsonFactory.create(options).format(resource.getCheck()));
   }
 }
diff --git a/java/com/google/gerrit/plugins/checks/api/GetChecker.java b/java/com/google/gerrit/plugins/checks/api/GetChecker.java
index 9a84a91..28d95fa 100644
--- a/java/com/google/gerrit/plugins/checks/api/GetChecker.java
+++ b/java/com/google/gerrit/plugins/checks/api/GetChecker.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.plugins.checks.api;
 
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.plugins.checks.CheckerJson;
 import com.google.inject.Inject;
@@ -29,7 +30,7 @@
   }
 
   @Override
-  public CheckerInfo apply(CheckerResource resource) {
-    return checkerJson.format(resource.getChecker());
+  public Response<CheckerInfo> apply(CheckerResource resource) {
+    return Response.ok(checkerJson.format(resource.getChecker()));
   }
 }
diff --git a/java/com/google/gerrit/plugins/checks/api/ListCheckers.java b/java/com/google/gerrit/plugins/checks/api/ListCheckers.java
index 0328a99..ae36fb9 100644
--- a/java/com/google/gerrit/plugins/checks/api/ListCheckers.java
+++ b/java/com/google/gerrit/plugins/checks/api/ListCheckers.java
@@ -17,6 +17,7 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
@@ -55,13 +56,13 @@
   }
 
   @Override
-  public List<CheckerInfo> apply(TopLevelResource resource)
+  public Response<List<CheckerInfo>> apply(TopLevelResource resource)
       throws RestApiException, PermissionBackendException, IOException {
     if (!self.get().isIdentifiedUser()) {
       throw new AuthException("Authentication required");
     }
     permissionBackend.currentUser().check(permission);
 
-    return checkers.listCheckers().stream().map(checkerJson::format).collect(toList());
+    return Response.ok(checkers.listCheckers().stream().map(checkerJson::format).collect(toList()));
   }
 }
diff --git a/java/com/google/gerrit/plugins/checks/api/ListChecks.java b/java/com/google/gerrit/plugins/checks/api/ListChecks.java
index 39346e4..4b5e0ae 100644
--- a/java/com/google/gerrit/plugins/checks/api/ListChecks.java
+++ b/java/com/google/gerrit/plugins/checks/api/ListChecks.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.plugins.checks.Check;
 import com.google.gerrit.plugins.checks.CheckJson;
@@ -55,7 +56,7 @@
   }
 
   @Override
-  public ImmutableList<CheckInfo> apply(RevisionResource resource)
+  public Response<ImmutableList<CheckInfo>> apply(RevisionResource resource)
       throws AuthException, BadRequestException, ResourceConflictException, StorageException,
           IOException {
     if (resource.getEdit().isPresent()) {
@@ -72,6 +73,6 @@
     for (Check check : allChecks) {
       result.add(checkJson.format(check));
     }
-    return result.build();
+    return Response.ok(result.build());
   }
 }
diff --git a/java/com/google/gerrit/plugins/checks/api/PostCheck.java b/java/com/google/gerrit/plugins/checks/api/PostCheck.java
index 18572e9..5531d9e 100644
--- a/java/com/google/gerrit/plugins/checks/api/PostCheck.java
+++ b/java/com/google/gerrit/plugins/checks/api/PostCheck.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestCollectionModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
@@ -74,7 +75,7 @@
   }
 
   @Override
-  public CheckInfo apply(RevisionResource rsrc, CheckInput input)
+  public Response<CheckInfo> apply(RevisionResource rsrc, CheckInput input)
       throws StorageException, IOException, RestApiException, PermissionBackendException,
           ConfigInvalidException {
     if (!self.get().isIdentifiedUser()) {
@@ -112,7 +113,7 @@
     } else {
       updatedCheck = checksUpdate.get().updateCheck(key, toCheckUpdate(input));
     }
-    return checkJsonFactory.noOptions().format(updatedCheck);
+    return Response.ok(checkJsonFactory.noOptions().format(updatedCheck));
   }
 
   private static CheckUpdate toCheckUpdate(CheckInput input) throws BadRequestException {
diff --git a/java/com/google/gerrit/plugins/checks/api/QueryPendingChecks.java b/java/com/google/gerrit/plugins/checks/api/QueryPendingChecks.java
index 281417c..870cd61 100644
--- a/java/com/google/gerrit/plugins/checks/api/QueryPendingChecks.java
+++ b/java/com/google/gerrit/plugins/checks/api/QueryPendingChecks.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
@@ -81,11 +82,11 @@
 
   public List<PendingChecksInfo> apply()
       throws RestApiException, IOException, ConfigInvalidException, StorageException {
-    return apply(TopLevelResource.INSTANCE);
+    return apply(TopLevelResource.INSTANCE).value();
   }
 
   @Override
-  public List<PendingChecksInfo> apply(TopLevelResource resource)
+  public Response<List<PendingChecksInfo>> apply(TopLevelResource resource)
       throws RestApiException, IOException, ConfigInvalidException, StorageException {
     if (queryString == null) {
       throw new BadRequestException("query is required");
@@ -98,7 +99,7 @@
 
     Optional<Checker> checker = checkers.getChecker(getCheckerUuidFromQuery(query));
     if (!checker.isPresent() || checker.get().isDisabled()) {
-      return ImmutableList.of();
+      return Response.ok(ImmutableList.of());
     }
 
     // The query system can only match against the current patch set; ignore non-current patch sets
@@ -123,7 +124,7 @@
         pendingChecks.add(createPendingChecksInfo(cd.project(), patchSet, checkerUuid, check));
       }
     }
-    return pendingChecks;
+    return Response.ok(pendingChecks);
   }
 
   private Predicate<Check> parseQuery(String query) throws BadRequestException {
diff --git a/java/com/google/gerrit/plugins/checks/api/RerunCheck.java b/java/com/google/gerrit/plugins/checks/api/RerunCheck.java
new file mode 100644
index 0000000..4c6b0a4
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/api/RerunCheck.java
@@ -0,0 +1,119 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.plugins.checks.api;
+
+import com.google.gerrit.extensions.common.Input;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.plugins.checks.AdministrateCheckersPermission;
+import com.google.gerrit.plugins.checks.Check;
+import com.google.gerrit.plugins.checks.CheckJson;
+import com.google.gerrit.plugins.checks.CheckKey;
+import com.google.gerrit.plugins.checks.CheckUpdate;
+import com.google.gerrit.plugins.checks.Checker;
+import com.google.gerrit.plugins.checks.CheckerUuid;
+import com.google.gerrit.plugins.checks.Checkers;
+import com.google.gerrit.plugins.checks.Checks;
+import com.google.gerrit.plugins.checks.Checks.GetCheckOptions;
+import com.google.gerrit.plugins.checks.ChecksUpdate;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.UserInitiated;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.Optional;
+import javax.inject.Provider;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+@Singleton
+public class RerunCheck implements RestModifyView<CheckResource, Input> {
+  private final Provider<CurrentUser> self;
+  private final PermissionBackend permissionBackend;
+  private final AdministrateCheckersPermission permission;
+  private final Checks checks;
+  private final Provider<ChecksUpdate> checksUpdate;
+  private final CheckJson.Factory checkJsonFactory;
+  private final Checkers checkers;
+
+  @Inject
+  RerunCheck(
+      Provider<CurrentUser> self,
+      PermissionBackend permissionBackend,
+      AdministrateCheckersPermission permission,
+      Checks checks,
+      @UserInitiated Provider<ChecksUpdate> checksUpdate,
+      CheckJson.Factory checkJsonFactory,
+      Checkers checkers) {
+    this.self = self;
+    this.permissionBackend = permissionBackend;
+    this.permission = permission;
+    this.checks = checks;
+    this.checksUpdate = checksUpdate;
+    this.checkJsonFactory = checkJsonFactory;
+    this.checkers = checkers;
+  }
+
+  @Override
+  public Response<CheckInfo> apply(CheckResource checkResource, Input input)
+      throws RestApiException, IOException, PermissionBackendException, ConfigInvalidException {
+    if (!self.get().isIdentifiedUser()) {
+      throw new AuthException("Authentication required");
+    }
+    permissionBackend.currentUser().check(permission);
+    if (checkResource.getRevisionResource().getEdit().isPresent()) {
+      throw new ResourceConflictException("checks are not supported on a change edit");
+    }
+    CheckKey key =
+        CheckKey.create(
+            checkResource.getRevisionResource().getProject(),
+            checkResource.getRevisionResource().getPatchSet().id(),
+            checkResource.getCheckerUuid());
+    Optional<Check> check = checks.getCheck(key, GetCheckOptions.defaults());
+    CheckerUuid checkerUuid = checkResource.getCheckerUuid();
+    Check updatedCheck;
+    if (!check.isPresent()) {
+      Checker checker =
+          checkers
+              .getChecker(checkerUuid)
+              .orElseThrow(
+                  () ->
+                      new ResourceNotFoundException(
+                          String.format("checker %s not found", checkerUuid)));
+      // This error should not be thrown since this case is filtered before reaching this code.
+      // Also return a backfilled check for checkers that do not apply to the change.
+      updatedCheck =
+          Check.newBackfilledCheck(
+              checkResource.getRevisionResource().getProject(),
+              checkResource.getRevisionResource().getPatchSet(),
+              checker);
+    } else {
+      CheckUpdate.Builder builder = CheckUpdate.builder();
+      builder
+          .setState(CheckState.NOT_STARTED)
+          .unsetFinished()
+          .unsetStarted()
+          .setMessage("")
+          .setUrl("");
+      updatedCheck = checksUpdate.get().updateCheck(key, builder.build());
+    }
+    return Response.ok(checkJsonFactory.noOptions().format(updatedCheck));
+  }
+}
diff --git a/java/com/google/gerrit/plugins/checks/api/UpdateCheck.java b/java/com/google/gerrit/plugins/checks/api/UpdateCheck.java
index ef7539f..1a57142 100644
--- a/java/com/google/gerrit/plugins/checks/api/UpdateCheck.java
+++ b/java/com/google/gerrit/plugins/checks/api/UpdateCheck.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -34,7 +35,7 @@
   }
 
   @Override
-  public CheckInfo apply(CheckResource checkResource, CheckInput input)
+  public Response<CheckInfo> apply(CheckResource checkResource, CheckInput input)
       throws RestApiException, IOException, StorageException, PermissionBackendException,
           ConfigInvalidException {
     if (input == null) {
diff --git a/java/com/google/gerrit/plugins/checks/api/UpdateChecker.java b/java/com/google/gerrit/plugins/checks/api/UpdateChecker.java
index 6512b69..1f81284 100644
--- a/java/com/google/gerrit/plugins/checks/api/UpdateChecker.java
+++ b/java/com/google/gerrit/plugins/checks/api/UpdateChecker.java
@@ -17,6 +17,7 @@
 import com.google.common.collect.ImmutableSortedSet;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
@@ -68,7 +69,7 @@
   }
 
   @Override
-  public CheckerInfo apply(CheckerResource resource, CheckerInput input)
+  public Response<CheckerInfo> apply(CheckerResource resource, CheckerInput input)
       throws RestApiException, PermissionBackendException, NoSuchCheckerException, IOException,
           ConfigInvalidException, StorageException {
     permissionBackend.currentUser().check(permission);
@@ -120,7 +121,7 @@
 
     Checker updatedChecker =
         checkersUpdate.get().updateChecker(checkerUuid, checkerUpdateBuilder.build());
-    return checkerJson.format(updatedChecker);
+    return Response.ok(checkerJson.format(updatedChecker));
   }
 
   private Project.NameKey resolveRepository(String repository)
diff --git a/java/com/google/gerrit/plugins/checks/db/CheckerConfig.java b/java/com/google/gerrit/plugins/checks/db/CheckerConfig.java
index 57fda1e..2594a96 100644
--- a/java/com/google/gerrit/plugins/checks/db/CheckerConfig.java
+++ b/java/com/google/gerrit/plugins/checks/db/CheckerConfig.java
@@ -28,7 +28,6 @@
 import com.google.gerrit.plugins.checks.Checkers;
 import com.google.gerrit.plugins.checks.CheckersUpdate;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.NameKey;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.git.meta.VersionedMetaData;
 import com.google.gerrit.server.util.time.TimeUtil;
@@ -152,7 +151,8 @@
    * @throws IOException if the repository can't be accessed for some reason
    * @throws ConfigInvalidException if the checker exists but can't be read due to an invalid format
    */
-  public static CheckerConfig loadForChecker(NameKey projectName, Repository repository, Ref ref)
+  public static CheckerConfig loadForChecker(
+      Project.NameKey projectName, Repository repository, Ref ref)
       throws IOException, ConfigInvalidException {
     CheckerConfig checkerConfig = new CheckerConfig(ref.getName());
     checkerConfig.load(projectName, repository);
diff --git a/java/com/google/gerrit/plugins/checks/db/NoteDbCheck.java b/java/com/google/gerrit/plugins/checks/db/NoteDbCheck.java
index cb60d2c..043028a 100644
--- a/java/com/google/gerrit/plugins/checks/db/NoteDbCheck.java
+++ b/java/com/google/gerrit/plugins/checks/db/NoteDbCheck.java
@@ -9,6 +9,7 @@
 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 java.sql.Timestamp;
 
 /** Representation of {@link Check} that can be serialized with GSON. */
@@ -74,11 +75,19 @@
       modified = true;
     }
     if (update.started().isPresent() && !update.started().get().equals(started)) {
-      started = update.started().get();
+      if (update.started().get().equals(TimeUtil.never())) {
+        started = null;
+      } else {
+        started = update.started().get();
+      }
       modified = true;
     }
     if (update.finished().isPresent() && !update.finished().get().equals(finished)) {
-      finished = update.finished().get();
+      if (update.finished().get().equals(TimeUtil.never())) {
+        finished = null;
+      } else {
+        finished = update.finished().get();
+      }
       modified = true;
     }
     return modified;
diff --git a/java/com/google/gerrit/plugins/checks/db/NoteDbChecks.java b/java/com/google/gerrit/plugins/checks/db/NoteDbChecks.java
index 02847e8..1e245fd 100644
--- a/java/com/google/gerrit/plugins/checks/db/NoteDbChecks.java
+++ b/java/com/google/gerrit/plugins/checks/db/NoteDbChecks.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.plugins.checks.CheckKey;
 import com.google.gerrit.plugins.checks.Checker;
 import com.google.gerrit.plugins.checks.CheckerQuery;
+import com.google.gerrit.plugins.checks.CheckerRef;
 import com.google.gerrit.plugins.checks.CheckerUuid;
 import com.google.gerrit.plugins.checks.Checkers;
 import com.google.gerrit.plugins.checks.Checks;
@@ -33,10 +34,10 @@
 import com.google.gerrit.plugins.checks.api.CheckerStatus;
 import com.google.gerrit.plugins.checks.api.CombinedCheckState;
 import com.google.gerrit.plugins.checks.api.CombinedCheckState.CheckStateCount;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSet.Id;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.NameKey;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -46,6 +47,10 @@
 import java.util.Map;
 import java.util.Optional;
 import java.util.stream.Stream;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
 
 /** Class to read checks from NoteDb. */
 @Singleton
@@ -55,6 +60,7 @@
   private final Checkers checkers;
   private final CheckBackfiller checkBackfiller;
   private final Provider<CheckerQuery> checkerQueryProvider;
+  private final GitRepositoryManager repoManager;
 
   @Inject
   NoteDbChecks(
@@ -62,12 +68,14 @@
       CheckNotes.Factory checkNotesFactory,
       Checkers checkers,
       CheckBackfiller checkBackfiller,
-      Provider<CheckerQuery> checkerQueryProvider) {
+      Provider<CheckerQuery> checkerQueryProvider,
+      GitRepositoryManager repoManager) {
     this.changeDataFactory = changeDataFactory;
     this.checkNotesFactory = checkNotesFactory;
     this.checkers = checkers;
     this.checkBackfiller = checkBackfiller;
     this.checkerQueryProvider = checkerQueryProvider;
+    this.repoManager = repoManager;
   }
 
   @Override
@@ -131,15 +139,15 @@
   }
 
   @Override
-  public CombinedCheckState getCombinedCheckState(NameKey projectName, Id patchSetId)
-      throws IOException, StorageException {
+  public CombinedCheckState getCombinedCheckState(
+      Project.NameKey projectName, PatchSet.Id patchSetId) throws IOException, StorageException {
     ImmutableListMultimap<CheckState, Boolean> statesAndRequired =
         getStatesAndRequiredMap(projectName, patchSetId);
     return CombinedCheckState.combine(statesAndRequired);
   }
 
   @Override
-  public boolean areAllRequiredCheckersPassing(NameKey projectName, Id patchSetId)
+  public boolean areAllRequiredCheckersPassing(Project.NameKey projectName, PatchSet.Id patchSetId)
       throws IOException, StorageException {
     ImmutableListMultimap<CheckState, Boolean> statesAndRequired =
         getStatesAndRequiredMap(projectName, patchSetId);
@@ -148,8 +156,17 @@
         && checkStateCount.inProgressRequiredCount() == 0;
   }
 
+  @Override
+  public String getETag(Project.NameKey projectName, Change.Id changeId) throws IOException {
+    try (Repository repo = repoManager.openRepository(projectName);
+        RevWalk rw = new RevWalk(repo)) {
+      Ref checkRef = repo.getRefDatabase().exactRef(CheckerRef.checksRef(changeId));
+      return checkRef != null ? checkRef.getObjectId().getName() : ObjectId.zeroId().getName();
+    }
+  }
+
   private ImmutableListMultimap<CheckState, Boolean> getStatesAndRequiredMap(
-      NameKey projectName, Id patchSetId) throws IOException, StorageException {
+      Project.NameKey projectName, PatchSet.Id patchSetId) throws IOException, StorageException {
     ChangeData changeData = changeDataFactory.create(projectName, patchSetId.changeId());
     ImmutableMap<String, Checker> allCheckersOfProject =
         checkers.checkersOf(projectName).stream()
diff --git a/java/com/google/gerrit/plugins/checks/rules/ChecksSubmitRule.java b/java/com/google/gerrit/plugins/checks/rules/ChecksSubmitRule.java
index bf4fb78..a1df4f5 100644
--- a/java/com/google/gerrit/plugins/checks/rules/ChecksSubmitRule.java
+++ b/java/com/google/gerrit/plugins/checks/rules/ChecksSubmitRule.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.plugins.checks.rules;
 
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitRecord.Status;
@@ -26,13 +25,12 @@
 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.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.rules.SubmitRule;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.util.Collection;
+import java.util.Optional;
 
 @Singleton
 public class ChecksSubmitRule implements SubmitRule {
@@ -61,7 +59,7 @@
   }
 
   @Override
-  public Collection<SubmitRecord> evaluate(ChangeData changeData, SubmitRuleOptions options) {
+  public Optional<SubmitRecord> evaluate(ChangeData changeData) {
     Project.NameKey project = changeData.project();
     Change.Id changeId = changeData.getId();
 
@@ -72,7 +70,7 @@
       String errorMessage =
           String.format("failed to load the current patch set of change %s", changeId);
       logger.atSevere().withCause(e).log(errorMessage);
-      return singletonRecordForRuleError(errorMessage);
+      return recordForRuleError(errorMessage);
     }
 
     boolean areAllRequiredCheckersPassing;
@@ -83,24 +81,24 @@
       String errorMessage =
           String.format("failed to evaluate check states for change %s", changeId);
       logger.atSevere().withCause(e).log(errorMessage);
-      return singletonRecordForRuleError(errorMessage);
+      return recordForRuleError(errorMessage);
     }
 
     SubmitRecord submitRecord = new SubmitRecord();
     if (areAllRequiredCheckersPassing) {
       submitRecord.status = Status.OK;
-      return ImmutableList.of(submitRecord);
+      return Optional.of(submitRecord);
     }
 
     submitRecord.status = Status.NOT_READY;
     submitRecord.requirements = ImmutableList.of(DEFAULT_SUBMIT_REQUIREMENT_FOR_CHECKS);
-    return ImmutableSet.of(submitRecord);
+    return Optional.of(submitRecord);
   }
 
-  private static Collection<SubmitRecord> singletonRecordForRuleError(String reason) {
+  private static Optional<SubmitRecord> recordForRuleError(String reason) {
     SubmitRecord submitRecord = new SubmitRecord();
     submitRecord.errorMessage = reason;
     submitRecord.status = SubmitRecord.Status.RULE_ERROR;
-    return ImmutableList.of(submitRecord);
+    return Optional.of(submitRecord);
   }
 }
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/CheckerRefsIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/CheckerRefsIT.java
index ee4b51f..71d468d 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/CheckerRefsIT.java
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/CheckerRefsIT.java
@@ -24,6 +24,7 @@
 
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.SkipProjectClone;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.common.ChangeInput;
@@ -51,6 +52,7 @@
 
 @SkipProjectClone
 public class CheckerRefsIT extends AbstractCheckersTest {
+  @Inject private ProjectOperations projectOperations;
   @Inject private Sequences seq;
   @Inject private ChangeInserter.Factory changeInserterFactory;
   @Inject private BatchUpdate.Factory updateFactory;
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/ChecksRestApiBindingsIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/ChecksRestApiBindingsIT.java
index 557aab0..03a9e7d 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/ChecksRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/ChecksRestApiBindingsIT.java
@@ -47,7 +47,8 @@
   private static final ImmutableList<RestCall> SCOPED_CHECK_ENDPOINTS =
       ImmutableList.of(
           RestCall.get("/changes/%s/revisions/%s/checks~checks/%s"),
-          RestCall.post("/changes/%s/revisions/%s/checks~checks/%s"));
+          RestCall.post("/changes/%s/revisions/%s/checks~checks/%s"),
+          RestCall.post("/changes/%s/revisions/%s/checks~checks/%s/rerun"));
 
   @Test
   public void rootEndpoints() throws Exception {
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 a8a8d19..d16df11 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/api/CreateCheckIT.java
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/api/CreateCheckIT.java
@@ -372,6 +372,36 @@
     assertThat(response.getEntityContent()).isEqualTo("checks are not supported on a change edit");
   }
 
+  @Test
+  public void creationOfCheckChangesETagOfChange() throws Exception {
+    CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
+
+    String oldETag = parseChangeResource(patchSetId.changeId().toString()).getETag();
+
+    CheckInput input = new CheckInput();
+    input.checkerUuid = checkerUuid.get();
+    input.state = CheckState.RUNNING;
+    checksApiFactory.revision(patchSetId).create(input).get();
+
+    String newETag = parseChangeResource(patchSetId.changeId().toString()).getETag();
+    assertThat(newETag).isNotEqualTo(oldETag);
+  }
+
+  @Test
+  public void creationOfCheckChangesETagOfRevisionActions() throws Exception {
+    CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
+
+    String oldETag = gApi.changes().id(patchSetId.changeId().toString()).current().etag();
+
+    CheckInput input = new CheckInput();
+    input.checkerUuid = checkerUuid.get();
+    input.state = CheckState.RUNNING;
+    checksApiFactory.revision(patchSetId).create(input).get();
+
+    String newETag = gApi.changes().id(patchSetId.changeId().toString()).current().etag();
+    assertThat(newETag).isNotEqualTo(oldETag);
+  }
+
   // 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)
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 9e2a066..ecd2596 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/api/GetCheckIT.java
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/api/GetCheckIT.java
@@ -89,7 +89,12 @@
   @Test
   public void getCheckWithOptions() throws Exception {
     CheckerUuid checkerUuid =
-        checkerOperations.newChecker().repository(project).name("My Checker").create();
+        checkerOperations
+            .newChecker()
+            .repository(project)
+            .name("My Checker")
+            .description("Description")
+            .create();
 
     CheckKey checkKey = CheckKey.create(project, patchSetId, checkerUuid);
     checkOperations.newCheck(checkKey).state(CheckState.RUNNING).upsert();
@@ -99,7 +104,7 @@
     expectedCheckInfo.checkerName = "My Checker";
     expectedCheckInfo.checkerStatus = CheckerStatus.ENABLED;
     expectedCheckInfo.blocking = ImmutableSortedSet.of();
-
+    expectedCheckInfo.checkerDescription = "Description";
     assertThat(getCheckInfo(patchSetId, checkerUuid, ListChecksOption.CHECKER))
         .isEqualTo(expectedCheckInfo);
   }
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 a19dee2..3f1f132 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/api/GetCheckerIT.java
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/api/GetCheckerIT.java
@@ -320,7 +320,7 @@
 
   @Test
   public void administrateCheckersCapabilityIsAdvertised() throws Exception {
-    Map<String, CapabilityInfo> capabilities = listCapabilities.apply(new ConfigResource());
+    Map<String, CapabilityInfo> capabilities = listCapabilities.apply(new ConfigResource()).value();
     String capability = "checks-administrateCheckers";
     assertThat(capabilities).containsKey(capability);
     CapabilityInfo info = capabilities.get(capability);
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/api/QueryPendingChecksIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/api/QueryPendingChecksIT.java
index 5bfbee0..2727203 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/api/QueryPendingChecksIT.java
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/api/QueryPendingChecksIT.java
@@ -23,6 +23,7 @@
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -50,6 +51,7 @@
 import org.junit.Test;
 
 public class QueryPendingChecksIT extends AbstractCheckersTest {
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   private PatchSet.Id patchSetId;
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/api/RerunCheckIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/api/RerunCheckIT.java
new file mode 100644
index 0000000..9cc539f
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/api/RerunCheckIT.java
@@ -0,0 +1,150 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.plugins.checks.acceptance.api;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+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.api.CheckInfo;
+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;
+
+public class RerunCheckIT extends AbstractCheckersTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private ProjectOperations projectOperations;
+
+  private PatchSet.Id patchSetId;
+  private CheckKey checkKey;
+
+  @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();
+    checkKey = CheckKey.create(project, patchSetId, checkerUuid);
+  }
+
+  @After
+  public void resetTime() {
+    TestTimeUtil.useSystemTime();
+  }
+
+  @Test
+  public void rerunResetsCheckInfo() throws Exception {
+    checkOperations
+        .newCheck(checkKey)
+        .state(CheckState.FAILED)
+        .started(TimeUtil.nowTs())
+        .finished(TimeUtil.nowTs())
+        .message("message")
+        .url("url.com")
+        .upsert();
+    Timestamp created = checkOperations.check(checkKey).get().created();
+    Timestamp updated = checkOperations.check(checkKey).get().updated();
+    CheckInfo info = checksApiFactory.revision(patchSetId).id(checkKey.checkerUuid()).rerun();
+    assertThat(info.state).isEqualTo(CheckState.NOT_STARTED);
+    assertThat(info.message).isEqualTo(null);
+    assertThat(info.url).isEqualTo(null);
+    assertThat(info.started).isEqualTo(null);
+    assertThat(info.finished).isEqualTo(null);
+    assertThat(info.created).isEqualTo(created);
+    assertThat(info.updated).isGreaterThan(updated);
+  }
+
+  @Test
+  public void rerunNotStartedCheck() throws Exception {
+    checkOperations.newCheck(checkKey).state(CheckState.NOT_STARTED).upsert();
+    CheckInfo info = checksApiFactory.revision(patchSetId).id(checkKey.checkerUuid()).rerun();
+    assertThat(info.state).isEqualTo(CheckState.NOT_STARTED);
+  }
+
+  @Test
+  public void rerunFinishedCheck() throws Exception {
+    checkOperations.newCheck(checkKey).state(CheckState.SUCCESSFUL).upsert();
+    CheckInfo info = checksApiFactory.revision(patchSetId).id(checkKey.checkerUuid()).rerun();
+    assertThat(info.state).isEqualTo(CheckState.NOT_STARTED);
+    assertThat(info.updated).isGreaterThan(info.created);
+  }
+
+  @Test
+  public void rerunCheckNotExistingButBackfilled() throws Exception {
+    CheckInfo info = checksApiFactory.revision(patchSetId).id(checkKey.checkerUuid()).rerun();
+    assertThat(info.state).isEqualTo(CheckState.NOT_STARTED);
+    assertThat(checkOperations.check(checkKey).exists()).isFalse();
+  }
+
+  @Test
+  public void rerunExistingCheckWithCheckerNotAppliedToChange() throws Exception {
+    Project.NameKey otherProject = projectOperations.newProject().create();
+    checkerOperations.checker(checkKey.checkerUuid()).forUpdate().repository(otherProject).update();
+    checkOperations.newCheck(checkKey).upsert();
+    CheckInfo info = checksApiFactory.revision(patchSetId).id(checkKey.checkerUuid()).rerun();
+    assertThat(info.state).isEqualTo(CheckState.NOT_STARTED);
+  }
+
+  @Test
+  public void rerunNonExistingCheckWithCheckerNotAppliedToChange() throws Exception {
+    Project.NameKey otherProject = projectOperations.newProject().create();
+    checkerOperations.checker(checkKey.checkerUuid()).forUpdate().repository(otherProject).update();
+    assertThrows(
+        ResourceNotFoundException.class,
+        () -> checksApiFactory.revision(patchSetId).id(checkKey.checkerUuid()).rerun());
+    assertThat(checkOperations.check(checkKey).exists()).isFalse();
+  }
+
+  @Test
+  public void cannotUpdateCheckWithoutAdministrateCheckers() throws Exception {
+    requestScopeOperations.setApiUser(user.id());
+    checkOperations.newCheck(checkKey).state(CheckState.SUCCESSFUL).upsert();
+
+    AuthException thrown =
+        assertThrows(
+            AuthException.class,
+            () -> checksApiFactory.revision(patchSetId).id(checkKey.checkerUuid()).rerun());
+    assertThat(thrown).hasMessageThat().contains("not permitted");
+  }
+
+  @Test
+  public void cannotUpdateCheckAnonymously() throws Exception {
+    requestScopeOperations.setApiUserAnonymous();
+    checkOperations.newCheck(checkKey).state(CheckState.SUCCESSFUL).upsert();
+
+    AuthException thrown =
+        assertThrows(
+            AuthException.class,
+            () -> checksApiFactory.revision(patchSetId).id(checkKey.checkerUuid()).rerun());
+    assertThat(thrown).hasMessageThat().contains("Authentication required");
+  }
+}
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 6d15caa..7cc2ce8 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/api/UpdateCheckIT.java
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/api/UpdateCheckIT.java
@@ -17,9 +17,11 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.plugins.checks.CheckKey;
 import com.google.gerrit.plugins.checks.CheckerUuid;
 import com.google.gerrit.plugins.checks.acceptance.AbstractCheckersTest;
@@ -42,6 +44,7 @@
 
 public class UpdateCheckIT extends AbstractCheckersTest {
   @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private ProjectOperations projectOperations;
 
   private PatchSet.Id patchSetId;
   private CheckKey checkKey;
@@ -156,6 +159,16 @@
   }
 
   @Test
+  public void unsetStarted() throws Exception {
+    checkOperations.check(checkKey).forUpdate().started(TimeUtil.nowTs()).upsert();
+
+    CheckInput input = new CheckInput();
+    input.started = TimeUtil.never();
+    CheckInfo info = checksApiFactory.revision(patchSetId).id(checkKey.checkerUuid()).update(input);
+    assertThat(info.started).isNull();
+  }
+
+  @Test
   public void updateFinished() throws Exception {
     CheckInput input = new CheckInput();
     input.finished = TimeUtil.nowTs();
@@ -165,6 +178,16 @@
   }
 
   @Test
+  public void unsetFinished() throws Exception {
+    checkOperations.check(checkKey).forUpdate().finished(TimeUtil.nowTs()).upsert();
+
+    CheckInput input = new CheckInput();
+    input.finished = TimeUtil.never();
+    CheckInfo info = checksApiFactory.revision(patchSetId).id(checkKey.checkerUuid()).update(input);
+    assertThat(info.finished).isNull();
+  }
+
+  @Test
   public void updateWithEmptyInput() throws Exception {
     assertThat(
             checksApiFactory
@@ -207,8 +230,9 @@
   }
 
   @Test
-  public void canUpdateCheckForCheckerThatDoesNotApplyToTheProject() throws Exception {
-    Project.NameKey otherProject = createProjectOverAPI("other", null, true, null);
+  public void canUpdateCheckForCheckerThatDoesNotApplyToTheProjectAndCheckExists()
+      throws Exception {
+    Project.NameKey otherProject = projectOperations.newProject().create();
     checkerOperations.checker(checkKey.checkerUuid()).forUpdate().repository(otherProject).update();
 
     CheckInput input = new CheckInput();
@@ -219,6 +243,22 @@
   }
 
   @Test
+  public void
+      throwExceptionForUpdateCheckForCheckerThatDoesNotApplyToTheProjectAndCheckDoesNotExist()
+          throws Exception {
+    Project.NameKey otherProject = projectOperations.newProject().create();
+    CheckerUuid checkerUuid = checkerOperations.newChecker().repository(otherProject).create();
+    CheckKey checkKey = CheckKey.create(otherProject, patchSetId, checkerUuid);
+    assertThrows(
+        ResourceNotFoundException.class,
+        () ->
+            checksApiFactory
+                .revision(patchSetId)
+                .id(checkKey.checkerUuid())
+                .update(new CheckInput()));
+  }
+
+  @Test
   public void canUpdateCheckForCheckerWithUnsupportedOperatorInQuery() throws Exception {
     checkerOperations
         .checker(checkKey.checkerUuid())
@@ -360,4 +400,56 @@
     assertThat(info.message).isEqualTo("some message");
     assertThat(info.url).isEqualTo("https://www.example.com");
   }
+
+  @Test
+  public void updateOfCheckChangesETagOfChange() throws Exception {
+    String oldETag = parseChangeResource(patchSetId.changeId().toString()).getETag();
+
+    CheckInput input = new CheckInput();
+    input.state = CheckState.FAILED;
+    checksApiFactory.revision(patchSetId).id(checkKey.checkerUuid()).update(input);
+
+    String newETag = parseChangeResource(patchSetId.changeId().toString()).getETag();
+    assertThat(newETag).isNotEqualTo(oldETag);
+  }
+
+  @Test
+  public void noOpUpdateOfCheckDoesNotChangeETagOfChange() throws Exception {
+    CheckInput input = new CheckInput();
+    input.state = CheckState.FAILED;
+    checksApiFactory.revision(patchSetId).id(checkKey.checkerUuid()).update(input);
+
+    String oldETag = parseChangeResource(patchSetId.changeId().toString()).getETag();
+
+    checksApiFactory.revision(patchSetId).id(checkKey.checkerUuid()).update(input);
+
+    String newETag = parseChangeResource(patchSetId.changeId().toString()).getETag();
+    assertThat(newETag).isEqualTo(oldETag);
+  }
+
+  @Test
+  public void updateOfCheckChangesETagOfRevisionActions() throws Exception {
+    String oldETag = gApi.changes().id(patchSetId.changeId().toString()).current().etag();
+
+    CheckInput input = new CheckInput();
+    input.state = CheckState.FAILED;
+    checksApiFactory.revision(patchSetId).id(checkKey.checkerUuid()).update(input);
+
+    String newETag = gApi.changes().id(patchSetId.changeId().toString()).current().etag();
+    assertThat(newETag).isNotEqualTo(oldETag);
+  }
+
+  @Test
+  public void noOpUpdateOfCheckDoesNotChangeETagOfRevisionActions() throws Exception {
+    CheckInput input = new CheckInput();
+    input.state = CheckState.FAILED;
+    checksApiFactory.revision(patchSetId).id(checkKey.checkerUuid()).update(input);
+
+    String oldETag = gApi.changes().id(patchSetId.changeId().toString()).current().etag();
+
+    checksApiFactory.revision(patchSetId).id(checkKey.checkerUuid()).update(input);
+
+    String newETag = gApi.changes().id(patchSetId.changeId().toString()).current().etag();
+    assertThat(newETag).isEqualTo(oldETag);
+  }
 }
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckOperationsImplTest.java b/javatests/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckOperationsImplTest.java
index 4e63bde..6458099 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckOperationsImplTest.java
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckOperationsImplTest.java
@@ -476,6 +476,18 @@
   }
 
   @Test
+  public void startedCanBeCleared() throws Exception {
+    CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
+    CheckKey checkKey = CheckKey.create(project, createChange().getPatchSetId(), checkerUuid);
+    checkOperations.newCheck(checkKey).started(TimeUtil.nowTs()).upsert();
+
+    checkOperations.check(checkKey).forUpdate().clearStarted().upsert();
+
+    Optional<Timestamp> currentStarted = checkOperations.check(checkKey).get().started();
+    assertThat(currentStarted).isEmpty();
+  }
+
+  @Test
   public void finishedCanBeUpdated() throws Exception {
     CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
     CheckKey checkKey = CheckKey.create(project, createChange().getPatchSetId(), checkerUuid);
@@ -489,6 +501,18 @@
   }
 
   @Test
+  public void finishedCanBeCleared() throws Exception {
+    CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
+    CheckKey checkKey = CheckKey.create(project, createChange().getPatchSetId(), checkerUuid);
+    checkOperations.newCheck(checkKey).finished(TimeUtil.nowTs()).upsert();
+
+    checkOperations.check(checkKey).forUpdate().clearFinished().upsert();
+
+    Optional<Timestamp> currentFinished = checkOperations.check(checkKey).get().finished();
+    assertThat(currentFinished).isEmpty();
+  }
+
+  @Test
   public void getNotesAsText() throws Exception {
     CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
     PatchSet.Id patchSetId = createChange().getPatchSetId();
diff --git a/javatests/com/google/gerrit/plugins/checks/rules/BUILD b/javatests/com/google/gerrit/plugins/checks/rules/BUILD
index 17fdb2a..2e76709 100644
--- a/javatests/com/google/gerrit/plugins/checks/rules/BUILD
+++ b/javatests/com/google/gerrit/plugins/checks/rules/BUILD
@@ -14,6 +14,7 @@
         "//lib:guava",
         "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/truth",
+        "//lib/truth:truth-java8-extension",
         "//plugins/checks:checks__plugin",
     ],
 )
diff --git a/javatests/com/google/gerrit/plugins/checks/rules/ChecksSubmitRuleTest.java b/javatests/com/google/gerrit/plugins/checks/rules/ChecksSubmitRuleTest.java
index afdda35..2d75e6b 100644
--- a/javatests/com/google/gerrit/plugins/checks/rules/ChecksSubmitRuleTest.java
+++ b/javatests/com/google/gerrit/plugins/checks/rules/ChecksSubmitRuleTest.java
@@ -15,22 +15,21 @@
 package com.google.gerrit.plugins.checks.rules;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
 import static org.easymock.EasyMock.anyObject;
 import static org.easymock.EasyMock.expect;
 import static org.easymock.EasyMock.replay;
 
-import com.google.common.collect.Iterables;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.plugins.checks.Checks;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.util.time.TimeUtil;
 import java.io.IOException;
-import java.util.Collection;
+import java.util.Optional;
 import org.easymock.EasyMock;
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Test;
@@ -47,8 +46,7 @@
     expect(cd.currentPatchSet()).andThrow(new IllegalStateException("Fail for test"));
     replay(cd);
 
-    Collection<SubmitRecord> submitRecords =
-        checksSubmitRule.evaluate(cd, SubmitRuleOptions.defaults());
+    Optional<SubmitRecord> submitRecords = checksSubmitRule.evaluate(cd);
     assertErrorRecord(submitRecords, "failed to load the current patch set of change 1");
   }
 
@@ -75,17 +73,15 @@
                 .build());
     replay(cd);
 
-    Collection<SubmitRecord> submitRecords =
-        checksSubmitRule.evaluate(cd, SubmitRuleOptions.defaults());
+    Optional<SubmitRecord> submitRecords = checksSubmitRule.evaluate(cd);
     assertErrorRecord(submitRecords, "failed to evaluate check states for change 1");
   }
 
   private static void assertErrorRecord(
-      Collection<SubmitRecord> submitRecords, String expectedErrorMessage) {
-    assertThat(submitRecords).hasSize(1);
+      Optional<SubmitRecord> submitRecord, String expectedErrorMessage) {
+    assertThat(submitRecord).isPresent();
 
-    SubmitRecord submitRecord = Iterables.getOnlyElement(submitRecords);
-    assertThat(submitRecord.status).isEqualTo(SubmitRecord.Status.RULE_ERROR);
-    assertThat(submitRecord.errorMessage).isEqualTo(expectedErrorMessage);
+    assertThat(submitRecord.get().status).isEqualTo(SubmitRecord.Status.RULE_ERROR);
+    assertThat(submitRecord.get().errorMessage).isEqualTo(expectedErrorMessage);
   }
 }
diff --git a/resources/Documentation/rest-api-checks.md b/resources/Documentation/rest-api-checks.md
index b34ed31..4395644 100644
--- a/resources/Documentation/rest-api-checks.md
+++ b/resources/Documentation/rest-api-checks.md
@@ -1,4 +1,4 @@
-# /changes/<id>/revisions/<id>/checks/ REST API
+# /changes/`id`/revisions/`id`/checks/ REST API
 
 This page describes the check-related REST endpoints that are added by the
 @PLUGIN@ plugin.
@@ -143,6 +143,7 @@
 
 ### <a id="update-check"> Update Check
 _'POST /changes/1/revisions/1/checks/'_
+
 _'POST /changes/1/revisions/1/checks/test:my-checker'_
 
 Updates a check. The semantics are the same as for [CreateCheck](#create-check).
@@ -156,27 +157,39 @@
 the URL, it must either match the value provided in the request body via
 [CheckInput](#check-input) or the value in the request body is omitted.
 
+### <a id="rerun-check"> Rerun Check
+
+_'POST /changes/1/revisions/1/checks/test:my-checker/rerun'_
+
+Reruns a check. As response the [CheckInfo](#check-info) entity is returned that
+describes the created check.
+
+
+This REST endpoint supports rerunning a check. It also resets all relevant check
+fields such as `message`, `url`, `started` and `finished`.
+
 ## <a id="json-entities"> JSON Entities
 
 ### <a id="check-info"> CheckInfo
 The `CheckInfo` entity describes a check.
 
-| Field Name        |          | Description |
-| ----------------- | -------- | ----------- |
-| `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.
-| `state`           |          | The state as string-serialized form of [CheckState](#check-state)
-| `message`         | optional | Short message explaining the check state.
-| `url`             | optional | A fully-qualified URL pointing to the result of the check on the checker's infrastructure.
-| `started`         | optional | The [timestamp](../../../Documentation/rest-api.html#timestamp) of when the check started processing.
-| `finished`        | optional | The [timestamp](../../../Documentation/rest-api.html#timestamp) of when the check finished processing.
-| `created`         |          | The [timestamp](../../../Documentation/rest-api.html#timestamp) of when the check was created.
-| `updated`         |          | The [timestamp](../../../Documentation/rest-api.html#timestamp) of when the check was last updated.
-| `checker_name`    | optional | The name of the checker that produced this check.<br />Only set if [checker details](#option-checker) are requested.
-| `checker_status`  | optional | The [status](rest-api-checkers.md#checker-info) of the checker that produced this check.<br />Only set if [checker details](#option-checker) are requested.
-| `blocking`        | optional | Set of [blocking conditions](rest-api-checkers.md#blocking-conditions) that apply to this checker.<br />Only set if [checker details](#option-checker) are requested.
+| Field Name            |          | Description |
+| --------------------- | -------- | ----------- |
+| `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.
+| `state`               |          | The state as string-serialized form of [CheckState](#check-state)
+| `message`             | optional | Short message explaining the check state.
+| `url`                 | optional | A fully-qualified URL pointing to the result of the check on the checker's infrastructure.
+| `started`             | optional | The [timestamp](../../../Documentation/rest-api.html#timestamp) of when the check started processing.
+| `finished`            | optional | The [timestamp](../../../Documentation/rest-api.html#timestamp) of when the check finished processing.
+| `created`             |          | The [timestamp](../../../Documentation/rest-api.html#timestamp) of when the check was created.
+| `updated`             |          | The [timestamp](../../../Documentation/rest-api.html#timestamp) of when the check was last updated.
+| `checker_name`        | optional | The name of the checker that produced this check.<br />Only set if [checker details](#option-checker) are requested.
+| `checker_status`      | optional | The [status](rest-api-checkers.md#checker-info) of the checker that produced this check.<br />Only set if [checker details](#option-checker) are requested.
+| `blocking`            | optional | Set of [blocking conditions](rest-api-checkers.md#blocking-conditions) that apply to this checker.<br />Only set if [checker details](#option-checker) are requested.
+| `checker_description` | optional | The description of the checker that reported this check.
 
 ### <a id="check-input"> CheckInput
 The `CheckInput` entity contains information for creating or updating a check.