Add methods for scheduling creation of missing SourceChange events

With new class ParentWalker that traverse from parents to children.
It includes optional attribute parentKey that specify that only
direct children of this parent commit should be traversed.

Solves: Jira GER-1545
Change-Id: I53afe4b07ce0a3211c7aa675ba4beeddad073530
diff --git a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/UnprocessedCommitsWalker.java b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/CommitsWalker.java
similarity index 72%
rename from src/main/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/UnprocessedCommitsWalker.java
rename to src/main/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/CommitsWalker.java
index 1ddd0e2..1c9a609 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/UnprocessedCommitsWalker.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/CommitsWalker.java
@@ -29,6 +29,7 @@
 import com.googlesource.gerrit.plugins.eventseiffel.cache.EiffelEventIdLookupException;
 import com.googlesource.gerrit.plugins.eventseiffel.eiffel.SourceChangeEventKey;
 import java.io.IOException;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Optional;
 import java.util.UUID;
@@ -42,7 +43,7 @@
 import org.eclipse.jgit.revwalk.RevSort;
 import org.eclipse.jgit.revwalk.RevWalk;
 
-public abstract class UnprocessedCommitsWalker implements AutoCloseable {
+public abstract class CommitsWalker implements AutoCloseable {
   @Singleton
   public static class Factory {
     private final EiffelEventHub eventHub;
@@ -59,6 +60,16 @@
       return new SccWalker(eventKey, eventHub, repoManager);
     }
 
+    public CommitsWalker childWalker(SourceChangeEventKey eventKey)
+        throws RepositoryNotFoundException, IOException, EiffelEventIdLookupException {
+      return new ChildWalker(eventKey, eventHub, repoManager, Optional.empty());
+    }
+
+    public CommitsWalker childWalker(SourceChangeEventKey eventKey, SourceChangeEventKey parentKey)
+        throws RepositoryNotFoundException, IOException, EiffelEventIdLookupException {
+      return new ChildWalker(eventKey, eventHub, repoManager, Optional.of(parentKey));
+    }
+
     public ScsWalker scsWalker(
         SourceChangeEventKey eventKey,
         String commitSha1TransactionEnd,
@@ -70,8 +81,6 @@
     }
   }
 
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
   private static void flagWithParents(RevCommit commit, RevFlag flag) {
     commit.add(flag);
     for (RevCommit parent : commit.getParents()) {
@@ -79,22 +88,23 @@
     }
   }
 
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   protected final SourceChangeEventKey eventKey;
   protected final EiffelEventHub eventHub;
   protected final Repository repo;
   protected final RevWalk rw;
-  protected final boolean hasEvents;
   protected final List<AutoCloseable> toClose = Lists.newArrayList();
   protected EventCreate next;
 
-  UnprocessedCommitsWalker(
+  CommitsWalker(
       SourceChangeEventKey eventKey, EiffelEventHub eventHub, GitRepositoryManager repoManager)
       throws RepositoryNotFoundException, IOException, EiffelEventIdLookupException {
     this(eventKey, eventHub, repoManager.openRepository(Project.nameKey(eventKey.repo())));
     this.toClose.add(this.repo);
   }
 
-  UnprocessedCommitsWalker(SourceChangeEventKey eventKey, EiffelEventHub eventHub, Repository repo)
+  CommitsWalker(SourceChangeEventKey eventKey, EiffelEventHub eventHub, Repository repo)
       throws RepositoryNotFoundException, IOException, EiffelEventIdLookupException {
     this.eventKey = eventKey;
     this.eventHub = eventHub;
@@ -102,7 +112,6 @@
     this.rw = new RevWalk(repo);
     this.toClose.add(rw);
     rw.setRetainBody(false);
-    this.hasEvents = hasEvents();
   }
 
   @Override
@@ -133,35 +142,6 @@
     return eventKey.copy(commit.getName());
   }
 
-  protected boolean isAlreadyHandled(
-      RevFlag hasEventFlag, RevCommit commit, SourceChangeEventKey key)
-      throws EiffelEventIdLookupException {
-    logger.atFine().log("%s has event flag: %b", key.copy(commit), commit.has(hasEventFlag));
-    return commit.has(hasEventFlag) || eventHub.getExistingId(key).isPresent();
-  }
-
-  /* Returns true if an event has been created for key.branch from any commits reachable from
-   * key.commit (i.e. initial commit) */
-  private boolean hasEvents()
-      throws MissingObjectException, IncorrectObjectTypeException, IOException,
-          EiffelEventIdLookupException {
-    rw.sort(RevSort.REVERSE);
-    rw.markStart(rw.parseCommit(ObjectId.fromString(eventKey.commit())));
-    RevCommit initialCommit = rw.next();
-    if (initialCommit == null) {
-      logger.atWarning().log("Unable to find initial commit for: %s", eventKey);
-      return false;
-    }
-    logger.atFine().log("Found initial commit: \"%s\" of %s.", initialCommit.name(), eventKey);
-
-    /* Reset RevWalk. */
-    rw.reset();
-
-    Optional<UUID> eventId = eventHub.getExistingId(toKey(initialCommit));
-    eventId.ifPresent(id -> logger.atFine().log("%s has events", eventKey));
-    return eventId.isPresent();
-  }
-
   public class EventCreate {
     SourceChangeEventKey key;
     RevCommit commit;
@@ -182,6 +162,53 @@
     }
   }
 
