Merge branch 'stable-3.3'

* stable-3.3:
  Fix test due to change in Gerrit core
  Index change when combined state changes
  ChecksSubmitRule.Module: Demote base class to AbstractModule

Change-Id: Ie14245216b2a73f6c82ab242b89c4234e9bb408f
diff --git a/gr-checks/gr-checks-all-statuses.js b/gr-checks/gr-checks-all-statuses.js
index bdaa06f..2aaed0b 100644
--- a/gr-checks/gr-checks-all-statuses.js
+++ b/gr-checks/gr-checks-all-statuses.js
@@ -44,7 +44,15 @@
  * @returns {boolean} if status is Unevaluated.
  */
 export function isUnevaluated(status) {
-  return isStatus(status, [Statuses.NOT_STARTED, Statuses.NOT_RELEVANT]);
+  return isStatus(status, [Statuses.NOT_STARTED]);
+}
+
+/**
+ * @param {string} status
+ * @returns {boolean} if status is Not Relevant
+ */
+export function isNotRelevant(status) {
+  return isStatus(status, [Statuses.NOT_RELEVANT]);
 }
 
 /**
diff --git a/gr-checks/gr-checks-item.js b/gr-checks/gr-checks-item.js
index 494a330..40c4d77 100644
--- a/gr-checks/gr-checks-item.js
+++ b/gr-checks/gr-checks-item.js
@@ -225,21 +225,21 @@
   if (secondsAgo % 60 !== 0) {
     durationSegments.push(`${secondsAgo % 60} sec`);
   }
-  const minutesAgo = Math.round(secondsAgo / 60);
+  const minutesAgo = Math.floor(secondsAgo / 60);
   if (minutesAgo % 60 !== 0) {
     durationSegments.push(`${minutesAgo % 60} min`);
   }
-  const hoursAgo = Math.round(minutesAgo / 60);
+  const hoursAgo = Math.floor(minutesAgo / 60);
   if (hoursAgo % 24 !== 0) {
     const hours = pluralize(hoursAgo % 24, 'hour', 'hours');
     durationSegments.push(`${hoursAgo % 24} ${hours}`);
   }
-  const daysAgo = Math.round(hoursAgo / 24);
+  const daysAgo = Math.floor(hoursAgo / 24);
   if (daysAgo % 30 !== 0) {
     const days = pluralize(daysAgo % 30, 'day', 'days');
     durationSegments.push(`${daysAgo % 30} ${days}`);
   }
-  const monthsAgo = Math.round(daysAgo / 30);
+  const monthsAgo = Math.floor(daysAgo / 30);
   if (monthsAgo > 0) {
     const months = pluralize(monthsAgo, 'month', 'months');
     durationSegments.push(`${monthsAgo} ${months}`);
@@ -255,4 +255,4 @@
  */
 function pluralize(unit, singular, plural) {
   return unit === 1 ? singular : plural;
-}
\ No newline at end of file
+}
diff --git a/gr-checks/gr-checks-item_test.html b/gr-checks/gr-checks-item_test.html
index e9ce401..305d965 100644
--- a/gr-checks/gr-checks-item_test.html
+++ b/gr-checks/gr-checks-item_test.html
@@ -79,6 +79,42 @@
             .querySelector(`td:nth-of-type(${idx})`);
         assert.equal(name.textContent.trim(), '0 sec');
       });
