Merge branch 'stable-3.1'

* stable-3.1:
  Format files with google-java-format

Change-Id: I5617d4ebf7b283a334a77c79fdb7379dd1e87896
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/ItsHookModule.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/ItsHookModule.java
index dc6f4a5..8835389 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/ItsHookModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/ItsHookModule.java
@@ -38,9 +38,11 @@
 import com.googlesource.gerrit.plugins.its.base.workflow.Condition;
 import com.googlesource.gerrit.plugins.its.base.workflow.CreateVersionFromProperty;
 import com.googlesource.gerrit.plugins.its.base.workflow.CustomAction;
+import com.googlesource.gerrit.plugins.its.base.workflow.FireEventOnCommits;
 import com.googlesource.gerrit.plugins.its.base.workflow.ItsRulesProjectCacheImpl;
 import com.googlesource.gerrit.plugins.its.base.workflow.LogEvent;
 import com.googlesource.gerrit.plugins.its.base.workflow.Rule;
+import com.googlesource.gerrit.plugins.its.base.workflow.commit_collector.SinceLastTagCommitCollector;
 import java.nio.file.Path;
 
 public class ItsHookModule extends FactoryModule {
@@ -78,6 +80,8 @@
     factory(AddPropertyToField.Factory.class);
     DynamicMap.mapOf(binder(), CustomAction.class);
     install(ItsRulesProjectCacheImpl.module());
+    factory(FireEventOnCommits.Factory.class);
+    factory(SinceLastTagCommitCollector.Factory.class);
   }
 
   @Provides
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/its/ItsConfig.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/its/ItsConfig.java
index 08f73c5..20c6c4a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/its/ItsConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/its/ItsConfig.java
@@ -94,16 +94,17 @@
   }
 
   public boolean isEnabled(Project.NameKey projectNK, String refName) {
-    ProjectState projectState = projectCache.get(projectNK);
-    if (projectState == null) {
+    Optional<ProjectState> projectState = projectCache.get(projectNK);
+    if (!projectState.isPresent()) {
       log.error(
           "Failed to check if {} is enabled for project {}: Project not found",
           pluginName,
           projectNK.get());
       return false;
     }
-    return isEnforcedByAnyParentProject(refName, projectState)
-        || (isEnabledForProject(projectState) && isEnabledForBranch(projectState, refName));
+    return isEnforcedByAnyParentProject(refName, projectState.get())
+        || (isEnabledForProject(projectState.get())
+            && isEnabledForBranch(projectState.get(), refName));
   }
 
   private boolean isEnforcedByAnyParentProject(String refName, ProjectState projectState) {
@@ -147,12 +148,14 @@
 
   // Project association
   public Optional<String> getItsProjectName(Project.NameKey projectNK) {
-    ProjectState projectState = projectCache.get(projectNK);
-    if (projectState == null) {
+    Optional<ProjectState> projectState = projectCache.get(projectNK);
+    if (!projectState.isPresent()) {
       return Optional.empty();
     }
     return Optional.ofNullable(
-        pluginCfgFactory.getFromProjectConfig(projectState, pluginName).getString("its-project"));
+        pluginCfgFactory
+            .getFromProjectConfig(projectState.get(), pluginName)
+            .getString("its-project"));
   }
 
   // Issue association --------------------------------------------------------
@@ -266,7 +269,7 @@
   private List<CommentLinkInfo> getCommentLinkInfo(final String commentlinkName) {
     NameKey projectName = currentProjectName.get();
     if (projectName != null) {
-      List<CommentLinkInfo> commentlinks = projectCache.get(projectName).getCommentLinks();
+      List<CommentLinkInfo> commentlinks = projectCache.get(projectName).get().getCommentLinks();
       return commentlinks.stream()
           .filter(input -> input.name.equals(commentlinkName))
           .collect(toList());
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/util/PropertyExtractor.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/util/PropertyExtractor.java
index 926db56..bf9dc0c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/util/PropertyExtractor.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/util/PropertyExtractor.java
@@ -34,6 +34,7 @@
 import com.google.gerrit.server.events.WorkInProgressStateChangedEvent;
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.its.base.workflow.RefEventProperties;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
@@ -230,16 +231,33 @@
       associations = extractFrom((WorkInProgressStateChangedEvent) event, common);
     }
 
-    Set<Map<String, String>> ret = new HashSet<>();
-    if (associations != null) {
-      for (Entry<String, Set<String>> assoc : associations.entrySet()) {
-        Map<String, String> properties = new HashMap<>();
-        properties.put("issue", assoc.getKey());
-        properties.put("association", String.join(" ", assoc.getValue()));
-        properties.putAll(common);
-        ret.add(properties);
-      }
+    Set<Map<String, String>> issuesProperties = extractIssuesProperties(common, associations);
+
+    Map<String, String> projectProperties = new HashMap<>(common);
+    projectProperties.put("source", "gerrit");
+    return new RefEventProperties(projectProperties, issuesProperties);
+  }
+
+  public Set<Map<String, String>> extractIssuesProperties(
+      Map<String, String> commonProperties, Map<String, Set<String>> associations) {
+    if (associations == null) {
+      return Collections.emptySet();
     }
-    return new RefEventProperties(common, ret);
+
+    Set<Map<String, String>> issuesProperties = new HashSet<>();
+    Map<String, String> completedCommonProperties = new HashMap<>(commonProperties);
+    if (!completedCommonProperties.containsKey("source")) {
+      completedCommonProperties.put("source", "its");
+    }
+
+    for (Entry<String, Set<String>> assoc : associations.entrySet()) {
+      Map<String, String> properties = new HashMap<>();
+      properties.put("issue", assoc.getKey());
+      properties.put("association", String.join(" ", assoc.getValue()));
+      properties.putAll(completedCommonProperties);
+      issuesProperties.add(properties);
+    }
+
+    return issuesProperties;
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionExecutor.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionExecutor.java
index b8682ad..44f5d73 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionExecutor.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionExecutor.java
@@ -36,6 +36,7 @@
   private final LogEvent.Factory logEventFactory;
   private final AddPropertyToField.Factory addPropertyToFieldFactory;
   private final CreateVersionFromProperty.Factory createVersionFromPropertyFactory;
+  private final FireEventOnCommits.Factory fireEventOnCommitsFactory;
   private final DynamicMap<CustomAction> customActions;
 
   @Inject
@@ -47,6 +48,7 @@
       LogEvent.Factory logEventFactory,
       AddPropertyToField.Factory addPropertyToFieldFactory,
       CreateVersionFromProperty.Factory createVersionFromPropertyFactory,
+      FireEventOnCommits.Factory fireEventOnCommitsFactory,
       DynamicMap<CustomAction> customActions) {
     this.itsFactory = itsFactory;
     this.addCommentFactory = addCommentFactory;
@@ -55,6 +57,7 @@
     this.logEventFactory = logEventFactory;
     this.addPropertyToFieldFactory = addPropertyToFieldFactory;
     this.createVersionFromPropertyFactory = createVersionFromPropertyFactory;
+    this.fireEventOnCommitsFactory = fireEventOnCommitsFactory;
     this.customActions = customActions;
   }
 
@@ -72,6 +75,8 @@
         return addPropertyToFieldFactory.create();
       case "create-version-from-property":
         return createVersionFromPropertyFactory.create();
+      case "fire-event-on-commits":
+        return fireEventOnCommitsFactory.create();
       default:
         return customActions.get(PluginName.GERRIT, actionName);
     }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/FireEventOnCommits.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/FireEventOnCommits.java
new file mode 100644
index 0000000..7ade76b
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/FireEventOnCommits.java
@@ -0,0 +1,89 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.its.base.workflow;
+
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.its.base.its.ItsFacade;
+import com.googlesource.gerrit.plugins.its.base.util.IssueExtractor;
+import com.googlesource.gerrit.plugins.its.base.util.PropertyExtractor;
+import com.googlesource.gerrit.plugins.its.base.workflow.commit_collector.CommitCollector;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/** Fires the triggering event on collected past commits */
+public class FireEventOnCommits extends ProjectAction {
+
+  public interface Factory {
+    FireEventOnCommits create();
+  }
+
+  private final PropertyExtractor propertyExtractor;
+  private final IssueExtractor issueExtractor;
+  private final RuleBase ruleBase;
+  private final ActionExecutor actionExecutor;
+  private final FireEventOnCommitsParametersExtractor parametersExtractor;
+
+  @Inject
+  public FireEventOnCommits(
+      PropertyExtractor propertyExtractor,
+      IssueExtractor issueExtractor,
+      RuleBase ruleBase,
+      ActionExecutor actionExecutor,
+      FireEventOnCommitsParametersExtractor parametersExtractor) {
+    this.propertyExtractor = propertyExtractor;
+    this.issueExtractor = issueExtractor;
+    this.ruleBase = ruleBase;
+    this.actionExecutor = actionExecutor;
+    this.parametersExtractor = parametersExtractor;
+  }
+
+  @Override
+  public void execute(
+      ItsFacade its, String itsProject, ActionRequest actionRequest, Map<String, String> properties)
+      throws IOException {
+    Optional<FireEventOnCommitsParameters> extractedParameters =
+        parametersExtractor.extract(actionRequest, properties);
+    if (!extractedParameters.isPresent()) {
+      return;
+    }
+
+    CommitCollector commitCollector = extractedParameters.get().getCommitCollector();
+    String projectName = extractedParameters.get().getProjectName();
+
+    Set<Map<String, String>> issuesProperties =
+        commitCollector.collect(properties).stream()
+            .map(commitId -> issueExtractor.getIssueIds(projectName, commitId))
+            .flatMap(Stream::of)
+            .map(
+                associations -> propertyExtractor.extractIssuesProperties(properties, associations))
+            .flatMap(Collection::stream)
+            .collect(Collectors.toSet());
+
+    issuesProperties.forEach(this::doExecute);
+  }
+
+  private void doExecute(Map<String, String> issueProperties) {
+    Collection<ActionRequest> actions = ruleBase.actionRequestsFor(issueProperties);
+    if (actions.isEmpty()) {
+      return;
+    }
+    actionExecutor.executeOnIssue(actions, issueProperties);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/FireEventOnCommitsParameters.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/FireEventOnCommitsParameters.java
new file mode 100644
index 0000000..d6ee4db
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/FireEventOnCommitsParameters.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.its.base.workflow;
+
+import com.googlesource.gerrit.plugins.its.base.workflow.commit_collector.CommitCollector;
+
+/** Parameters needed by {@link FireEventOnCommits} action */
+public class FireEventOnCommitsParameters {
+
+  private final CommitCollector commitCollector;
+  private final String projectName;
+
+  public FireEventOnCommitsParameters(CommitCollector commitCollector, String projectName) {
+    this.commitCollector = commitCollector;
+    this.projectName = projectName;
+  }
+
+  /**
+   * @return The collector that will be used to collect the commits on which the event will be fired
+   */
+  public CommitCollector getCommitCollector() {
+    return commitCollector;
+  }
+
+  /** @return The name of the project from which the commits will be collected */
+  public String getProjectName() {
+    return projectName;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/FireEventOnCommitsParametersExtractor.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/FireEventOnCommitsParametersExtractor.java
new file mode 100644
index 0000000..d7ae586
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/FireEventOnCommitsParametersExtractor.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.its.base.workflow;
+
+import com.google.common.base.Strings;
+import com.googlesource.gerrit.plugins.its.base.workflow.commit_collector.CommitCollector;
+import com.googlesource.gerrit.plugins.its.base.workflow.commit_collector.SinceLastTagCommitCollector;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Optional;
+import javax.inject.Inject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class FireEventOnCommitsParametersExtractor {
+
+  private static final Logger log =
+      LoggerFactory.getLogger(FireEventOnCommitsParametersExtractor.class);
+
+  private final SinceLastTagCommitCollector.Factory sinceLastTagCommitCollectorFactory;
+
+  @Inject
+  public FireEventOnCommitsParametersExtractor(
+      SinceLastTagCommitCollector.Factory sinceLastTagCommitCollectorFactory) {
+    this.sinceLastTagCommitCollectorFactory = sinceLastTagCommitCollectorFactory;
+  }
+
+  private CommitCollector getCommitCollector(String name) {
+    switch (name) {
+      case "since-last-tag":
+        return sinceLastTagCommitCollectorFactory.create();
+      default:
+        return null;
+    }
+  }
+
+  /**
+   * @return The parameters needed by {@link FireEventOnCommits}. Empty if the parameters could not
+   *     be extracted.
+   */
+  public Optional<FireEventOnCommitsParameters> extract(
+      ActionRequest actionRequest, Map<String, String> properties) {
+    String[] parameters = actionRequest.getParameters();
+    if (parameters.length != 1) {
+      log.error(
+          "Wrong number of received parameters. Received parameters are {}. Only one parameter is expected, the collector name.",
+          Arrays.toString(parameters));
+      return Optional.empty();
+    }
+
+    String collectorName = parameters[0];
+    if (Strings.isNullOrEmpty(collectorName)) {
+      log.error("Received collector name is blank");
+      return Optional.empty();
+    }
+
+    CommitCollector commitCollector = getCommitCollector(collectorName);
+    if (commitCollector == null) {
+      log.error("No commit collector found for name {}", collectorName);
+      return Optional.empty();
+    }
+
+    String projectName = properties.get("project");
+    return Optional.of(new FireEventOnCommitsParameters(commitCollector, projectName));
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ItsRulesProjectCacheImpl.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ItsRulesProjectCacheImpl.java
index 72fe43f..4dc46d3 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ItsRulesProjectCacheImpl.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ItsRulesProjectCacheImpl.java
@@ -98,7 +98,10 @@
 
     @Override
     public List<Rule> load(String projectName) throws IOException {
-      ProjectState project = projectCache.checkedGet(Project.nameKey(projectName));
+      ProjectState project =
+          projectCache
+              .get(Project.nameKey(projectName))
+              .orElseThrow(() -> new IOException("Can't load " + projectName));
       List<Rule> projectRules = readRulesFrom(project);
       if (projectRules.isEmpty()) {
         for (ProjectState parent : project.parents()) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/RefEventProperties.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/RefEventProperties.java
index 30940bb..01b6395 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/RefEventProperties.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/RefEventProperties.java
@@ -14,7 +14,6 @@
 
 package com.googlesource.gerrit.plugins.its.base.workflow;
 
-import java.util.Collections;
 import java.util.Map;
 import java.util.Set;
 
@@ -31,8 +30,8 @@
    */
   public RefEventProperties(
       Map<String, String> projectProperties, Set<Map<String, String>> issuesProperties) {
-    this.projectProperties = Collections.unmodifiableMap(projectProperties);
-    this.issuesProperties = Collections.unmodifiableSet(issuesProperties);
+    this.projectProperties = projectProperties;
+    this.issuesProperties = issuesProperties;
   }
 
   /** @return Properties of the ref event */
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/commit_collector/CommitCollector.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/commit_collector/CommitCollector.java
new file mode 100644
index 0000000..73ea110
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/commit_collector/CommitCollector.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.its.base.workflow.commit_collector;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+/** Collects commits based on the event properties provided in input */
+public interface CommitCollector {
+  List<String> collect(Map<String, String> properties) throws IOException;
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/commit_collector/SinceLastTagCommitCollector.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/commit_collector/SinceLastTagCommitCollector.java
new file mode 100644
index 0000000..d8f1ec1
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/commit_collector/SinceLastTagCommitCollector.java
@@ -0,0 +1,97 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.its.base.workflow.commit_collector;
+
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import javax.inject.Inject;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevTag;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+/** Collects all commits between the last tag and HEAD */
+public class SinceLastTagCommitCollector implements CommitCollector {
+
+  public interface Factory {
+    SinceLastTagCommitCollector create();
+  }
+
+  private final GitRepositoryManager repoManager;
+
+  @Inject
+  public SinceLastTagCommitCollector(GitRepositoryManager repoManager) {
+    this.repoManager = repoManager;
+  }
+
+  @Override
+  public List<String> collect(Map<String, String> properties) throws IOException {
+    String projectName = properties.get("project");
+    String revision = properties.get("revision");
+
+    try (Repository repo = repoManager.openRepository(Project.nameKey(projectName))) {
+      return collect(repo, revision);
+    }
+  }
+
+  private List<String> collect(Repository repo, String currentRevision) throws IOException {
+    try (RevWalk revWalk = new RevWalk(repo)) {
+      RevCommit currentCommit = revWalk.parseCommit(repo.resolve(currentRevision));
+      revWalk.markStart(currentCommit);
+      List<String> commitIds = new ArrayList<>();
+      for (RevCommit commit : revWalk) {
+        if (!currentCommit.getId().equals(commit.getId()) && isTagged(repo, commit)) {
+          break;
+        }
+        commitIds.add(ObjectId.toString(commit.getId()));
+      }
+      return commitIds;
+    }
+  }
+
+  /** @return True if {@code commit} is tagged */
+  private boolean isTagged(Repository repo, RevCommit commit) throws IOException {
+    ObjectId commitId = commit.getId();
+    try (RevWalk revWalk = new RevWalk(repo)) {
+      return repo.getRefDatabase().getRefsByPrefix(Constants.R_TAGS).stream()
+          .map(Ref::getObjectId)
+          .map(
+              refObjectId -> {
+                try {
+                  return revWalk.parseTag(refObjectId);
+                } catch (IncorrectObjectTypeException e) {
+                  return null;
+                } catch (IOException e) {
+                  throw new UncheckedIOException(e);
+                }
+              })
+          .filter(Objects::nonNull)
+          .map(RevTag::getObject)
+          .map(RevObject::getId)
+          .anyMatch(commitId::equals);
+    }
+  }
+}
diff --git a/src/main/resources/Documentation/config-rulebase-common.md b/src/main/resources/Documentation/config-rulebase-common.md
index d232e57..50e1023 100644
--- a/src/main/resources/Documentation/config-rulebase-common.md
+++ b/src/main/resources/Documentation/config-rulebase-common.md
@@ -574,6 +574,13 @@
 `uploaderUsername`
 : username of the user that uploaded this patch set.
 
+### Common properties for all events
+
+`source`
+: source of the event that triggered the action. Can be `its` or `gerrit`.
+  `its` sourced events are fired by its actions. e.g. `fire-event-on-commits` action fires `its` sourced events.
+  `gerrit` sourced events are directly fired by the Gerrit event system.
+
 ## Actions
 
 Lines of the form
@@ -693,6 +700,26 @@
   action = create-version-from-property ref
 ```
 
+### Action: fire-event-on-commits
+
+The `fire-event-on-commits` action start by collecting commits using a collector then fire the event
+on each collected commit.
+
+This is useful when you want to trigger rules on a multiple past commits.
+
+Available collectors are:
+- `since-last-tag`: Collects all commits between the current ref and previous tag
+
+To avoid to trigger issue actions twice for the same event, you should condition your rule on
+the event property `source`.
+
+Example:
+
+```
+  action = fire-event-on-commits since-last-tag
+```
+
+
 ### Action: log-event
 
 The `log-event` action appends the event's properties to Gerrit's log.
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/base/its/ItsConfigTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/base/its/ItsConfigTest.java
index 3379908..34068bb 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/its/base/its/ItsConfigTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/base/its/ItsConfigTest.java
@@ -56,8 +56,8 @@
       String enabled, String itsProject, String parentEnabled, String[] branches) {
     ProjectState projectState = mock(ProjectState.class);
 
-    when(projectCache.get(Project.nameKey("testProject"))).thenReturn(projectState);
-    when(projectCache.get(Project.nameKey("parentProject"))).thenReturn(projectState);
+    when(projectCache.get(Project.nameKey("testProject"))).thenReturn(Optional.of(projectState));
+    when(projectCache.get(Project.nameKey("parentProject"))).thenReturn(Optional.of(projectState));
 
     Iterable<ProjectState> parents;
     if (parentEnabled == null) {
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/base/util/PropertyExtractorTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/base/util/PropertyExtractorTest.java
index ac7ea92..d4f0605 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/its/base/util/PropertyExtractorTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/base/util/PropertyExtractorTest.java
@@ -41,6 +41,7 @@
 import com.google.inject.Guice;
 import com.google.inject.Injector;
 import com.googlesource.gerrit.plugins.its.base.testutil.LoggingMockingTestCase;
+import com.googlesource.gerrit.plugins.its.base.workflow.RefEventProperties;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
@@ -374,13 +375,30 @@
       when(issueExtractor.getIssueIds("testProject", "testRevision")).thenReturn(issueMap);
     }
 
-    Set<Map<String, String>> actual = propertyExtractor.extractFrom(event).getIssuesProperties();
+    RefEventProperties refEventProperties = propertyExtractor.extractFrom(event);
+
+    Map<String, String> actualProjectProperties = refEventProperties.getProjectProperties();
+
+    Map<String, String> expectedProjectProperties =
+        ImmutableMap.<String, String>builder()
+            .put("itsName", "ItsTestName")
+            .put("event", "com.google.gerrit.server.events." + className)
+            .put("event-type", type)
+            .put("source", "gerrit")
+            .putAll(common)
+            .build();
+
+    assertEquals(
+        "Project properties do not match", expectedProjectProperties, actualProjectProperties);
+
+    Set<Map<String, String>> actualIssuesProperties = refEventProperties.getIssuesProperties();
 
     Map<String, String> propertiesIssue4711 =
         ImmutableMap.<String, String>builder()
             .put("itsName", "ItsTestName")
             .put("event", "com.google.gerrit.server.events." + className)
             .put("event-type", type)
+            .put("source", "its")
             .put("association", "body anywhere")
             .put("issue", "4711")
             .putAll(common)
@@ -390,15 +408,17 @@
             .put("itsName", "ItsTestName")
             .put("event", "com.google.gerrit.server.events." + className)
             .put("event-type", type)
+            .put("source", "its")
             .put("association", "anywhere footer")
             .put("issue", "42")
             .putAll(common)
             .build();
-    Set<Map<String, String>> expected = new HashSet<>();
-    expected.add(propertiesIssue4711);
-    expected.add(propertiesIssue42);
+    Set<Map<String, String>> expectedIssuesProperties = new HashSet<>();
+    expectedIssuesProperties.add(propertiesIssue4711);
+    expectedIssuesProperties.add(propertiesIssue42);
 
-    assertEquals("Properties do not match", expected, actual);
+    assertEquals(
+        "Issues properties do not match", expectedIssuesProperties, actualIssuesProperties);
   }
 
   @Override
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionExecutorTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionExecutorTest.java
index 5b8c43f..822602b 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionExecutorTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionExecutorTest.java
@@ -48,6 +48,7 @@
   private LogEvent.Factory logEventFactory;
   private AddPropertyToField.Factory addPropertyToFieldFactory;
   private CreateVersionFromProperty.Factory createVersionFromPropertyFactory;
+  private FireEventOnCommits.Factory fireEventOnCommitsFactory;
   private CustomAction customAction;
 
   private Map<String, String> properties =
@@ -212,7 +213,8 @@
 
     CreateVersionFromProperty createVersionFromProperty = mock(CreateVersionFromProperty.class);
     when(createVersionFromPropertyFactory.create()).thenReturn(createVersionFromProperty);
-    when(itsFacadeFactory.getFacade(Project.nameKey(properties.get("project")))).thenReturn(its);
+    when(itsFacadeFactory.getFacade(Project.nameKey(projectProperties.get("project"))))
+        .thenReturn(its);
 
     ActionExecutor actionExecutor = createActionExecutor();
     actionExecutor.executeOnProject(Collections.singleton(actionRequest), projectProperties);
@@ -237,6 +239,21 @@
     verify(addPropertyToField).execute(its, "4711", actionRequest, properties);
   }
 
+  public void testFireEventOnCommitsDelegation() throws IOException {
+    ActionRequest actionRequest = mock(ActionRequest.class);
+    when(actionRequest.getName()).thenReturn("fire-event-on-commits");
+
+    FireEventOnCommits fireEventOnCommits = mock(FireEventOnCommits.class);
+    when(fireEventOnCommitsFactory.create()).thenReturn(fireEventOnCommits);
+    when(itsFacadeFactory.getFacade(Project.nameKey(projectProperties.get("project"))))
+        .thenReturn(its);
+
+    ActionExecutor actionExecutor = createActionExecutor();
+    actionExecutor.executeOnProject(Collections.singleton(actionRequest), projectProperties);
+
+    verify(fireEventOnCommits).execute(its, "itsTestProject", actionRequest, projectProperties);
+  }
+
   public void testExecuteIssueCustomAction() throws IOException {
     when(customAction.getType()).thenReturn(ActionType.ISSUE);
 
@@ -304,6 +321,9 @@
       createVersionFromPropertyFactory = mock(CreateVersionFromProperty.Factory.class);
       bind(CreateVersionFromProperty.Factory.class).toInstance(createVersionFromPropertyFactory);
 
+      fireEventOnCommitsFactory = mock(FireEventOnCommits.Factory.class);
+      bind(FireEventOnCommits.Factory.class).toInstance(fireEventOnCommitsFactory);
+
       DynamicMap.mapOf(binder(), CustomAction.class);
       customAction = mock(CustomAction.class);
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/FireEventOnCommitsParametersExtractorTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/FireEventOnCommitsParametersExtractorTest.java
new file mode 100644
index 0000000..c99bf33
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/FireEventOnCommitsParametersExtractorTest.java
@@ -0,0 +1,102 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.its.base.workflow;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.googlesource.gerrit.plugins.its.base.workflow.commit_collector.SinceLastTagCommitCollector;
+import java.util.Collections;
+import java.util.Optional;
+import junit.framework.TestCase;
+
+public class FireEventOnCommitsParametersExtractorTest extends TestCase {
+
+  private static final String SINCE_LAST_TAG_COLLECTOR = "since-last-tag";
+
+  private SinceLastTagCommitCollector.Factory sinceLastTagCommitCollectorFactory;
+  private FireEventOnCommitsParametersExtractor extractor;
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    Injector injector = Guice.createInjector(new TestModule());
+    extractor = injector.getInstance(FireEventOnCommitsParametersExtractor.class);
+  }
+
+  private class TestModule extends FactoryModule {
+    @Override
+    protected void configure() {
+      sinceLastTagCommitCollectorFactory = mock(SinceLastTagCommitCollector.Factory.class);
+      bind(SinceLastTagCommitCollector.Factory.class)
+          .toInstance(sinceLastTagCommitCollectorFactory);
+    }
+  }
+
+  public void testNoParameter() {
+    testWrongNumberOfReceivedParameters(new String[] {});
+  }
+
+  public void testTwoParameters() {
+    testWrongNumberOfReceivedParameters(
+        new String[] {SINCE_LAST_TAG_COLLECTOR, SINCE_LAST_TAG_COLLECTOR});
+  }
+
+  private void testWrongNumberOfReceivedParameters(String[] parameters) {
+    ActionRequest actionRequest = mock(ActionRequest.class);
+    when(actionRequest.getParameters()).thenReturn(parameters);
+
+    Optional<FireEventOnCommitsParameters> extractedParameters =
+        extractor.extract(actionRequest, Collections.emptyMap());
+    assertFalse(extractedParameters.isPresent());
+  }
+
+  public void testBlankCollectorName() {
+    ActionRequest actionRequest = mock(ActionRequest.class);
+    when(actionRequest.getParameters()).thenReturn(new String[] {""});
+
+    Optional<FireEventOnCommitsParameters> extractedParameters =
+        extractor.extract(actionRequest, Collections.emptyMap());
+    assertFalse(extractedParameters.isPresent());
+  }
+
+  public void testUnknownCollectorName() {
+    ActionRequest actionRequest = mock(ActionRequest.class);
+    when(actionRequest.getParameters()).thenReturn(new String[] {"foo"});
+
+    Optional<FireEventOnCommitsParameters> extractedParameters =
+        extractor.extract(actionRequest, Collections.emptyMap());
+    assertFalse(extractedParameters.isPresent());
+  }
+
+  public void testSinceLastTagCollector() {
+    ActionRequest actionRequest = mock(ActionRequest.class);
+    when(actionRequest.getParameters()).thenReturn(new String[] {SINCE_LAST_TAG_COLLECTOR});
+
+    SinceLastTagCommitCollector collector = mock(SinceLastTagCommitCollector.class);
+    when(sinceLastTagCommitCollectorFactory.create()).thenReturn(collector);
+
+    Optional<FireEventOnCommitsParameters> extractedParameters =
+        extractor.extract(actionRequest, Collections.singletonMap("project", "testProject"));
+    if (!extractedParameters.isPresent()) {
+      fail();
+    }
+    assertEquals(collector, extractedParameters.get().getCommitCollector());
+    assertEquals("testProject", extractedParameters.get().getProjectName());
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/FireEventOnCommitsTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/FireEventOnCommitsTest.java
new file mode 100644
index 0000000..e80081d
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/FireEventOnCommitsTest.java
@@ -0,0 +1,115 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.its.base.workflow;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.googlesource.gerrit.plugins.its.base.its.ItsFacade;
+import com.googlesource.gerrit.plugins.its.base.util.IssueExtractor;
+import com.googlesource.gerrit.plugins.its.base.util.PropertyExtractor;
+import com.googlesource.gerrit.plugins.its.base.workflow.commit_collector.SinceLastTagCommitCollector;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import junit.framework.TestCase;
+
+public class FireEventOnCommitsTest extends TestCase {
+
+  private static final String ITS_PROJECT = "test-project";
+  private static final String COMMIT = "1234";
+  private static final String PROJECT = "testProject";
+
+  private Injector injector;
+  private ItsFacade its;
+  private PropertyExtractor propertyExtractor;
+  private IssueExtractor issueExtractor;
+  private RuleBase ruleBase;
+  private ActionExecutor actionExecutor;
+  private FireEventOnCommitsParametersExtractor parametersExtractor;
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    injector = Guice.createInjector(new TestModule());
+  }
+
+  private class TestModule extends FactoryModule {
+    @Override
+    protected void configure() {
+      its = mock(ItsFacade.class);
+      bind(ItsFacade.class).toInstance(its);
+
+      propertyExtractor = mock(PropertyExtractor.class);
+      bind(PropertyExtractor.class).toInstance(propertyExtractor);
+
+      ruleBase = mock(RuleBase.class);
+      bind(RuleBase.class).toInstance(ruleBase);
+
+      issueExtractor = mock(IssueExtractor.class);
+      bind(IssueExtractor.class).toInstance(issueExtractor);
+
+      actionExecutor = mock(ActionExecutor.class);
+      bind(ActionExecutor.class).toInstance(actionExecutor);
+
+      parametersExtractor = mock(FireEventOnCommitsParametersExtractor.class);
+      bind(FireEventOnCommitsParametersExtractor.class).toInstance(parametersExtractor);
+    }
+  }
+
+  public void testHappyPath() throws IOException {
+    Map<String, String> properties = ImmutableMap.of("project", PROJECT);
+
+    FireEventOnCommitsParameters parameters = mock(FireEventOnCommitsParameters.class);
+    SinceLastTagCommitCollector collector = mock(SinceLastTagCommitCollector.class);
+    when(parameters.getCommitCollector()).thenReturn(collector);
+    when(parameters.getProjectName()).thenReturn(PROJECT);
+
+    ActionRequest actionRequest = mock(ActionRequest.class);
+    when(parametersExtractor.extract(actionRequest, properties))
+        .thenReturn(Optional.of(parameters));
+    when(collector.collect(properties)).thenReturn(Collections.singletonList(COMMIT));
+
+    Map<String, Set<String>> associations = Maps.newHashMap();
+    when(issueExtractor.getIssueIds(PROJECT, COMMIT)).thenReturn(associations);
+
+    Set<Map<String, String>> issuesProperties = ImmutableSet.of(properties);
+    when(propertyExtractor.extractIssuesProperties(properties, associations))
+        .thenReturn(issuesProperties);
+
+    ActionRequest subActionRequest = mock(ActionRequest.class);
+    Collection<ActionRequest> subActionRequests = Collections.singleton(subActionRequest);
+    when(ruleBase.actionRequestsFor(properties)).thenReturn(subActionRequests);
+
+    FireEventOnCommits fireEventOnCommits = createFireEventOnCommits();
+    fireEventOnCommits.execute(its, ITS_PROJECT, actionRequest, properties);
+
+    verify(actionExecutor).executeOnIssue(subActionRequests, properties);
+  }
+
+  private FireEventOnCommits createFireEventOnCommits() {
+    return injector.getInstance(FireEventOnCommits.class);
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/ItsRulesProjectCacheTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/ItsRulesProjectCacheTest.java
index 6b83097..592722c 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/ItsRulesProjectCacheTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/ItsRulesProjectCacheTest.java
@@ -36,6 +36,7 @@
 import java.io.IOException;
 import java.util.Collection;
 import java.util.List;
+import java.util.Optional;
 import org.eclipse.jgit.lib.Config;
 
 public class ItsRulesProjectCacheTest extends LoggingMockingTestCase {
@@ -90,7 +91,7 @@
     ProjectLevelConfig projectLevelConfigPlugin = mock(ProjectLevelConfig.class);
     when(projectLevelConfigPlugin.get()).thenReturn(new Config());
     when(projectState.getConfig(RuleBaseKind.ITS.fileName)).thenReturn(projectLevelConfigPlugin);
-    when(projectCache.checkedGet(Project.nameKey(TEST_PROJECT))).thenReturn(projectState);
+    when(projectCache.get(Project.nameKey(TEST_PROJECT))).thenReturn(Optional.of(projectState));
     when(rulesConfigReader.getRulesFromConfig(any(Config.class)))
         .thenReturn(ImmutableList.of(rule1))
         .thenReturn(ImmutableList.of());
@@ -132,7 +133,7 @@
     when(parentProjectState.getConfig(RuleBaseKind.ITS.fileName))
         .thenReturn(parentProjectConfigPlugin);
     when(projectState.parents()).thenReturn(FluentIterable.of(parentProjectState));
-    when(projectCache.checkedGet(Project.nameKey(TEST_PROJECT))).thenReturn(projectState);
+    when(projectCache.get(Project.nameKey(TEST_PROJECT))).thenReturn(Optional.of(projectState));
 
     when(rulesConfigReader.getRulesFromConfig(any(Config.class)))
         .thenReturn(ImmutableList.of())