+  public abstract static class UnprocessedCommitsWalker extends CommitsWalker {
+    protected final boolean hasEvents;
+
+    UnprocessedCommitsWalker(
+        SourceChangeEventKey eventKey, EiffelEventHub eventHub, GitRepositoryManager repoManager)
+        throws RepositoryNotFoundException, IOException, EiffelEventIdLookupException {
+      this(eventKey, eventHub, repoManager.openRepository(Project.nameKey(eventKey.repo())));
+      this.toClose.add(this.repo);
+    }
+
+    UnprocessedCommitsWalker(
+        SourceChangeEventKey eventKey, EiffelEventHub eventHub, Repository repo)
+        throws RepositoryNotFoundException, IOException, EiffelEventIdLookupException {
+      super(eventKey, eventHub, repo);
+      this.hasEvents = hasEvents();
+    }
+
+    protected boolean isAlreadyHandled(
+        RevFlag hasEventFlag, RevCommit commit, SourceChangeEventKey key)
+        throws EiffelEventIdLookupException {
+      logger.atFine().log("%s has event flag: %b", key.copy(commit), commit.has(hasEventFlag));
+      return commit.has(hasEventFlag) || eventHub.getExistingId(key).isPresent();
+    }
+
+    /* Returns true if an event has been created for key.branch from any commits reachable from
+     * key.commit (i.e. initial commit) */
+    private boolean hasEvents()
+        throws MissingObjectException, IncorrectObjectTypeException, IOException,
+            EiffelEventIdLookupException {
+      rw.sort(RevSort.REVERSE);
+      rw.markStart(rw.parseCommit(ObjectId.fromString(eventKey.commit())));
+      RevCommit initialCommit = rw.next();
+      if (initialCommit == null) {
+        logger.atWarning().log("Unable to find initial commit for: %s", eventKey);
+        return false;
+      }
+      logger.atFine().log("Found initial commit: \"%s\" of %s.", initialCommit.name(), eventKey);
+
+      /* Reset RevWalk. */
+      rw.reset();
+
+      Optional<UUID> eventId = eventHub.getExistingId(toKey(initialCommit));
+      eventId.ifPresent(id -> logger.atFine().log("%s has events", eventKey));
+      return eventId.isPresent();
+    }
+  }
+
   static class SccWalker extends UnprocessedCommitsWalker {
     private RevFlag hasSccEventFlag;
 
@@ -328,4 +355,52 @@
       }
     }
   }
+
+  static class ChildWalker extends CommitsWalker {
+    private Optional<SourceChangeEventKey> parentKey;
+
+    ChildWalker(
+        SourceChangeEventKey eventKey,
+        EiffelEventHub eventHub,
+        GitRepositoryManager repoManager,
+        Optional<SourceChangeEventKey> parentKey)
+        throws RepositoryNotFoundException, IOException, EiffelEventIdLookupException {
+      super(eventKey, eventHub, repoManager);
+      this.parentKey = parentKey;
+      checkState(
+          eventKey.type().equals(SCC) || eventKey.type().equals(SCS),
+          "EventKey must have type SCC or SCS for ChildWalker.");
+      rw.markStart(rw.parseCommit(ObjectId.fromString(eventKey.commit())));
+      rw.sort(RevSort.TOPO);
+      rw.sort(RevSort.REVERSE, true);
+      setNext();
+    }
+
+    @Override
+    protected void setNext()
+        throws MissingObjectException, EiffelEventIdLookupException, IOException {
+      next = null;
+      RevCommit commit = rw.next();
+
+      if (parentKey.isPresent()) {
+        while (commit != null) {
+          if (isDirectChild(commit)) {
+            rw.markUninteresting(commit);
+            break;
+          }
+          commit = rw.next();
+        }
+      }
+      if (commit == null) {
+        return;
+      }
+      next = new EventCreate(commit);
+    }
+
+    private boolean isDirectChild(RevCommit commit) {
+      return Arrays.asList(commit.getParents()).stream()
+          .map(RevCommit::getName)
+          .anyMatch(parent -> parent.equals(parentKey.get().commit()));
+    }
+  }
 }
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 3285993..0c39152 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
@@ -51,6 +51,35 @@
       throws EventParsingException;
 
   /**
+   * Recreates missing SCC events that are referenced by other, existing, events.
+   *
+   * @param repoName - Name of the repository were the commits exists.
+   * @param branchRef - Ref of the branch to create events for.
+   * @throws EventParsingException - When event-creation fails.
+   */
+  void fillGapsForSccFromBranch(String repoName, String branchRef) throws EventParsingException;
+
+  /**
+   * Recreates missing SCC events that are referenced by other, existing, events.
+   *
+   * @param repoName - Name of the repository were the commits exists.
+   * @param branchRef - Ref of the branch to create events for.
+   * @param commit - Creates missing events for all commits reachable from commit.
+   * @throws EventParsingException - When event-creation fails.
+   */
+  void fillGapsForSccFromCommit(String repoName, String branchRef, String commit)
+      throws EventParsingException;
+
+  /**
+   * Recreates missing SCS events that are referenced by other, existing, events.
+   *
+   * @param repoName - Name of the repository were the commits exists.
+   * @param branchRef - Ref of the branch to create events for.
+   * @throws EventParsingException - When event-creation fails.
+   */
+  void fillGapsForScsFromBranch(String repoName, String branchRef) throws EventParsingException;
+
+  /**
    * Creates missing SCS events for repoName, branch.
    *
    * @param repoName - Name of the repository where the branch exists.
diff --git a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/EiffelEventParserImpl.java b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/EiffelEventParserImpl.java
index 64a370b..d0e5267 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/EiffelEventParserImpl.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/EiffelEventParserImpl.java
@@ -15,6 +15,7 @@
 package com.googlesource.gerrit.plugins.eventseiffel.parsing;
 
 import static com.googlesource.gerrit.plugins.eventseiffel.eiffel.dto.EiffelEventType.SCC;
+import static com.googlesource.gerrit.plugins.eventseiffel.eiffel.dto.EiffelEventType.SCS;
 
 import com.github.rholder.retry.RetryException;
 import com.github.rholder.retry.Retryer;
@@ -39,11 +40,14 @@
 import com.googlesource.gerrit.plugins.eventseiffel.eiffel.SourceChangeEventKey;
 import com.googlesource.gerrit.plugins.eventseiffel.eiffel.dto.EiffelEvent;
 import com.googlesource.gerrit.plugins.eventseiffel.mapping.EiffelEventMapper;
-import com.googlesource.gerrit.plugins.eventseiffel.parsing.UnprocessedCommitsWalker.EventCreate;
-import com.googlesource.gerrit.plugins.eventseiffel.parsing.UnprocessedCommitsWalker.ScsWalker;
+import com.googlesource.gerrit.plugins.eventseiffel.parsing.CommitsWalker.EventCreate;
+import com.googlesource.gerrit.plugins.eventseiffel.parsing.CommitsWalker.ScsWalker;
+import com.googlesource.gerrit.plugins.eventseiffel.parsing.CommitsWalker.UnprocessedCommitsWalker;
 import java.io.IOException;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Optional;
+import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
@@ -68,7 +72,7 @@
 
   private final EiffelEventHub eventHub;
   private final GitRepositoryManager repoManager;
-  private final UnprocessedCommitsWalker.Factory walkerFactory;
+  private final CommitsWalker.Factory walkerFactory;
   private final EiffelEventMapper mapper;
   private final Provider<EventsFilter> eventsFilter;
 
@@ -77,7 +81,7 @@
       EiffelEventHub eventQueue,
       GitRepositoryManager repoManager,
       EiffelEventMapper mapper,
-      UnprocessedCommitsWalker.Factory walkerFactory,
+      CommitsWalker.Factory walkerFactory,
       Provider<EventsFilter> eventsFilter) {
     this.eventHub = eventQueue;
     this.repoManager = repoManager;
@@ -160,6 +164,49 @@
     }
   }
 
+  @Override
+  public void fillGapsForScsFromBranch(String repoName, String branchRef)
+      throws EventParsingException {
+    ObjectId tip = getTipOf(repoName, branchRef);
+    if (tip == null) {
+      return;
+    }
+    SourceChangeEventKey scs = SourceChangeEventKey.scsKey(repoName, branchRef, tip.getName());
+    fillGapsForSc(scs);
+  }
+
+  @Override
+  public void fillGapsForSccFromBranch(String repoName, String branchRef)
+      throws EventParsingException {
+    ObjectId tip = getTipOf(repoName, branchRef);
+    if (tip == null) {
+      return;
+    }
+    fillGapsForSccFromCommit(repoName, branchRef, tip.getName());
+  }
+
+  @Override
+  public void fillGapsForSccFromCommit(String repoName, String branchRef, String commit)
+      throws EventParsingException {
+    SourceChangeEventKey scc = SourceChangeEventKey.sccKey(repoName, branchRef, commit);
+    fillGapsForSc(scc);
+  }
+
+  @VisibleForTesting
+  private void fillGapsForSc(SourceChangeEventKey key) throws EventParsingException {
+    logger.atFine().log("Start publishing missing events starting from: %s", key);
+    try (CommitsWalker commitFinder = walkerFactory.childWalker(key)) {
+      fillGapsForSc(key, commitFinder);
+    } catch (IOException
+        | EiffelEventIdLookupException
+        | NoSuchEntityException
+        | ConfigInvalidException
+        | InterruptedException e) {
+      throw new EventParsingException(e, "Event creation failed for: %s", key);
+    }
+    logger.atFine().log("Done publishing missing events for starting from: %s", key);
+  }
+
   /* (non-Javadoc)
    * @see com.googlesource.gerrit.plugins.eventseiffel.parsing.EiffelEventParser#createAndScheduleMissingScssFromBranch(java.lang.String, java.lang.String)
    */