+
+      test('renders > 1m correctly', () => {
+        element.check = {
+          checkId: 'test-check-id',
+           started: '2019-02-06T22:25:19.269Z',
+          finished: '2019-02-06T22:45:29.269Z',
+        };
+        const idx = CHECKS_ITEM.DURATION;
+        const name = element.shadowRoot
+            .querySelector(`td:nth-of-type(${idx})`);
+        assert.equal(name.textContent.trim(), '20 min 10 sec');
+      });
+
+      test('renders > 1h correctly', () => {
+        element.check = {
+          checkId: 'test-check-id',
+           started: '2019-02-06T22:25:19.269Z',
+          finished: '2019-02-06T23:45:29.269Z',
+        };
+        const idx = CHECKS_ITEM.DURATION;
+        const name = element.shadowRoot
+            .querySelector(`td:nth-of-type(${idx})`);
+        assert.equal(name.textContent.trim(), '1 hour 20 min');
+      });
+
+      test('renders > 1d correctly', () => {
+        element.check = {
+          checkId: 'test-check-id',
+           started: '2019-02-06T22:25:19.269Z',
+          finished: '2019-02-07T23:45:29.269Z',
+        };
+        const idx = CHECKS_ITEM.DURATION;
+        const name = element.shadowRoot
+            .querySelector(`td:nth-of-type(${idx})`);
+        assert.equal(name.textContent.trim(), '1 day 1 hour');
+      });
     });
 
     test('renders a link to the log', () => {
diff --git a/gr-checks/gr-checks-reboot.js b/gr-checks/gr-checks-reboot.js
new file mode 100644
index 0000000..1e7bdc2
--- /dev/null
+++ b/gr-checks/gr-checks-reboot.js
@@ -0,0 +1,164 @@
+/**
+ * @license
+ * Copyright (C) 2020 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.
+ */
+
+/**
+ * Heads up! Everything in this file is still in flux. The new reboot checks API
+ * is still in development. So everything in this file can change. And it is
+ * expected that the amount of comments and tests is limited for the time being.
+ */
+
+function pluralize(count, noun) {
+  if (count === 0) return '';
+  return `${count} ${noun}` + (count > 1 ? 's' : '');
+}
+
+function generateDurationString(startTime, endTime) {
+  const secondsAgo = Math.round((endTime - startTime) / 1000);
+
+  if (secondsAgo === 0) {
+    return ZERO_SECONDS;
+  }
+
+  const durationSegments = [];
+  if (secondsAgo % 60 !== 0) {
+    durationSegments.push(`${secondsAgo % 60} sec`);
+  }
+  const minutesAgo = Math.floor(secondsAgo / 60);
+  if (minutesAgo % 60 !== 0) {
+    durationSegments.push(`${minutesAgo % 60} min`);
+  }
+  const hoursAgo = Math.floor(minutesAgo / 60);
+  if (hoursAgo % 24 !== 0) {
+    const hours = pluralize(hoursAgo % 24, 'hour', 'hours');
+    durationSegments.push(`${hours}`);
+  }
+  const daysAgo = Math.floor(hoursAgo / 24);
+  if (daysAgo % 30 !== 0) {
+    const days = pluralize(daysAgo % 30, 'day', 'days');
+    durationSegments.push(`${days}`);
+  }
+  const monthsAgo = Math.floor(daysAgo / 30);
+  if (monthsAgo > 0) {
+    const months = pluralize(monthsAgo, 'month', 'months');
+    durationSegments.push(`${months}`);
+  }
+  return durationSegments.reverse().slice(0, 2).join(' ');
+}
+
+function computeDuration(check) {
+  if (!check.started || !check.finished) {
+    return '-';
+  }
+  const startTime = new Date(check.started);
+  const finishTime = check.finished ? new Date(check.finished) : new Date();
+  return generateDurationString(startTime, finishTime);
+}
+
+export class RebootFetcher {
+  constructor(restApi) {
+    this.restApi = restApi;
+  }
+
+  async fetchCurrent() {
+    return this.fetch(this.changeNumber, this.patchsetNumber);
+  }
+
+  async fetch(changeData) {
+    const {changeNumber, patchsetNumber} = changeData;
+    this.changeNumber = changeNumber;
+    this.patchsetNumber = patchsetNumber;
+    const checks = await this.apiGet('?o=CHECKER');
+    return {
+      responseCode: 'OK',
+      runs: checks.map(check => this.convert(check)),
+    };
+  }
+
+  async apiGet(suffix) {
+    return this.restApi.get(
+        '/changes/' + this.changeNumber + '/revisions/' + this.patchsetNumber
+        + '/checks' + suffix);
+  }
+
+  async apiPost(suffix) {
+    return this.restApi.post(
+        '/changes/' + this.changeNumber + '/revisions/' + this.patchsetNumber
+        + '/checks' + suffix);
+  }
+
+  /**
+   * Converts a Checks Plugin CheckInfo object into a Reboot Checks API Run
+   * object.
+   *
+   * TODO(brohlfs): Refine this conversion and add tests.
+   */
+  convert(check) {
+    let status = 'RUNNABLE';
+    if (check.state === 'RUNNING' || check.state === 'SCHEDULED') {
+      status = 'RUNNING';
+    } else if (check.state === 'FAILED' || check.state === 'SUCCESSFUL') {
+      status = 'COMPLETED';
+    }
+    const run = {
+      checkName: check.checker_name,
+      checkDescription: check.checker_description,
+      externalId: check.checker_uuid,
+      status,
+    };
+    if (check.started) run.startedTimestamp = new Date(check.started);
+    if (check.finished) run.finishedTimestamp = new Date(check.finished);
+    if (status === 'RUNNING') {
+      run.statusDescription = check.message;
+      if (check.url) {
+        run.statusLink = check.url;
+      }
+    } else if (check.state === 'SUCCESSFUL') {
+      run.statusDescription =
+          check.message || `Passed (${computeDuration(check)})`;
+      if (check.url) {
+        run.statusLink = check.url;
+      }
+    } else if (check.state === 'FAILED') {
+      run.results = [{
+        category: 'ERROR',
+        summary: check.message || `Failed (${computeDuration(check)})`,
+      }];
+      if (check.url) {
+        run.results[0].links = [{
+          url: check.url,
+          primary: true,
+          icon: 'EXTERNAL',
+        }];
+      }
+    }
+    if (status !== 'RUNNING') {
+      run.actions = [{
+        name: 'Run',
+        primary: true,
+        callback: () => this.run(check.checker_uuid),
+      }];
+    }
+    return run;
+  }
+
+  run(uuid) {
+    return this.apiPost('/' + uuid + '/rerun')
+        .catch(e => {
+          return {errorMessage: `Triggering the run failed: ${e.message}`};
+        });
+  }
+}
diff --git a/gr-checks/gr-checks-status.js b/gr-checks/gr-checks-status.js
index 3a22f2e..557e81c 100644
--- a/gr-checks/gr-checks-status.js
+++ b/gr-checks/gr-checks-status.js
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 import {htmlTemplate} from './gr-checks-status_html.js';
-import {isUnevaluated, isInProgress, isRunning, isScheduled, isSuccessful, isFailed} from './gr-checks-all-statuses.js';
+import {isNotRelevant, isUnevaluated, isInProgress, isRunning, isScheduled, isSuccessful, isFailed} from './gr-checks-all-statuses.js';
 
 class GrChecksStatus extends Polymer.GestureEventListeners(
     Polymer.LegacyElementMixin(
@@ -43,6 +43,10 @@
     };
   }
 
+  _isNotRelevant(status) {
+    return isNotRelevant(status);
+  }
+
   _isUnevaluated(status) {
     return isUnevaluated(status);
   }
diff --git a/gr-checks/gr-checks-status_html.js b/gr-checks/gr-checks-status_html.js
index 0325b95..5cca639 100644
--- a/gr-checks/gr-checks-status_html.js
+++ b/gr-checks/gr-checks-status_html.js
@@ -36,6 +36,19 @@
         }
       </style>
     <span>
+      <template is="dom-if" if="[[_isNotRelevant(status)]]">
+        <svg width="18" height="18" xmlns="http://www.w3.org/2000/svg">
+          <g fill="none" fill-rule="evenodd">
+            <path d="M0 0h18v18H0z"/>
+            <path d="M9 11.8a2.8 2.8 0 1 1 0-5.6 2.8 2.8 0 0 1 0 5.6M9 2a7 7 0 1 0 0 14A7 7 0 0 0 9 2" fill="#9E9E9E"/>
+          </g>
+        </svg>
+        <template is="dom-if" if="[[showText]]">
+          <span>
+            Not relevant
+          </span>
+        </template>
+      </template>
       <template is="dom-if" if="[[_isUnevaluated(status)]]">
         <svg width="18" height="18" xmlns="http://www.w3.org/2000/svg">
           <g fill="none" fill-rule="evenodd">
diff --git a/gr-checks/gr-checks.js b/gr-checks/gr-checks.js
index b8c2920..39152f5 100644
--- a/gr-checks/gr-checks.js
+++ b/gr-checks/gr-checks.js
@@ -21,8 +21,9 @@
 import './gr-checks-change-list-item-cell-view.js';
 import './gr-checks-item.js';
 import './gr-checks-status.js';
+import {RebootFetcher} from './gr-checks-reboot.js';
 
-Gerrit.install(plugin => {
+function installChecksLegacy(plugin) {
   const getChecks = (change, revision) => {
     return plugin.restApi().get(
         '/changes/' + change + '/revisions/' + revision + '/checks?o=CHECKER');
@@ -54,4 +55,24 @@
         view['isConfigured'] = repository => Promise.resolve(true);
         view['getChecks'] = getChecks;
       });
-});
\ No newline at end of file
+}
+
+function installChecksReboot(plugin) {
+  const checksApi = plugin.checks();
+  const fetcher = new RebootFetcher(plugin.restApi());
+  checksApi.register({
+    fetch: (data) => fetcher.fetch(data)
+  });
+}
+
+Gerrit.install(plugin => {
+  const experiments = window.ENABLED_EXPERIMENTS || [];
+  if (experiments.includes("UiFeature__ci_reboot_checks_checks")) {
+    // Until end of 2020 this is only interesting for developing purposes. So
+    // no real user is affected for the time being.
+    console.log('Installing checks REBOOT plugin.');
+    installChecksReboot(plugin);
+  } else {
+    installChecksLegacy(plugin);
+  }
+});
diff --git a/java/com/google/gerrit/plugins/checks/Module.java b/java/com/google/gerrit/plugins/checks/Module.java
index 3431ec1..4787df6 100644
--- a/java/com/google/gerrit/plugins/checks/Module.java
+++ b/java/com/google/gerrit/plugins/checks/Module.java
@@ -31,8 +31,8 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.UserInitiated;
-import com.google.gerrit.server.change.ChangeAttributeFactory;
 import com.google.gerrit.server.change.ChangeETagComputation;
