Create missing SCS when creating tag events

With this change we create an SCS event for the commit being tagged if
it is missing and the commit is reachable from one of the branches
that is not blocked. With this change we also log the name of the
tag when we fail to create a CD.

Solves: Jira GER-1688
Solves: Jira GER-1689
Change-Id: I77b5e4ad5600548a4aad824a7f0aec42a09e6d1c
diff --git a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/listeners/RefUpdateListener.java b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/listeners/RefUpdateListener.java
index 98ca829..bd60da4 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/listeners/RefUpdateListener.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/listeners/RefUpdateListener.java
@@ -51,7 +51,7 @@
       }
       /* Updated reference is a tag. */
       else if (event.getRefName().startsWith(RefNames.REFS_TAGS)) {
-        queue.scheduleArtcCreation(event);
+        queue.scheduleArtcCreation(event, branch -> config.refIsBlocked(branch));
       }
     }
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/EiffelEventParser.java b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/EiffelEventParser.java
index 771816c..7210ae7 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/EiffelEventParser.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/EiffelEventParser.java
@@ -47,6 +47,7 @@
 import java.util.UUID;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -57,6 +58,8 @@
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.revwalk.RevWalkUtils;
 
 /** Creates and pushes missing Eiffel events to the Eiffel event queue. */
 public class EiffelEventParser {
@@ -214,13 +217,17 @@
   }
 
   public void createAndScheduleArtc(
-      String projectName, String tagName, Long creationTime, boolean force) {
+      String projectName,
+      String tagName,
+      Long creationTime,
+      boolean force,
+      Function<String, Boolean> blockedRefMatcher) {
     try {
       CompositionDefinedEventKey cd =
           CompositionDefinedEventKey.create(mapper.tagCompositionName(projectName), tagName);
       Optional<UUID> oldCdId = eventHub.getExistingId(cd);
       if (oldCdId.isEmpty() || force) {
-        createAndScheduleCd(projectName, tagName, creationTime, force);
+        createAndScheduleCd(projectName, tagName, creationTime, force, blockedRefMatcher);
         Optional<UUID> cdId = eventHub.getExistingId(cd);
         if (cdId.isPresent() && !cdId.equals(oldCdId)) {
           pushToHub(mapper.toArtc(projectName, tagName, creationTime, cdId.get()), force);
@@ -243,35 +250,90 @@
   }
 
   private void createAndScheduleCd(
-      String projectName, String tagName, Long creationTime, boolean force) {
-    SourceChangeEventKey scs = null;
+      String projectName,
+      String tagName,
+      Long creationTime,
+      boolean force,
+      Function<String, Boolean> blockedRefMatcher) {
     Optional<UUID> scsId = Optional.empty();
+    List<Ref> refs = null;
 
     try {
-      String commitId = peelTag(projectName, tagName);
-      if (commitId == null) {
+      ObjectId objectId = peelTag(projectName, tagName);
+      if (objectId == null) {
         return;
       }
-      scs = SourceChangeEventKey.scsKey(projectName, RefNames.REFS_HEADS + "master", commitId);
+      String commitId = objectId.getName();
+
+      /* Check if an event for commit~master has been created. */
+      SourceChangeEventKey scs =
+          SourceChangeEventKey.scsKey(projectName, RefNames.REFS_HEADS + "master", commitId);
       scsId = eventHub.getExistingId(scs);
 
       if (scsId.isEmpty()) {
+        /* No event created for commit~master. Check if event is created for any
+        of the other branches. */
         Retryer<Optional<UUID>> retryer =
             RetryerBuilder.<Optional<UUID>>newBuilder()
                 .retryIfResult(Optional::isEmpty)
                 .withWaitStrategy(WaitStrategies.fixedWait(10, TimeUnit.SECONDS))
                 .withStopStrategy(StopStrategies.stopAfterAttempt(2))
                 .build();
-        try {
-          scsId = retryer.call(() -> findSourceChangeEventKey(projectName, commitId));
-        } catch (RetryException | ExecutionException e) {
+        try (Repository repo = repoManager.openRepository(Project.nameKey(projectName))) {
+          refs =
+              repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_HEADS).stream()
+                  .collect(Collectors.toList());
+        } catch (IOException e) {
           logger.atSevere().withCause(e).log(
-              "Failed to find SCS for %s in %s", commitId, projectName);
+              "Unable to get branches for: %s:%s", projectName, tagName);
           return;
         }
+        try {
+          List<String> branches =
+              refs.stream()
+                  .map(Ref::getName)
+                  .filter(branch -> !branch.equals(RefNames.REFS_HEADS + "master"))
+                  .collect(Collectors.toList());
+          scsId = retryer.call(() -> findSourceChangeEventKey(projectName, commitId, branches));
+        } catch (RetryException | ExecutionException e) {
+          logger.atWarning().withCause(e).log(
+              "Failed to find SCS for %s in %s when trying to create CD for tag %s",
+              commitId, projectName, tagName);
+        }
       }
+
       if (scsId.isEmpty()) {
-        logger.atSevere().log("Could not find SCS for: %s in %s", commitId, projectName);
+        /* No event has been created for the commit. Find any non-blocked branch that
+        commit is merged into and create SCS events for that branch. */
+        List<Ref> branches;
+        try (Repository repo = repoManager.openRepository(Project.nameKey(projectName))) {
+          RevWalk rw = new RevWalk(repo);
+          refs =
+              refs.stream()
+                  .filter(ref -> !blockedRefMatcher.apply(ref.getName()))
+                  .collect(Collectors.toList());
+          branches = RevWalkUtils.findBranchesReachableFrom(rw.parseCommit(objectId), rw, refs);
+        } catch (IOException e) {
+          logger.atSevere().withCause(e).log(
+              "Unable to get reachable branches for: %s:%s", projectName, tagName);
+          return;
+        }
+        if (branches.isEmpty()) {
+          logger.atWarning().log(
+              "Could not find any unblocked branch for SCS with: %s in %s so CD could not be created for tag %s",
+              commitId, projectName, tagName);
+          return;
+        }
+        String branch = branches.get(0).getName();
+        scs = SourceChangeEventKey.scsKey(projectName, branch, commitId);
+        createAndScheduleMissingScss(scs, null, null, null);
+        scsId = eventHub.getExistingId(scs);
+      }
+
+      if (scsId.isEmpty()) {
+        logger.atWarning().log(
+            "Could not find or create SCS for %s in %s so CD could not be created for tag %s",
+            commitId, projectName, tagName);
         return;
       }
       pushToHub(mapper.toCd(projectName, tagName, creationTime, scsId.get()), force);
@@ -282,12 +344,12 @@
     }
   }
 
-  private String peelTag(String projectName, String tagName) {
+  private ObjectId peelTag(String projectName, String tagName) {
     try (Repository repo = repoManager.openRepository(Project.nameKey(projectName))) {
       Ref tagRef = repo.getRefDatabase().exactRef(Constants.R_TAGS + tagName);
       if (tagRef != null) {
         ObjectId peeled = repo.getRefDatabase().peel(tagRef).getPeeledObjectId();
-        return peeled != null ? peeled.getName() : tagRef.getObjectId().getName();
+        return peeled != null ? peeled : tagRef.getObjectId();
       }
       logger.atSevere().log("Cannot find tag: %s:%s", projectName, tagName);
     } catch (IOException e) {
@@ -296,18 +358,9 @@
     return null;
   }
 
-  private Optional<UUID> findSourceChangeEventKey(String projectName, String commitId)
+  private Optional<UUID> findSourceChangeEventKey(
+      String projectName, String commitId, List<String> branches)
       throws EiffelEventIdLookupException {
-    List<String> branches;
-    try (Repository repo = repoManager.openRepository(Project.nameKey(projectName))) {
-      branches =
-          repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_HEADS).stream()
-              .map(Ref::getName)
-              .collect(Collectors.toList());
-    } catch (IOException ioe) {
-      logger.atSevere().withCause(ioe).log("Unable to find branches of %s.", projectName);
-      return Optional.empty();
-    }
     return eventHub.getScsForCommit(projectName, commitId, branches);
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/EiffelEventParsingQueue.java b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/EiffelEventParsingQueue.java
index 1fd7ec5..ba99d07 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/EiffelEventParsingQueue.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/EiffelEventParsingQueue.java
@@ -32,6 +32,7 @@
 import com.googlesource.gerrit.plugins.eventseiffel.eiffel.dto.EiffelEventType;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ScheduledFuture;
+import java.util.function.Function;
 
 public class EiffelEventParsingQueue {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -142,12 +143,14 @@
         });
   }
 
-  public void scheduleArtcCreation(GitReferenceUpdatedListener.Event event) {
+  public void scheduleArtcCreation(
+      GitReferenceUpdatedListener.Event event, Function<String, Boolean> blockedRefMatcher) {
     scheduleArtcCreation(
         event.getProjectName(),
         event.getRefName().substring(RefNames.REFS_TAGS.length()),
         TimeUtil.nowMs(),
-        false);
+        false,
+        blockedRefMatcher);
   }
 
   public void scheduleArtcCreation(TagResource resource, boolean force) {
@@ -158,18 +161,24 @@
             ? tagRef.substring(RefNames.REFS_TAGS.length())
             : tagRef,
         resource.getTagInfo().created.getTime(),
-        force);
+        force,
+        branch -> false);
   }
 
   public void scheduleArtcCreation(
-      String projectName, String tagName, Long creationTime, boolean force) {
+      String projectName,
+      String tagName,
+      Long creationTime,
+      boolean force,
+      Function<String, Boolean> blockedRefMatcher) {
     schedule(
         new EventParsingWorker(ARTC, projectName, tagName) {
 
           @Override
           public void doRun() {
             try {
-              eventParser.createAndScheduleArtc(projectName, tagName, creationTime, force);
+              eventParser.createAndScheduleArtc(
+                  projectName, tagName, creationTime, force, blockedRefMatcher);
             } catch (Exception e) {
               logger.atSevere().withCause(e).log(
                   "Failed to create ARTC for %s:%s", projectName, tagName);
diff --git a/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/listeners/GerritEventListenersIT.java b/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/listeners/GerritEventListenersIT.java
index 126eb50..b394add 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/listeners/GerritEventListenersIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/listeners/GerritEventListenersIT.java
@@ -200,8 +200,16 @@
 
   @Test
   public void lightweightTagCreatedResultsInEvent() throws Exception {
-    UUID parentEventId = markMasterAsHandled(SCS);
-    String tagName = createTagRef(false).substring(RefNames.REFS_TAGS.length());
+    tagCreatedResultsInEvent(false);
+  }
+
+  @Test
+  public void annotatedTagCreatedResultsInEvent() throws Exception {
+    tagCreatedResultsInEvent(true);
+  }
+
+  private void tagCreatedResultsInEvent(boolean annotated) throws Exception {
+    String tagName = createTagRef(annotated).substring(RefNames.REFS_TAGS.length());
 
     EventKey artcKey = ArtifactEventKey.create(tagPURL(project.get(), tagName));
     EiffelEvent artcEvent = publisher.getPublished(artcKey);
@@ -213,13 +221,37 @@
     assertNotNull("Publisher did not find CD event", cdEvent);
     assertEquals(EventKey.fromEvent(cdEvent), cdKey);
 
+    EventKey scsKey = SourceChangeEventKey.scsKey(project.get(), "master", getMaster());
+    EiffelEvent scsEvent = publisher.getPublished(scsKey);
+    assertNotNull("Publisher did not find SCS event", scsEvent);
+    assertEquals(EventKey.fromEvent(scsEvent), scsKey);
+
     assertArtcLinks(cdEvent.meta.id, artcEvent.links);
-    assertCdLinks(parentEventId, cdEvent.links);
+    assertCdLinks(scsEvent.meta.id, cdEvent.links);
   }
 
   @Test
-  public void annotatedTagCreatedResultsInEvent() throws Exception {
+  public void tagCreatedResultsInNoEventWhenBranchIsBlocked() throws Exception {
+    TestEventListenerConfigProvider.setBlockedRefPatterns("refs/heads/master");
+    String tagName = createTagRef(true).substring(RefNames.REFS_TAGS.length());
+
+    EventKey artcKey = ArtifactEventKey.create(tagPURL(project.get(), tagName));
+    EiffelEvent artcEvent = publisher.getPublished(artcKey);
+    assertNull("Publisher found ARTC event", artcEvent);
+
+    EventKey cdKey = CompositionDefinedEventKey.create(tagCompositionName(project.get()), tagName);
+    EiffelEvent cdEvent = publisher.getPublished(cdKey);
+    assertNull("Publisher found CD event", cdEvent);
+
+    EventKey scsKey = SourceChangeEventKey.scsKey(project.get(), "master", getMaster());
+    EiffelEvent scsEvent = publisher.getPublished(scsKey);
+    assertNull("Publisher found SCS event", scsEvent);
+  }
+
+  @Test
+  public void tagCreatedResultsInNoEventWhenBranchIsBlockedSCSHandled() throws Exception {
     UUID parentEventId = markMasterAsHandled(SCS);
+    TestEventListenerConfigProvider.setBlockedRefPatterns("refs/heads/master");
     String tagName = createTagRef(true).substring(RefNames.REFS_TAGS.length());
 
     EventKey artcKey = ArtifactEventKey.create(tagPURL(project.get(), tagName));
diff --git a/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/EiffelEventParserIT.java b/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/EiffelEventParserIT.java
index 3afa5ff..cf4194c 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/EiffelEventParserIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/EiffelEventParserIT.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.acceptance.TestPlugin;
 import com.google.gerrit.acceptance.UseLocalDisk;
 import com.google.gerrit.acceptance.config.GlobalPluginConfig;
+import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.inject.AbstractModule;
 import com.google.inject.Scopes;
@@ -209,7 +210,7 @@
     String ref = setCdHandled();
     ArtifactEventKey artc = ArtifactEventKey.create(tagPURL(project.get(), ref));
     markAsHandled(artc);
-    eventParser.createAndScheduleArtc(project.get(), ref, EPOCH_MILLIS, false);
+    eventParser.createAndScheduleArtc(project.get(), ref, EPOCH_MILLIS, false, branch -> false);
     assertEquals(0, TestEventHub.EVENTS.size());
   }
 
@@ -223,7 +224,7 @@
     markAsHandled(cd);
     ArtifactEventKey artc = ArtifactEventKey.create(tagPURL(project.get(), ref));
     markAsHandled(artc);
-    eventParser.createAndScheduleArtc(project.get(), ref, EPOCH_MILLIS, true);
+    eventParser.createAndScheduleArtc(project.get(), ref, EPOCH_MILLIS, true, branch -> false);
     assertEquals(2, TestEventHub.EVENTS.size());
     assertCorrectEvent(0, cd);
     assertCorrectEvent(1, artc);
@@ -247,7 +248,60 @@
 
   @Test
   public void artcQueued() throws Exception {
-    eventParser.createAndScheduleArtc(project.get(), TAG_NAME, EPOCH_MILLIS, false);
+    String ref =
+        createTagRef(getHead(repo(), "HEAD").getName(), true).substring(Constants.R_TAGS.length());
+
+    SourceChangeEventKey scc =
+        SourceChangeEventKey.sccKey(project.get(), getHead(), getHeadRevision());
+    SourceChangeEventKey scs = scc.copy(SCS);
+    ArtifactEventKey artc = ArtifactEventKey.create(tagPURL(project.get(), ref, "localhost"));
+    CompositionDefinedEventKey cd =
+        CompositionDefinedEventKey.create(tagCompositionName(project.get(), "localhost"), ref);
+
+    eventParser.createAndScheduleArtc(project.get(), ref, EPOCH_MILLIS, false, branch -> false);
+    assertEquals(4, TestEventHub.EVENTS.size());
+    assertCorrectEvent(0, scc);
+    assertCorrectEvent(1, scs);
+    assertCorrectEvent(2, cd);
+    assertCorrectEvent(3, artc);
+  }
+
+  @Test
+  public void artcQueuedBlockBranch() throws Exception {
+    String ref =
+        createTagRef(getHead(repo(), "HEAD").getName(), true).substring(Constants.R_TAGS.length());
+
+    eventParser.createAndScheduleArtc(
+        project.get(), ref, EPOCH_MILLIS, false, branch -> branch.equals("refs/heads/master"));
+    assertEquals(0, TestEventHub.EVENTS.size());
+  }
+
+  @Test
+  public void artcQueuedBlockBranchButReachableFromOtherBranch() throws Exception {
+    createBranch(BranchNameKey.create(project, "other-branch"));
+    String ref =
+        createTagRef(getHead(repo(), "HEAD").getName(), true).substring(Constants.R_TAGS.length());
+
+    SourceChangeEventKey scc =
+        SourceChangeEventKey.sccKey(project.get(), "other-branch", getHeadRevision());
+    SourceChangeEventKey scs = scc.copy(SCS);
+    ArtifactEventKey artc = ArtifactEventKey.create(tagPURL(project.get(), ref, "localhost"));
+    CompositionDefinedEventKey cd =
+        CompositionDefinedEventKey.create(tagCompositionName(project.get(), "localhost"), ref);
+
+    eventParser.createAndScheduleArtc(
+        project.get(), ref, EPOCH_MILLIS, false, branch -> branch.equals("refs/heads/master"));
+    assertEquals(4, TestEventHub.EVENTS.size());
+    assertCorrectEvent(0, scc);
+    assertCorrectEvent(1, scs);
+    assertCorrectEvent(2, cd);
+    assertCorrectEvent(3, artc);
+  }
+
+  @Test
+  public void artcQueuedNoTagCreated() throws Exception {
+    eventParser.createAndScheduleArtc(
+        project.get(), TAG_NAME, EPOCH_MILLIS, false, branch -> false);
     assertEquals(0, TestEventHub.EVENTS.size());
   }
 
@@ -266,7 +320,7 @@
     String ref = setCdHandled();
     ArtifactEventKey artc = ArtifactEventKey.create(tagPURL(project.get(), ref));
 
-    eventParser.createAndScheduleArtc(project.get(), ref, EPOCH_MILLIS, false);
+    eventParser.createAndScheduleArtc(project.get(), ref, EPOCH_MILLIS, false, branch -> false);
     assertEquals(0, TestEventHub.EVENTS.size());
   }
 
@@ -290,7 +344,7 @@
     CompositionDefinedEventKey cd =
         CompositionDefinedEventKey.create(tagCompositionName(project.get(), namespace), ref);
 
-    eventParser.createAndScheduleArtc(project.get(), ref, EPOCH_MILLIS, false);
+    eventParser.createAndScheduleArtc(project.get(), ref, EPOCH_MILLIS, false, branch -> false);
     assertEquals(2, TestEventHub.EVENTS.size());
     assertCorrectEvent(0, cd);
     assertCorrectEvent(1, artc);
diff --git a/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/rest/EventsEiffelRestIT.java b/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/rest/EventsEiffelRestIT.java
index d483a66..691acf1 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/rest/EventsEiffelRestIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/rest/EventsEiffelRestIT.java
@@ -123,7 +123,6 @@
 
   @Test
   public void createArtcAsAdmin() throws Exception {
-    createScss("master", true).assertStatus(202);
     String tagName = createTagRef(true).substring(RefNames.REFS_TAGS.length());
     createArtcs(tagName, true).assertStatus(202);
     ArtifactEventKey artc = ArtifactEventKey.create(tagPURL(project.get(), tagName));
@@ -133,7 +132,6 @@
 
   @Test
   public void createArtcAsNonAdminForbidden() throws Exception {
-    createScss("master", true).assertStatus(202);
     String tagName = createTagRef(true).substring(RefNames.REFS_TAGS.length());
     createArtcs(tagName, false).assertStatus(403);
     ArtifactEventKey artc = ArtifactEventKey.create(tagPURL(project.get(), tagName));