@@ -407,6 +454,46 @@
     }
   }
 
+  /* Callers are responsible for closing commitFinder. */
+  private void fillGapsForSc(SourceChangeEventKey tip, CommitsWalker commitFinder)
+      throws MissingObjectException, EiffelEventIdLookupException, IOException,
+          NoSuchEntityException, ConfigInvalidException, InterruptedException,
+          EventParsingException {
+
+    while (commitFinder.hasNext()) {
+      CommitsWalker.EventCreate job = commitFinder.next();
+      RevCommit commit = job.commit;
+      SourceChangeEventKey key = job.key;
+      logger.atFine().log("Processing event-creation for missing event: %s", key);
+
+      Optional<UUID> id = eventHub.getExistingId(key);
+      if (id.isEmpty()) {
+        id = findUuid(tip, key);
+        try {
+          EiffelEvent event;
+          if (key.type() == SCC) {
+            event = mapper.toScc(commit, key.repo(), key.branch(), getParentUuids(key, commit), id);
+          } else {
+            event =
+                mapper.toScs(
+                    commit,
+                    key.repo(),
+                    key.branch(),
+                    null,
+                    null,
+                    getParentUuids(key, commit),
+                    getSccUuid(key),
+                    id);
+          }
+          pushToHub(event);
+        } catch (InterruptedException e) {
+          logger.atSevere().log("Interrupted while pushing %s to EventHub.", key);
+          throw e;
+        }
+      }
+    }
+  }
+
   @VisibleForTesting
   void createAndScheduleMissingSccs(SourceChangeEventKey scc)
       throws MissingObjectException, IncorrectObjectTypeException, IOException,
@@ -472,6 +559,74 @@
     return parentIds;
   }
 