+import com.google.gerrit.server.change.ChangePluginDefinedInfoFactory;
 import com.google.gerrit.server.git.validators.CommitValidationListener;
 import com.google.gerrit.server.git.validators.MergeValidationListener;
 import com.google.gerrit.server.git.validators.RefOperationValidationListener;
@@ -67,7 +67,8 @@
         .to(ChecksETagComputation.class)
         .in(SINGLETON);
 
-    DynamicSet.bind(binder(), ChangeAttributeFactory.class).to(ChangeCheckAttributeFactory.class);
+    DynamicSet.bind(binder(), ChangePluginDefinedInfoFactory.class)
+        .to(ChangeCheckAttributeFactory.class);
     bind(DynamicOptions.DynamicBean.class)
         .annotatedWith(Exports.named(GetChange.class))
         .to(GetChangeOptions.class);
diff --git a/java/com/google/gerrit/plugins/checks/api/ChangeCheckAttributeFactory.java b/java/com/google/gerrit/plugins/checks/api/ChangeCheckAttributeFactory.java
index 9c48851..f456279 100644
--- a/java/com/google/gerrit/plugins/checks/api/ChangeCheckAttributeFactory.java
+++ b/java/com/google/gerrit/plugins/checks/api/ChangeCheckAttributeFactory.java
@@ -14,13 +14,22 @@
 
 package com.google.gerrit.plugins.checks.api;
 
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.extensions.common.PluginDefinedInfo;
 import com.google.gerrit.plugins.checks.CombinedCheckStateCache;
 import com.google.gerrit.server.DynamicOptions.BeanProvider;
 import com.google.gerrit.server.DynamicOptions.DynamicBean;