+  private UUID getSccUuid(SourceChangeEventKey key)
+      throws NoSuchEntityException, EiffelEventIdLookupException {
+    SourceChangeEventKey siblingKey = key.copy(SCC);
+    Optional<UUID> id = eventHub.getExistingId(siblingKey);
+    if (id.isPresent()) {
+      return id.get();
+    } else {
+      throw new NoSuchEntityException(
+          String.format(
+              "Unable to lookup SCC (%s) event UUID for %s even though it should exist.",
+              key.commit(), key));
+    }
+  }
+
+  /* Attempt to determine which UUID is used by other events that links to THIS event. */
+  private Optional<UUID> findUuid(SourceChangeEventKey tip, SourceChangeEventKey key)
+      throws EventParsingException, EiffelEventIdLookupException {
+    Optional<UUID> id = getUuidFromScsChangeLink(key);
+    if (id.isPresent()) {
+      return id;
+    }
+    Set<UUID> uuids = null;
+    /* Check which UUID the children of this event(commit) use to point to THIS event. */
+    try (CommitsWalker commitFinder = walkerFactory.childWalker(tip, key)) {
+      while (commitFinder.hasNext()) {
+        SourceChangeEventKey childKey = commitFinder.next().key;
+
+        Optional<List<UUID>> ids = eventHub.getParentLinks(childKey);
+        if (ids.isEmpty()) {
+          logger.atFine().log("Eiffel-event is missing for child: %s", childKey);
+          continue;
+        }
+
+        if (uuids == null) {
+          uuids = new HashSet<>(ids.get());
+        } else {
+          /* Do intersection between the set of parents for each child to determine the UUID for THIS event. */
+          uuids.retainAll(ids.get());
+        }
+        if (uuids.size() == 1) { // Found one UUID that all the processed children points to.
+          return Optional.of((UUID) uuids.toArray()[0]);
+        }
+      }
+    } catch (IOException e) {
+      throw new EventParsingException(e, "Unable to get reachable UUID for children of: %s", key);
+    }
+    if (uuids == null) {
+      /* Occur if event/s of child/s commit is missing or if the commmit of the event does not have
+      a child. In this situation we can generate a new UUID. */
+      logger.atFine().log("Could not find an event that reference: %s", key);
+      return Optional.empty();
+    }
+    /* If we found several potential UUID:s we can not generate a new UUID so we need to throw an
+    exception to prevent the creation of the event. */
+    String potentialUUID =
+        String.join(", ", uuids.stream().map(uuid -> uuid.toString()).collect(Collectors.toList()));
+    throw new EventParsingException(
+        "Found several potential UUID for %s potential UUID: %s", key, potentialUUID);
+  }
+
+  private Optional<UUID> getUuidFromScsChangeLink(SourceChangeEventKey key)
+      throws EiffelEventIdLookupException {
+    if (key.type().equals(SCC)) {
+      return eventHub.getSccEventLink(key.copy(SCS));
+    }
+    return Optional.empty();
+  }
+
   private void exceptionForMissingParent(SourceChangeEventKey key, RevCommit parent)
       throws NoSuchEntityException {
     throw new NoSuchEntityException(
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 e11ee3d..f30fe3a 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
@@ -19,7 +19,9 @@
 import static com.googlesource.gerrit.plugins.eventseiffel.eiffel.dto.EiffelEventType.SCC;
 import static com.googlesource.gerrit.plugins.eventseiffel.eiffel.dto.EiffelEventType.SCS;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestPlugin;
 import com.google.gerrit.acceptance.UseLocalDisk;
@@ -31,8 +33,13 @@
 import com.googlesource.gerrit.plugins.eventseiffel.eiffel.ArtifactEventKey;
 import com.googlesource.gerrit.plugins.eventseiffel.eiffel.CompositionDefinedEventKey;
 import com.googlesource.gerrit.plugins.eventseiffel.eiffel.SourceChangeEventKey;
+import com.googlesource.gerrit.plugins.eventseiffel.eiffel.dto.EiffelEvent;
 import com.googlesource.gerrit.plugins.eventseiffel.eiffel.dto.EiffelSourceChangeSubmittedEventInfo;
 import java.time.Instant;
+import java.util.Arrays;
+import java.util.UUID;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Before;
@@ -330,6 +337,463 @@
     assertArtcQueuedScsHandled(false, NAMESPACE);
   }
 
+  @Test
+  public void missingSucceedingSccs() throws Exception {
+    RevCommit initial = getHead(repo(), "HEAD");
+    RevCommit middle1 = pushTo(getHead()).getCommit();
+    RevCommit middle2 = pushTo(getHead()).getCommit();
+    RevCommit tip = pushTo(getHead()).getCommit();
+
+    SourceChangeEventKey scsInitial = SourceChangeEventKey.scsKey(project.get(), "master", initial);
+    SourceChangeEventKey sccInitial = scsInitial.copy(SCC);
+    SourceChangeEventKey scsMiddle1 = SourceChangeEventKey.scsKey(project.get(), "master", middle1);
+    SourceChangeEventKey sccMiddle1 = scsMiddle1.copy(SCC);
+    SourceChangeEventKey scsMiddle2 = SourceChangeEventKey.scsKey(project.get(), "master", middle2);
+    SourceChangeEventKey sccMiddle2 = scsMiddle2.copy(SCC);
+    SourceChangeEventKey scsTip = SourceChangeEventKey.scsKey(project.get(), "master", tip);
+    SourceChangeEventKey sccTip = scsTip.copy(SCC);
+    eventParser.createAndScheduleMissingScssFromBranch(project.get(), getHead());
+
+    assertEquals(8, TestEventHub.EVENTS.size());
+    assertCorrectEvent(0, sccInitial);
+    assertCorrectEvent(1, sccMiddle1);
+    assertCorrectEvent(2, sccMiddle2);
+    assertCorrectEvent(3, sccTip);
+    assertCorrectEvent(4, scsInitial);
+    assertCorrectEvent(5, scsMiddle1);
+    assertCorrectEvent(6, scsMiddle2);
+    assertCorrectEvent(7, scsTip);
+
+    EiffelEvent eventSccTip = TestEventHub.EVENTS.remove(3);
+    EiffelEvent eventSccMiddle2 = TestEventHub.EVENTS.remove(2);
+    EiffelEvent eventSccMiddle1 = TestEventHub.EVENTS.remove(1);
+    EiffelEvent eventSccInitial = TestEventHub.EVENTS.remove(0);
+
+    eventParser.fillGapsForSccFromCommit(project.get(), getHead(), tip.getName());
+
+    assertEquals(8, TestEventHub.EVENTS.size());
+    assertCorrectEvent(0, scsInitial);
+    assertCorrectEvent(1, scsMiddle1);
+    assertCorrectEvent(2, scsMiddle2);
+    assertCorrectEvent(3, scsTip);
+    assertCorrectEvent(4, sccInitial);
+    assertCorrectEvent(5, sccMiddle1);
+    assertCorrectEvent(6, sccMiddle2);
+    assertCorrectEvent(7, sccTip);
+    assertEquals(eventSccInitial.meta.id, TestEventHub.EVENTS.get(4).meta.id);
+    assertEquals(eventSccMiddle1.meta.id, TestEventHub.EVENTS.get(5).meta.id);
+    assertEquals(eventSccMiddle2.meta.id, TestEventHub.EVENTS.get(6).meta.id);
+    assertEquals(eventSccTip.meta.id, TestEventHub.EVENTS.get(7).meta.id);
+    assertParentReferences(TestEventHub.EVENTS.get(5), TestEventHub.EVENTS.get(4));
+    assertParentReferences(TestEventHub.EVENTS.get(6), TestEventHub.EVENTS.get(5));
+    assertParentReferences(TestEventHub.EVENTS.get(7), TestEventHub.EVENTS.get(6));
+  }
+
+  @Test
+  public void missingSccWithNoScs() throws Exception {
+    RevCommit initial = getHead(repo(), "HEAD");
+    RevCommit middle = pushTo(getHead()).getCommit();
+    RevCommit tip = pushTo(getHead()).getCommit();
+
+    SourceChangeEventKey scsInitial = SourceChangeEventKey.scsKey(project.get(), "master", initial);
+    SourceChangeEventKey sccInitial = scsInitial.copy(SCC);
+    SourceChangeEventKey scsMiddle = SourceChangeEventKey.scsKey(project.get(), "master", middle);
+    SourceChangeEventKey sccMiddle = scsMiddle.copy(SCC);
+    SourceChangeEventKey scsTip = SourceChangeEventKey.scsKey(project.get(), "master", tip);
+    SourceChangeEventKey sccTip = scsTip.copy(SCC);
+    eventParser.createAndScheduleMissingScssFromBranch(project.get(), getHead());
+
+    assertEquals(6, TestEventHub.EVENTS.size());
+    assertCorrectEvent(0, sccInitial);
+    assertCorrectEvent(1, sccMiddle);
+    assertCorrectEvent(2, sccTip);
+    assertCorrectEvent(3, scsInitial);
+    assertCorrectEvent(4, scsMiddle);
+    assertCorrectEvent(5, scsTip);
+
+    TestEventHub.EVENTS.remove(3);
+    EiffelEvent eventSccMiddle = TestEventHub.EVENTS.remove(1);
+    EiffelEvent eventSccInitial = TestEventHub.EVENTS.remove(0);
+
+    eventParser.fillGapsForSccFromCommit(project.get(), getHead(), tip.getName());
+
+    assertEquals(5, TestEventHub.EVENTS.size());
+    assertCorrectEvent(0, sccTip);
+    assertCorrectEvent(1, scsMiddle);
+    assertCorrectEvent(2, scsTip);
+    assertCorrectEvent(3, sccInitial);
+    assertCorrectEvent(4, sccMiddle);
+
+    assertNotEquals(eventSccInitial.meta.id, TestEventHub.EVENTS.get(3).meta.id);
+    assertEquals(eventSccMiddle.meta.id, TestEventHub.EVENTS.get(4).meta.id);
+    assertParentReferences(TestEventHub.EVENTS.get(4), TestEventHub.EVENTS.get(3));
+    assertParentReferences(TestEventHub.EVENTS.get(0), TestEventHub.EVENTS.get(4));
+  }
+
+  @Test
+  public void missingAllSccs() throws Exception {
+    RevCommit initial = getHead(repo(), "HEAD");
+    RevCommit middle = pushTo(getHead()).getCommit();
+    RevCommit tip = pushTo(getHead()).getCommit();
+
+    SourceChangeEventKey sccInitial = SourceChangeEventKey.sccKey(project.get(), "master", initial);
+    SourceChangeEventKey sccMiddle = SourceChangeEventKey.sccKey(project.get(), "master", middle);
+    SourceChangeEventKey sccTip = SourceChangeEventKey.sccKey(project.get(), "master", tip);
+
+    eventParser.fillGapsForSccFromCommit(project.get(), getHead(), tip.getName());
+
+    assertEquals(3, TestEventHub.EVENTS.size());
+    assertCorrectEvent(0, sccInitial);
+    assertCorrectEvent(1, sccMiddle);
+    assertCorrectEvent(2, sccTip);
+    assertParentReferences(TestEventHub.EVENTS.get(1), TestEventHub.EVENTS.get(0));
+    assertParentReferences(TestEventHub.EVENTS.get(2), TestEventHub.EVENTS.get(1));
+  }
+
+  @Test
+  public void missingSucceedingScss() throws Exception {
+    RevCommit initial = getHead(repo(), "HEAD");
+    RevCommit middle1 = pushTo(getHead()).getCommit();
+    RevCommit middle2 = pushTo(getHead()).getCommit();
+    RevCommit tip = pushTo(getHead()).getCommit();
+
+    SourceChangeEventKey scsInitial = SourceChangeEventKey.scsKey(project.get(), "master", initial);
+    SourceChangeEventKey sccInitial = scsInitial.copy(SCC);
+    SourceChangeEventKey scsMiddle1 = SourceChangeEventKey.scsKey(project.get(), "master", middle1);
+    SourceChangeEventKey sccMiddle1 = scsMiddle1.copy(SCC);
+    SourceChangeEventKey scsMiddle2 = SourceChangeEventKey.scsKey(project.get(), "master", middle2);
+    SourceChangeEventKey sccMiddle2 = scsMiddle2.copy(SCC);
+    SourceChangeEventKey scsTip = SourceChangeEventKey.scsKey(project.get(), "master", tip);
+    SourceChangeEventKey sccTip = scsTip.copy(SCC);
+    eventParser.createAndScheduleMissingScssFromBranch(project.get(), getHead());
+
+    assertEquals(8, TestEventHub.EVENTS.size());
+    assertCorrectEvent(0, sccInitial);
+    assertCorrectEvent(1, sccMiddle1);
+    assertCorrectEvent(2, sccMiddle2);
+    assertCorrectEvent(3, sccTip);
+    assertCorrectEvent(4, scsInitial);
+    assertCorrectEvent(5, scsMiddle1);
+    assertCorrectEvent(6, scsMiddle2);
+    assertCorrectEvent(7, scsTip);
+
+    EiffelEvent eventScsTip = TestEventHub.EVENTS.remove(7);
+    EiffelEvent eventScsMiddle2 = TestEventHub.EVENTS.remove(6);
+    EiffelEvent eventScsMiddle1 = TestEventHub.EVENTS.remove(5);
+    EiffelEvent eventScsInitial = TestEventHub.EVENTS.remove(4);
+
+    eventParser.fillGapsForScsFromBranch(project.get(), getHead());
+
+    assertEquals(8, TestEventHub.EVENTS.size());
+    assertCorrectEvent(0, sccInitial);
+    assertCorrectEvent(1, sccMiddle1);
+    assertCorrectEvent(2, sccMiddle2);
+    assertCorrectEvent(3, sccTip);
+    assertCorrectEvent(4, scsInitial);
+    assertCorrectEvent(5, scsMiddle1);
+    assertCorrectEvent(6, scsMiddle2);
+    assertCorrectEvent(7, scsTip);
+    assertNotEquals(eventScsInitial.meta.id, TestEventHub.EVENTS.get(4).meta.id);
+    assertNotEquals(eventScsMiddle1.meta.id, TestEventHub.EVENTS.get(5).meta.id);
+    assertNotEquals(eventScsMiddle2.meta.id, TestEventHub.EVENTS.get(6).meta.id);
+    assertNotEquals(eventScsTip.meta.id, TestEventHub.EVENTS.get(7).meta.id);
+    assertParentReferences(TestEventHub.EVENTS.get(5), TestEventHub.EVENTS.get(4));
+    assertParentReferences(TestEventHub.EVENTS.get(6), TestEventHub.EVENTS.get(5));
+    assertParentReferences(TestEventHub.EVENTS.get(7), TestEventHub.EVENTS.get(6));
+  }
+
+  @Test
+  public void missingScsWithMergeCommit() throws Exception {
+    RevCommit initial = getHead(repo(), "HEAD");
+    RevCommit middle1 = pushTo(getHead()).getCommit();
+    // reset HEAD in order to create a sibling of the first change
+    testRepo.reset(initial);
+    RevCommit middle2 = pushTo(getHead()).getCommit();
+
+    PushOneCommit m = pushFactory.create(admin.newIdent(), testRepo);
+    m.setParents(ImmutableList.of(middle1, middle2));
+    PushOneCommit.Result result = m.to(getHead());
+    result.assertOkStatus();
+    RevCommit tip = result.getCommit();
+
+    SourceChangeEventKey scsInitial = SourceChangeEventKey.scsKey(project.get(), "master", initial);
+    SourceChangeEventKey sccInitial = scsInitial.copy(SCC);
+    SourceChangeEventKey scsMiddle1 = SourceChangeEventKey.scsKey(project.get(), "master", middle1);
+    SourceChangeEventKey sccMiddle1 = scsMiddle1.copy(SCC);
+    SourceChangeEventKey scsMiddle2 = SourceChangeEventKey.scsKey(project.get(), "master", middle2);
+    SourceChangeEventKey sccMiddle2 = scsMiddle2.copy(SCC);
+    SourceChangeEventKey scsTip = SourceChangeEventKey.scsKey(project.get(), "master", tip);
+    SourceChangeEventKey sccTip = scsTip.copy(SCC);
+    eventParser.createAndScheduleMissingScssFromBranch(project.get(), getHead());
+
+    assertEquals(8, TestEventHub.EVENTS.size());
+    assertCorrectEvent(0, sccInitial);
+    assertCorrectEvent(1, sccMiddle1);
+    assertCorrectEvent(2, sccMiddle2);
+    assertCorrectEvent(3, sccTip);
+    assertCorrectEvent(4, scsInitial);
+    assertCorrectEvent(5, scsMiddle1);
+    assertCorrectEvent(6, scsMiddle2);
+    assertCorrectEvent(7, scsTip);
+
+    EiffelEvent eventScsTip = TestEventHub.EVENTS.remove(7);
+    EiffelEvent eventScsMiddle2 = TestEventHub.EVENTS.remove(6);
+    EiffelEvent eventScsMiddle1 = TestEventHub.EVENTS.remove(5);
+    EiffelEvent eventScsInitial = TestEventHub.EVENTS.remove(4);
+
+    eventParser.fillGapsForScsFromBranch(project.get(), getHead());
+
+    assertEquals(8, TestEventHub.EVENTS.size());
+    assertCorrectEvent(0, sccInitial);
+    assertCorrectEvent(1, sccMiddle1);
+    assertCorrectEvent(2, sccMiddle2);
+    assertCorrectEvent(3, sccTip);
+    assertCorrectEvent(4, scsInitial);
+    assertCorrectEvent(5, scsMiddle1);
+    assertCorrectEvent(6, scsMiddle2);
+    assertCorrectEvent(7, scsTip);
+    assertNotEquals(eventScsInitial.meta.id, TestEventHub.EVENTS.get(4).meta.id);
+    assertNotEquals(eventScsMiddle1.meta.id, TestEventHub.EVENTS.get(5).meta.id);
+    assertNotEquals(eventScsMiddle2.meta.id, TestEventHub.EVENTS.get(6).meta.id);
+    assertNotEquals(eventScsTip.meta.id, TestEventHub.EVENTS.get(7).meta.id);
+    assertParentReferences(TestEventHub.EVENTS.get(5), TestEventHub.EVENTS.get(4));
+    assertParentReferences(TestEventHub.EVENTS.get(6), TestEventHub.EVENTS.get(4));
+    assertParentReferences(
+        TestEventHub.EVENTS.get(7), TestEventHub.EVENTS.get(5), TestEventHub.EVENTS.get(6));
+  }
+
+  @Test
+  public void missingScsWithMergeCommit3Parents() throws Exception {
+    RevCommit initial = getHead(repo(), "HEAD");
+    RevCommit middle1 = testRepo.commit().parent(initial).create();
+    RevCommit middle2 = testRepo.commit().parent(initial).create();
+    RevCommit middle3 = testRepo.commit().parent(initial).create();
+
+    PushOneCommit m = pushFactory.create(admin.newIdent(), testRepo);
+    m.setParents(ImmutableList.of(middle1, middle2, middle3));
+    PushOneCommit.Result result = m.to(getHead());
+    result.assertOkStatus();
+    RevCommit tip = result.getCommit();
+
+    SourceChangeEventKey scsInitial = SourceChangeEventKey.scsKey(project.get(), "master", initial);
+    SourceChangeEventKey sccInitial = scsInitial.copy(SCC);
+    SourceChangeEventKey scsMiddle1 = SourceChangeEventKey.scsKey(project.get(), "master", middle1);
+    SourceChangeEventKey sccMiddle1 = scsMiddle1.copy(SCC);
+    SourceChangeEventKey scsMiddle2 = SourceChangeEventKey.scsKey(project.get(), "master", middle2);
+    SourceChangeEventKey sccMiddle2 = scsMiddle2.copy(SCC);
+    SourceChangeEventKey scsMiddle3 = SourceChangeEventKey.scsKey(project.get(), "master", middle3);
+    SourceChangeEventKey sccMiddle3 = scsMiddle3.copy(SCC);
+    SourceChangeEventKey scsTip = SourceChangeEventKey.scsKey(project.get(), "master", tip);
+    SourceChangeEventKey sccTip = scsTip.copy(SCC);
+    eventParser.createAndScheduleMissingScssFromBranch(project.get(), getHead());
+
+    assertEquals(10, TestEventHub.EVENTS.size());
+    assertCorrectEvent(0, sccInitial);
+    assertCorrectEvent(1, sccMiddle1);
+    assertCorrectEvent(2, sccMiddle2);
+    assertCorrectEvent(3, sccMiddle3);
+    assertCorrectEvent(4, sccTip);
+    assertCorrectEvent(5, scsInitial);
+    assertCorrectEvent(6, scsMiddle1);
+    assertCorrectEvent(7, scsMiddle2);
+    assertCorrectEvent(8, scsMiddle3);
+    assertCorrectEvent(9, scsTip);
+
+    EiffelEvent eventScsTip = TestEventHub.EVENTS.remove(9);
+    EiffelEvent eventScsMiddle3 = TestEventHub.EVENTS.remove(8);
+    EiffelEvent eventScsMiddle2 = TestEventHub.EVENTS.remove(7);
+    EiffelEvent eventScsMiddle1 = TestEventHub.EVENTS.remove(6);
+    EiffelEvent eventScsInitial = TestEventHub.EVENTS.remove(5);
+
+    eventParser.fillGapsForScsFromBranch(project.get(), getHead());
+
+    assertEquals(10, TestEventHub.EVENTS.size());
+    assertCorrectEvent(0, sccInitial);
+    assertCorrectEvent(1, sccMiddle1);
+    assertCorrectEvent(2, sccMiddle2);
+    assertCorrectEvent(3, sccMiddle3);
+    assertCorrectEvent(4, sccTip);
+    assertCorrectEvent(5, scsInitial);
+    assertCorrectEvent(6, scsMiddle1);
+    assertCorrectEvent(7, scsMiddle2);
+    assertCorrectEvent(8, scsMiddle3);
+    assertCorrectEvent(9, scsTip);
+    assertNotEquals(eventScsInitial.meta.id, TestEventHub.EVENTS.get(5).meta.id);
+    assertNotEquals(eventScsMiddle1.meta.id, TestEventHub.EVENTS.get(6).meta.id);
+    assertNotEquals(eventScsMiddle2.meta.id, TestEventHub.EVENTS.get(7).meta.id);
+    assertNotEquals(eventScsMiddle3.meta.id, TestEventHub.EVENTS.get(8).meta.id);
+    assertNotEquals(eventScsTip.meta.id, TestEventHub.EVENTS.get(9).meta.id);
+    assertParentReferences(TestEventHub.EVENTS.get(6), TestEventHub.EVENTS.get(5));
+    assertParentReferences(TestEventHub.EVENTS.get(7), TestEventHub.EVENTS.get(5));
+    assertParentReferences(TestEventHub.EVENTS.get(8), TestEventHub.EVENTS.get(5));
+    assertParentReferences(
+        TestEventHub.EVENTS.get(9),
+        TestEventHub.EVENTS.get(6),
+        TestEventHub.EVENTS.get(7),
+        TestEventHub.EVENTS.get(8));
+  }
+
+  @Test
+  public void missingScsWithCommonParents() throws Exception {
+    RevCommit initial = getHead(repo(), "HEAD");
+    RevCommit middle1 = testRepo.commit().parent(initial).create();
+    RevCommit middle2 = testRepo.commit().parent(initial).create();
+    RevCommit middle3 = testRepo.commit().parent(middle1).parent(middle2).create();
+    RevCommit middle4 = testRepo.commit().parent(middle1).parent(middle2).create();
+
+    PushOneCommit m = pushFactory.create(admin.newIdent(), testRepo);
+    m.setParents(ImmutableList.of(middle3, middle4));
+    PushOneCommit.Result result = m.to(getHead());
+    result.assertOkStatus();
+    RevCommit tip = result.getCommit();
+
+    SourceChangeEventKey scsInitial = SourceChangeEventKey.scsKey(project.get(), "master", initial);
+    SourceChangeEventKey sccInitial = scsInitial.copy(SCC);
+    SourceChangeEventKey scsMiddle1 = SourceChangeEventKey.scsKey(project.get(), "master", middle1);
+    SourceChangeEventKey sccMiddle1 = scsMiddle1.copy(SCC);
+    SourceChangeEventKey scsMiddle2 = SourceChangeEventKey.scsKey(project.get(), "master", middle2);
+    SourceChangeEventKey sccMiddle2 = scsMiddle2.copy(SCC);
+    SourceChangeEventKey scsMiddle3 = SourceChangeEventKey.scsKey(project.get(), "master", middle3);
+    SourceChangeEventKey sccMiddle3 = scsMiddle3.copy(SCC);
+    SourceChangeEventKey scsMiddle4 = SourceChangeEventKey.scsKey(project.get(), "master", middle4);
+    SourceChangeEventKey sccMiddle4 = scsMiddle4.copy(SCC);
+    SourceChangeEventKey scsTip = SourceChangeEventKey.scsKey(project.get(), "master", tip);
+    SourceChangeEventKey sccTip = scsTip.copy(SCC);
+    eventParser.createAndScheduleMissingScssFromBranch(project.get(), getHead());
+
+    assertEquals(12, TestEventHub.EVENTS.size());
+    assertCorrectEvent(0, sccInitial);
+    assertCorrectEvent(1, sccMiddle1);
+    assertCorrectEvent(2, sccMiddle2);
+    assertCorrectEvent(3, sccMiddle3);
+    assertCorrectEvent(4, sccMiddle4);
+    assertCorrectEvent(5, sccTip);
+    assertCorrectEvent(6, scsInitial);
+    assertCorrectEvent(7, scsMiddle1);
+    assertCorrectEvent(8, scsMiddle2);
+    assertCorrectEvent(9, scsMiddle3);
+    assertCorrectEvent(10, scsMiddle4);
+    assertCorrectEvent(11, scsTip);
+
+    EiffelEvent eventScsMiddle1 = TestEventHub.EVENTS.remove(7);
+
+    EventParsingException thrown =
+        assertThrows(
+            EventParsingException.class,
+            () -> eventParser.fillGapsForScsFromBranch(project.get(), getHead()));
+    UUID id1 = eventScsMiddle1.meta.id;
+    UUID id2 = TestEventHub.EVENTS.get(7).meta.id;
+    String expectedMessage =
+        String.format(
+            "Found several potential UUID for %s potential UUID: (%2$s, %3$s|%3$s, %2$s)$",
+            Pattern.quote(scsMiddle1.toString()), id1, id2);
+    assertThat(thrown).hasMessageThat().matches(expectedMessage);
+
+    assertEquals(11, TestEventHub.EVENTS.size());
+    assertCorrectEvent(0, sccInitial);
+    assertCorrectEvent(1, sccMiddle1);
+    assertCorrectEvent(2, sccMiddle2);
+    assertCorrectEvent(3, sccMiddle3);
+    assertCorrectEvent(4, sccMiddle4);
+    assertCorrectEvent(5, sccTip);
+    assertCorrectEvent(6, scsInitial);
+    assertCorrectEvent(7, scsMiddle2);
+    assertCorrectEvent(8, scsMiddle3);
+    assertCorrectEvent(9, scsMiddle4);
+    assertCorrectEvent(10, scsTip);
+  }
+
+  @Test
+  public void missingScsWithCircularCommonParents() throws Exception {
+    RevCommit initial = getHead(repo(), "HEAD");
+    RevCommit middle1 = testRepo.commit().parent(initial).create();
+    RevCommit middle2 = testRepo.commit().parent(initial).create();
+    RevCommit middle3 = testRepo.commit().parent(initial).create();
+    RevCommit middle4 = testRepo.commit().parent(initial).create();
+    RevCommit middle5 = testRepo.commit().parent(middle1).parent(middle2).parent(middle3).create();
+    RevCommit middle6 = testRepo.commit().parent(middle2).parent(middle3).parent(middle4).create();
+    RevCommit middle7 = testRepo.commit().parent(middle3).parent(middle4).parent(middle1).create();
+
+    PushOneCommit m = pushFactory.create(admin.newIdent(), testRepo);
+    m.setParents(ImmutableList.of(middle5, middle6, middle7));
+    PushOneCommit.Result result = m.to(getHead());
+    result.assertOkStatus();
+    RevCommit tip = result.getCommit();
+
+    SourceChangeEventKey scsInitial = SourceChangeEventKey.scsKey(project.get(), "master", initial);
+    SourceChangeEventKey sccInitial = scsInitial.copy(SCC);
+    SourceChangeEventKey scsMiddle1 = SourceChangeEventKey.scsKey(project.get(), "master", middle1);
+    SourceChangeEventKey sccMiddle1 = scsMiddle1.copy(SCC);
+    SourceChangeEventKey scsMiddle2 = SourceChangeEventKey.scsKey(project.get(), "master", middle2);
+    SourceChangeEventKey sccMiddle2 = scsMiddle2.copy(SCC);
+    SourceChangeEventKey scsMiddle3 = SourceChangeEventKey.scsKey(project.get(), "master", middle3);
+    SourceChangeEventKey sccMiddle3 = scsMiddle3.copy(SCC);
+    SourceChangeEventKey scsMiddle4 = SourceChangeEventKey.scsKey(project.get(), "master", middle4);
+    SourceChangeEventKey sccMiddle4 = scsMiddle4.copy(SCC);
+    SourceChangeEventKey scsMiddle5 = SourceChangeEventKey.scsKey(project.get(), "master", middle5);
+    SourceChangeEventKey sccMiddle5 = scsMiddle5.copy(SCC);
+    SourceChangeEventKey scsMiddle6 = SourceChangeEventKey.scsKey(project.get(), "master", middle6);
+    SourceChangeEventKey sccMiddle6 = scsMiddle6.copy(SCC);
+    SourceChangeEventKey scsMiddle7 = SourceChangeEventKey.scsKey(project.get(), "master", middle7);
+    SourceChangeEventKey sccMiddle7 = scsMiddle7.copy(SCC);
+    SourceChangeEventKey scsTip = SourceChangeEventKey.scsKey(project.get(), "master", tip);
+    SourceChangeEventKey sccTip = scsTip.copy(SCC);
+    eventParser.createAndScheduleMissingScssFromBranch(project.get(), getHead());
+
+    assertEquals(18, TestEventHub.EVENTS.size());
+    assertCorrectEvent(0, sccInitial);
+    assertCorrectEvent(1, sccMiddle1);
+    assertCorrectEvent(2, sccMiddle2);
+    assertCorrectEvent(3, sccMiddle3);
+    assertCorrectEvent(4, sccMiddle4);
+    assertCorrectEvent(5, sccMiddle5);
+    assertCorrectEvent(6, sccMiddle6);
+    assertCorrectEvent(7, sccMiddle7);
+    assertCorrectEvent(8, sccTip);
+    assertCorrectEvent(9, scsInitial);
+    assertCorrectEvent(10, scsMiddle1);
+    assertCorrectEvent(11, scsMiddle2);
+    assertCorrectEvent(12, scsMiddle3);
+    assertCorrectEvent(13, scsMiddle4);
+    assertCorrectEvent(14, scsMiddle5);
+    assertCorrectEvent(15, scsMiddle6);
+    assertCorrectEvent(16, scsMiddle7);
+    assertCorrectEvent(17, scsTip);
+
+    EiffelEvent eventScsMiddle3 = TestEventHub.EVENTS.remove(12);
+
+    eventParser.fillGapsForScsFromBranch(project.get(), getHead());
+
+    assertEquals(18, TestEventHub.EVENTS.size());
+    assertCorrectEvent(0, sccInitial);
+    assertCorrectEvent(1, sccMiddle1);
+    assertCorrectEvent(2, sccMiddle2);
+    assertCorrectEvent(3, sccMiddle3);
+    assertCorrectEvent(4, sccMiddle4);
+    assertCorrectEvent(5, sccMiddle5);
+    assertCorrectEvent(6, sccMiddle6);
+    assertCorrectEvent(7, sccMiddle7);
+    assertCorrectEvent(8, sccTip);
+    assertCorrectEvent(9, scsInitial);
+    assertCorrectEvent(10, scsMiddle1);
+    assertCorrectEvent(11, scsMiddle2);
+    assertCorrectEvent(12, scsMiddle4);
+    assertCorrectEvent(13, scsMiddle5);
+    assertCorrectEvent(14, scsMiddle6);
+    assertCorrectEvent(15, scsMiddle7);
+    assertCorrectEvent(16, scsTip);
+    assertCorrectEvent(17, scsMiddle3);
+    assertEquals(eventScsMiddle3.meta.id, TestEventHub.EVENTS.get(17).meta.id);
+    assertParentReferences(TestEventHub.EVENTS.get(17), TestEventHub.EVENTS.get(9));
+  }
+
+  private void assertParentReferences(EiffelEvent child, EiffelEvent... parents) {
+    for (EiffelEvent parent : parents) {
+      assertThat(Arrays.stream(child.links).map(link -> link.target).collect(Collectors.toList()))
+          .contains(parent.meta.id);
+    }
+  }
+
   private void assertArtcQueuedScsHandled(boolean annotated) throws Exception {
     assertArtcQueuedScsHandled(annotated, "localhost");
   }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/ParsingQueuePersistenceIT.java b/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/ParsingQueuePersistenceIT.java
index 8c9ed72..a5a2cc8 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/ParsingQueuePersistenceIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/ParsingQueuePersistenceIT.java
@@ -198,6 +198,15 @@
     public void createAndScheduleSccFromCommit(String repoName, String branchRef, String commit) {}
 
     @Override
+    public void fillGapsForSccFromBranch(String repoName, String branchRef) {}
+
+    @Override
+    public void fillGapsForSccFromCommit(String repoName, String branchRef, String commit) {}
+
+    @Override
+    public void fillGapsForScsFromBranch(String repoName, String branchRef) {}
+
+    @Override
     public void createAndScheduleMissingScssFromBranch(String repoName, String branchRef) {}
 
     @Override