-import com.google.gerrit.server.change.ChangeAttributeFactory;
+import com.google.gerrit.server.change.ChangePluginDefinedInfoFactory;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import java.util.AbstractMap;
+import java.util.AbstractMap.SimpleImmutableEntry;
+import java.util.Collection;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 import org.kohsuke.args4j.Option;
 
 /**
@@ -28,7 +37,7 @@
  * a {@code ChangeInfo}.
  */
 @Singleton
-public class ChangeCheckAttributeFactory implements ChangeAttributeFactory {
+public class ChangeCheckAttributeFactory implements ChangePluginDefinedInfoFactory {
   private static final String COMBINED_OPTION_NAME = "--combined";
   private static final String COMBINED_OPTION_USAGE = "include combined check state";
 
@@ -50,19 +59,37 @@
   }
 
   @Override
-  public ChangeCheckInfo create(ChangeData cd, BeanProvider beanProvider, String plugin) {
+  public Map<Change.Id, PluginDefinedInfo> createPluginDefinedInfos(
+      Collection<ChangeData> cds, BeanProvider beanProvider, String plugin) {
     DynamicBean opts = beanProvider.getDynamicBean(plugin);
     if (opts == null) {
-      return null;
+      return ImmutableMap.of();
     }
     if (opts instanceof GetChangeOptions) {
-      return forGetChange(cd, (GetChangeOptions) opts);
+      return evalAndCollect(
+          cds,
+          cd ->
+              new AbstractMap.SimpleImmutableEntry<>(
+                  cd.getId(), forGetChange(cd, (GetChangeOptions) opts)));
     } else if (opts instanceof QueryChangesOptions) {
-      return forQueryChanges(cd, (QueryChangesOptions) opts);
+      return evalAndCollect(
+          cds,
+          cd ->
+              new AbstractMap.SimpleImmutableEntry<>(
+                  cd.getId(), forQueryChanges(cd, (QueryChangesOptions) opts)));
     }
     throw new IllegalStateException("unexpected options type: " + opts);
   }
 
+  private Map<Change.Id, PluginDefinedInfo> evalAndCollect(
+      Collection<ChangeData> cds,
+      Function<ChangeData, SimpleImmutableEntry<Change.Id, ChangeCheckInfo>> transformFn) {
+    return cds.stream()
+        .map(transformFn)
+        .filter(e -> e.getKey() != null && e.getValue() != null)
+        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+  }
+
   private ChangeCheckInfo forGetChange(ChangeData cd, GetChangeOptions opts) {
     if (opts == null || !opts.combined) {
       return null;
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/CheckerRefsIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/CheckerRefsIT.java
index abb015a..df918fa 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/CheckerRefsIT.java
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/CheckerRefsIT.java
@@ -30,7 +30,6 @@
 import com.google.gerrit.entities.Permission;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.client.ChangeStatus;
-import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.plugins.checks.CheckerRef;
 import com.google.gerrit.plugins.checks.CheckerUuid;
@@ -305,52 +304,6 @@
     r.assertOkStatus();
   }
 
-  @Test
-  public void createChangeForCheckerRefsViaApiIsDisabled() throws Exception {
-    CheckerUuid checkerUuid = checkerOperations.newChecker().create();
-    String checkerRef = checkerUuid.toRefName();
-
-    TestRepository<InMemoryRepository> repo = cloneProject(allProjects, admin);
-    fetch(repo, checkerRef + ":checkerRef");
-    repo.reset("checkerRef");
-    RevCommit head = getHead(repo.getRepository(), "HEAD");
-
-    ChangeInput input = new ChangeInput();
-    input.project = allProjects.get();
-    input.branch = checkerRef;
-    input.baseCommit = head.name();
-    input.subject = "A change.";
-
-    ResourceConflictException thrown =
-        assertThrows(ResourceConflictException.class, () -> gApi.changes().create(input));
-    assertThat(thrown).hasMessageThat().contains("creating change for checker ref not allowed");
-  }
-
-  @Test
-  public void createChangeForCheckerLikeRefViaApi() throws Exception {
-    String checkerRef = CheckerUuid.parse("foo:bar").toRefName();
-
-    projectOperations
-        .project(project)
-        .forUpdate()
-        .add(allow(Permission.CREATE).ref(checkerRef).group(adminGroupUuid()))
-        .update();
-    createBranch(BranchNameKey.create(project, checkerRef));
-
-    TestRepository<InMemoryRepository> repo = cloneProject(project, admin);
-    fetch(repo, checkerRef + ":checkerRef");
-    repo.reset("checkerRef");
-    RevCommit head = getHead(repo.getRepository(), "HEAD");
-
-    // creating a change on a checker ref via API should work in any project except All-Projects
-    ChangeInput input = new ChangeInput();
-    input.project = project.get();
-    input.branch = checkerRef;
-    input.baseCommit = head.name();
-    input.subject = "A change.";
-    assertThat(gApi.changes().create(input).get()).isNotNull();
-  }
-
   private String createChangeWithoutCommitValidation(Project.NameKey project, String targetRef)
       throws Exception {
     try (Repository git = repoManager.openRepository(project);
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 b1e0273..9c554ed 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/api/CreateCheckIT.java
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/api/CreateCheckIT.java
@@ -383,21 +383,6 @@
     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/UpdateCheckIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/api/UpdateCheckIT.java
index c763abc..3e16afc 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/api/UpdateCheckIT.java
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/api/UpdateCheckIT.java
@@ -443,30 +443,4 @@
     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/resources/com/google/gerrit/plugins/checks/email/CombinedCheckStateUpdated.soy b/resources/com/google/gerrit/plugins/checks/email/CombinedCheckStateUpdated.soy
index 5e914ed..cd57453 100644
--- a/resources/com/google/gerrit/plugins/checks/email/CombinedCheckStateUpdated.soy
+++ b/resources/com/google/gerrit/plugins/checks/email/CombinedCheckStateUpdated.soy
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-{namespace com.google.gerrit.server.mail.template}
+{namespace com.google.gerrit.server.mail.template.CombinedCheckStateUpdated}
 
 /**
  * The .CombinedCheckStateUpdated template will determine the contents of the email related to a
diff --git a/resources/com/google/gerrit/plugins/checks/email/CombinedCheckStateUpdatedHtml.soy b/resources/com/google/gerrit/plugins/checks/email/CombinedCheckStateUpdatedHtml.soy
index 8feb6ff..0f35c40 100644
--- a/resources/com/google/gerrit/plugins/checks/email/CombinedCheckStateUpdatedHtml.soy
+++ b/resources/com/google/gerrit/plugins/checks/email/CombinedCheckStateUpdatedHtml.soy
@@ -14,7 +14,9 @@
  * limitations under the License.
  */
 
-{namespace com.google.gerrit.server.mail.template}
+{namespace com.google.gerrit.server.mail.template.CombinedCheckStateUpdatedHtml}
+
+import * as mailTemplate from 'com/google/gerrit/server/mail/Private.soy';
 
 /**
  * The .CombinedCheckStateUpdatedHtml template will determine the contents of the email related to
@@ -91,7 +93,7 @@
 
   {if $email.changeUrl}
     <p>
-      {call .ViewChangeButton data="all" /}
+      {call mailTemplate.ViewChangeButton data="all" /}
     </p>
   {/if}
 {/template}