Refactor RefUpdateHandler

Split the logic from RefUpdateHandler into handling a separate
EventHandler to support injecting update requests from outside the
GitReferenceUpdatedListener.

Change-Id: I19cdb1dadce43d7461f4df04884e6e5dc1c66afc
diff --git a/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/EventHandler.java b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/EventHandler.java
new file mode 100644
index 0000000..5925ec0
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/EventHandler.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2015 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.repositoryuse;
+
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+
+import com.google.inject.Inject;
+
+public class EventHandler implements GitReferenceUpdatedListener {
+  RefUpdateHandlerFactory refUpdateHandlerFactory;
+
+  @Inject
+  public EventHandler(RefUpdateHandlerFactory refUpdateHandlerFactory) {
+    this.refUpdateHandlerFactory = refUpdateHandlerFactory;
+  }
+
+  @Override
+  public void onGitReferenceUpdated(Event event) {
+    RefUpdate update = new RefUpdate(event);
+    refUpdateHandlerFactory.create(update).run();
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/Module.java b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/Module.java
index 4c7b822..1a97a63 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/Module.java
@@ -20,16 +20,20 @@
 import com.google.inject.AbstractModule;
 import com.google.inject.Provides;
 import com.google.inject.Singleton;
+import com.google.inject.assistedinject.FactoryModuleBuilder;
 import com.google.inject.internal.UniqueAnnotations;
 
 public class Module extends AbstractModule {
   @Override
   protected void configure() {
     DynamicSet.bind(binder(), GitReferenceUpdatedListener.class)
-        .to(RefUpdateHandler.class);
+        .to(EventHandler.class);
     requestStaticInjection(Config.class);
     requestStaticInjection(Ref.Table.class);
     requestStaticInjection(Usage.Table.class);
+    install(new FactoryModuleBuilder()
+        .implement(RefUpdateHandler.class, RefUpdateHandlerImpl.class)
+        .build(RefUpdateHandlerFactory.class));
     bind(LifecycleListener.class).annotatedWith(UniqueAnnotations.create())
         .to(SQLDriver.class);
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/RefUpdate.java b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/RefUpdate.java
new file mode 100644
index 0000000..a9a3923
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/RefUpdate.java
@@ -0,0 +1,73 @@
+// Copyright (C) 2015 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.repositoryuse;
+
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+
+import org.eclipse.jgit.lib.ObjectId;
+
+public class RefUpdate {
+  private String projectName;
+  private String refName;
+  private String oldObjectId;
+  private String newObjectId;
+  private boolean isCreate;
+  private boolean isDelete;
+
+  public RefUpdate(String projectName, String refName, String oldObjectId,
+      String newObjectId) {
+    this.projectName = projectName;
+    this.refName = refName;
+    this.oldObjectId = oldObjectId;
+    this.newObjectId = newObjectId;
+    this.isCreate = oldObjectId.equals(ObjectId.zeroId().name());
+    this.isDelete = newObjectId.equals(ObjectId.zeroId().name());
+  }
+
+  public RefUpdate(GitReferenceUpdatedListener.Event event) {
+    this.projectName = event.getProjectName();
+    this.refName = event.getRefName();
+    this.oldObjectId = event.getOldObjectId();
+    this.newObjectId = event.getNewObjectId();
+    // TODO: could use event.isCreate() / isDelete() hree, but keep
+    // some compatibility with Gerrit 2.11
+    this.isCreate = oldObjectId.equals(ObjectId.zeroId().name());
+    this.isDelete = newObjectId.equals(ObjectId.zeroId().name());
+  }
+
+  public String getProjectName() {
+    return projectName;
+  }
+
+  public String getRefName() {
+    return refName;
+  }
+
+  public String getOldObjectId() {
+    return oldObjectId;
+  }
+
+  public String getNewObjectId() {
+    return newObjectId;
+  }
+
+  public boolean isCreate() {
+    return isCreate;
+  }
+
+  public boolean isDelete() {
+    return isDelete;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/RefUpdateHandler.java b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/RefUpdateHandler.java
index f3685b2..e56417d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/RefUpdateHandler.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/RefUpdateHandler.java
@@ -14,279 +14,6 @@
 
 package com.googlesource.gerrit.plugins.repositoryuse;
 
-import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.config.CanonicalWebUrl;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.inject.Inject;
-
-import org.eclipse.jgit.diff.DiffEntry;
-import org.eclipse.jgit.diff.DiffFormatter;
-import org.eclipse.jgit.diff.RawTextComparator;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.FileMode;
-import org.eclipse.jgit.lib.ObjectLoader;
-import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevTree;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.submodule.SubmoduleWalk;
-import org.eclipse.jgit.treewalk.TreeWalk;
-import org.eclipse.jgit.util.io.DisabledOutputStream;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-public class RefUpdateHandler implements GitReferenceUpdatedListener {
-
-  private static final Logger log =
-      LoggerFactory.getLogger(RefUpdateHandler.class);
-  private final GitRepositoryManager repoManager;
-  private final String serverName;
-
-  @Inject
-  RefUpdateHandler(GitRepositoryManager repoManager,
-      @CanonicalWebUrl String canonicalWebUrl) {
-    this.repoManager = repoManager;
-    if (canonicalWebUrl != null) {
-      try {
-        URL url = new URL(canonicalWebUrl);
-        canonicalWebUrl = url.getHost();
-      } catch (MalformedURLException e) {
-        log.warn("Could not parse canonicalWebUrl", e);
-      }
-    }
-    this.serverName = canonicalWebUrl;
-  }
-
-  @Override
-  public void onGitReferenceUpdated(Event event) {
-    if (event.isDelete() && event.getRefName().startsWith(Constants.R_HEADS)
-        || event.getRefName().startsWith(Constants.R_TAGS)) {
-      // Ref was deleted... clean up any references
-      Ref ref = Ref.fetchByRef(event.getProjectName(), event.getRefName());
-      if (ref != null) {
-        ref.delete();
-      }
-      if (event.getRefName().startsWith(Constants.R_HEADS)) {
-        // Also clean up uses from this ref
-        Usage.deleteByBranch(getCanonicalProject(event.getProjectName()),
-            event.getRefName());
-      }
-    } else if (event.getRefName().startsWith(Constants.R_TAGS)) {
-      Ref updatedRef = new Ref(event.getProjectName(), event.getRefName(),
-          event.getNewObjectId());
-      updatedRef.save();
-    } else if (event.getRefName().startsWith(Constants.R_HEADS)) {
-      Ref updatedRef = new Ref(event.getProjectName(), event.getRefName(),
-          event.getNewObjectId());
-      updatedRef.save();
-      Project.NameKey nameKey = new Project.NameKey(event.getProjectName());
-      try {
-        if (Config.refreshAllSubmodules() || event.isCreate()
-            || isSubmoduleUpdate(event, nameKey)) {
-          Map<String, String> submodules = getSubmodules(event, nameKey);
-          updateProjects(event.getProjectName(), event.getRefName(),
-              submodules);
-        }
-        if (Config.parseManifests()) {
-          parseManifests(event, nameKey);
-        }
-      } catch (IOException e) {
-        log.error(e.getMessage(), e);
-      }
-    }
-  }
-
-  private void parseManifests(Event event, Project.NameKey project)
-      throws RepositoryNotFoundException, IOException {
-    if (event.isDelete()) {
-      return;
-    }
-    try (Repository repo = repoManager.openRepository(project)) {
-      try (RevWalk walk = new RevWalk(repo); TreeWalk tw = new TreeWalk(repo)) {
-        RevCommit commit =
-            walk.parseCommit(repo.resolve(event.getNewObjectId()));
-
-        tw.setRecursive(false);
-        tw.addTree(commit.getTree());
-        ObjectReader or = tw.getObjectReader();
-        while (tw.next()) {
-          String path = tw.getPathString();
-          if (path.endsWith(".xml")) {
-            ManifestParser mp = new ManifestParser();
-            ObjectLoader ol = or.open(tw.getObjectId(0));
-            if (!ol.isLarge()) {
-              Map<String, String> tmp = mp.parseManifest(ol.getBytes());
-              HashMap<String, String> projects = new HashMap<>();
-              for (String key : tmp.keySet()) {
-                projects
-                    .put(
-                        normalizePath(String.format("%s:%s",
-                            event.getProjectName(), path), key, true),
-                    tmp.get(key));
-              }
-              updateProjects(
-                  String.format("%s:%s", event.getProjectName(), path),
-                  event.getRefName(), projects);
-            } else {
-              log.warn(String.format("%s is too large, skipping manifest parse",
-                  tw.getPathString()));
-            }
-          }
-        }
-      }
-    }
-  }
-
-  /**
-   * Has a submodule been updated?
-   *
-   * @param event the Event
-   * @return True if a submodule update occurred, otherwise False.
-   */
-  private boolean isSubmoduleUpdate(Event event, Project.NameKey project)
-      throws RepositoryNotFoundException, IOException {
-    if (event.isDelete()) {
-      return false;
-    }
-    try (Repository repo = repoManager.openRepository(project)) {
-      try (RevWalk walk = new RevWalk(repo);
-          DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE)) {
-        RevTree aTree = null;
-        if (!event.isCreate()) {
-          // If this is a new ref, we can't get the original commit.
-          // We can still use the DiffFormatter to give us what changed
-          // by passing null, however.
-          RevCommit aCommit =
-              walk.parseCommit(repo.resolve(event.getOldObjectId()));
-          aTree = aCommit.getTree();
-        }
-        RevCommit bCommit =
-            walk.parseCommit(repo.resolve(event.getNewObjectId()));
-        RevTree bTree = bCommit.getTree();
-
-        df.setRepository(repo);
-        df.setDiffComparator(RawTextComparator.DEFAULT);
-        df.setDetectRenames(true);
-        List<DiffEntry> diffEntries = df.scan(aTree, bTree);
-        for (DiffEntry de : diffEntries) {
-          FileMode oldMode = de.getOldMode();
-          FileMode newMode = de.getNewMode();
-          if ((oldMode != null && oldMode == FileMode.GITLINK)
-              || (newMode != null && newMode == FileMode.GITLINK)) {
-            return true;
-          }
-        }
-      }
-    }
-    return false;
-  }
-
-  private Map<String, String> getSubmodules(Event event,
-      Project.NameKey project) throws RepositoryNotFoundException, IOException {
-    HashMap<String, String> submodules = new HashMap<>();
-    try (Repository repo = repoManager.openRepository(project)) {
-      try (RevWalk walk = new RevWalk(repo);
-          SubmoduleWalk sw = new SubmoduleWalk(repo)) {
-        RevCommit commit =
-            walk.parseCommit(repo.resolve(event.getNewObjectId()));
-        sw.setTree(commit.getTree());
-        sw.setRootTree(commit.getTree());
-        while (sw.next()) {
-          submodules.put(
-              normalizePath(event.getProjectName(), sw.getModulesUrl(), false),
-              sw.getObjectId().name());
-        }
-      } catch (ConfigInvalidException e) {
-        log.warn("Invalid .gitmodules configuration while parsing "
-            + event.getProjectName());
-      }
-    }
-    return submodules;
-  }
-
-  private void updateProjects(String project, String branch,
-      Map<String, String> projects) {
-    String canonicalProject = getCanonicalProject(project);
-    List<Usage> uses = Usage.fetchByProject(canonicalProject, branch);
-    for (Usage use : uses) {
-      if (!projects.containsKey(use.getDestination())) {
-        // No longer exists; delete.
-        use.delete();
-      } else {
-        // Update SHA1 here.
-        use.setRef(projects.get(use.getDestination()));
-        use.save();
-        projects.remove(use.getDestination());
-      }
-    }
-    // At this point, submodules only contains new elements.
-    // Create them.
-    for (String key : projects.keySet()) {
-      Usage use = new Usage(canonicalProject, branch, key, projects.get(key));
-      use.save();
-    }
-  }
-
-  private String getCanonicalProject(String project) {
-    String canonicalProject =
-        String.format("https://%s/%s", serverName, project);
-    try {
-      URL url = new URL(canonicalProject);
-      canonicalProject = url.getHost() + url.getPath();
-    } catch (MalformedURLException e) {
-      log.warn("Could not parse project as URL: " + canonicalProject);
-    }
-    return canonicalProject;
-  }
-
-  private String normalizePath(String project, String destination,
-      boolean isManifest) {
-    String originalProject =
-        isManifest ? project.substring(0, project.lastIndexOf(":")) : project;
-
-    // Handle relative and absolute paths on the same server
-    if (destination.startsWith("/")) {
-      if (serverName != null) {
-        destination = serverName + destination;
-      } else {
-        log.warn("Could not parse absolute path; canonicalWebUrl not set");
-      }
-    } else if (destination.startsWith(".")) {
-      if (serverName != null) {
-        Path path = Paths.get(String.format("/%s/%s", project, destination));
-        destination = serverName + path.normalize().toString();
-      } else {
-        log.warn("Could not parse relative path; canonicalWebUrl not set");
-      }
-    } else if (!destination.matches("^[^:]+://.*")) {
-      if (serverName != null) {
-        destination = serverName + "/" + originalProject + "/" + destination;
-      } else {
-        log.warn("Could not parse relative path; canonicalWebURl not set");
-      }
-    }
-
-    try {
-      // Replace the protocol with a known scheme, to avoid angering URL
-      destination = destination.replaceFirst("^[^:]+://", "");
-      URL url = new URL("https://" + destination);
-      destination = url.getHost() + url.getPath();
-    } catch (MalformedURLException e) {
-      log.warn("Could not parse destination as URL: " + destination);
-    }
-    return destination;
-  }
+public interface RefUpdateHandler {
+  void run();
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/RefUpdateHandlerFactory.java b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/RefUpdateHandlerFactory.java
new file mode 100644
index 0000000..8c8cedb
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/RefUpdateHandlerFactory.java
@@ -0,0 +1,19 @@
+// Copyright (C) 2015 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.repositoryuse;
+
+public interface RefUpdateHandlerFactory {
+  RefUpdateHandler create(RefUpdate update);
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/RefUpdateHandlerImpl.java b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/RefUpdateHandlerImpl.java
new file mode 100644
index 0000000..638c267
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/RefUpdateHandlerImpl.java
@@ -0,0 +1,295 @@
+// Copyright (C) 2015 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.repositoryuse;
+
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.diff.RawTextComparator;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.submodule.SubmoduleWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.util.io.DisabledOutputStream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class RefUpdateHandlerImpl implements RefUpdateHandler {
+  private static final Logger log =
+      LoggerFactory.getLogger(RefUpdateHandlerImpl.class);
+
+  private RefUpdate event;
+  private final GitRepositoryManager repoManager;
+  private final String serverName;
+
+  @Inject
+  public RefUpdateHandlerImpl(@Assisted RefUpdate event,
+      GitRepositoryManager repoManager,
+      @CanonicalWebUrl String canonicalWebUrl) {
+    this.event = event;
+    this.repoManager = repoManager;
+    if (canonicalWebUrl != null) {
+      try {
+        URL url = new URL(canonicalWebUrl);
+        canonicalWebUrl = url.getHost();
+      } catch (MalformedURLException e) {
+        log.warn("Could not parse canonicalWebUrl", e);
+      }
+    }
+    this.serverName = canonicalWebUrl;
+  }
+
+  @Override
+  public void run() {
+    if (event.isDelete() && event.getRefName().startsWith(Constants.R_HEADS)
+        || event.getRefName().startsWith(Constants.R_TAGS)) {
+      // Ref was deleted... clean up any references
+      Ref ref = Ref.fetchByRef(event.getProjectName(), event.getRefName());
+      if (ref != null) {
+        ref.delete();
+      }
+      if (event.getRefName().startsWith(Constants.R_HEADS)) {
+        // Also clean up uses from this ref
+        Usage.deleteByBranch(getCanonicalProject(event.getProjectName()),
+            event.getRefName());
+      }
+    } else if (event.getRefName().startsWith(Constants.R_TAGS)) {
+      Ref updatedRef = new Ref(event.getProjectName(), event.getRefName(),
+          event.getNewObjectId());
+      updatedRef.save();
+    } else if (event.getRefName().startsWith(Constants.R_HEADS)) {
+      Ref updatedRef = new Ref(event.getProjectName(), event.getRefName(),
+          event.getNewObjectId());
+      updatedRef.save();
+      Project.NameKey nameKey = new Project.NameKey(event.getProjectName());
+      try {
+        if (Config.refreshAllSubmodules() || event.isCreate()
+            || isSubmoduleUpdate(event, nameKey)) {
+          Map<String, String> submodules = getSubmodules(event, nameKey);
+          updateProjects(event.getProjectName(), event.getRefName(),
+              submodules);
+        }
+        if (Config.parseManifests()) {
+          parseManifests(event, nameKey);
+        }
+      } catch (IOException e) {
+        log.error(e.getMessage(), e);
+      }
+    }
+  }
+
+  private void parseManifests(RefUpdate event, Project.NameKey project)
+      throws RepositoryNotFoundException, IOException {
+    if (event.isDelete()) {
+      return;
+    }
+    try (Repository repo = repoManager.openRepository(project)) {
+      try (RevWalk walk = new RevWalk(repo); TreeWalk tw = new TreeWalk(repo)) {
+        RevCommit commit =
+            walk.parseCommit(repo.resolve(event.getNewObjectId()));
+
+        tw.setRecursive(false);
+        tw.addTree(commit.getTree());
+        ObjectReader or = tw.getObjectReader();
+        while (tw.next()) {
+          String path = tw.getPathString();
+          if (path.endsWith(".xml")) {
+            ManifestParser mp = new ManifestParser();
+            ObjectLoader ol = or.open(tw.getObjectId(0));
+            if (!ol.isLarge()) {
+              Map<String, String> tmp = mp.parseManifest(ol.getBytes());
+              HashMap<String, String> projects = new HashMap<>();
+              for (String key : tmp.keySet()) {
+                projects
+                    .put(
+                        normalizePath(String.format("%s:%s",
+                            event.getProjectName(), path), key, true),
+                    tmp.get(key));
+              }
+              updateProjects(
+                  String.format("%s:%s", event.getProjectName(), path),
+                  event.getRefName(), projects);
+            } else {
+              log.warn(String.format("%s is too large, skipping manifest parse",
+                  tw.getPathString()));
+            }
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Has a submodule been updated?
+   *
+   * @param event the Event
+   * @return True if a submodule update occurred, otherwise False.
+   */
+  private boolean isSubmoduleUpdate(RefUpdate event, Project.NameKey project)
+      throws RepositoryNotFoundException, IOException {
+    if (event.isDelete()) {
+      return false;
+    }
+    try (Repository repo = repoManager.openRepository(project)) {
+      try (RevWalk walk = new RevWalk(repo);
+          DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE)) {
+        RevTree aTree = null;
+        if (!event.isCreate()) {
+          // If this is a new ref, we can't get the original commit.
+          // We can still use the DiffFormatter to give us what changed
+          // by passing null, however.
+          RevCommit aCommit =
+              walk.parseCommit(repo.resolve(event.getOldObjectId()));
+          aTree = aCommit.getTree();
+        }
+        RevCommit bCommit =
+            walk.parseCommit(repo.resolve(event.getNewObjectId()));
+        RevTree bTree = bCommit.getTree();
+
+        df.setRepository(repo);
+        df.setDiffComparator(RawTextComparator.DEFAULT);
+        df.setDetectRenames(true);
+        List<DiffEntry> diffEntries = df.scan(aTree, bTree);
+        for (DiffEntry de : diffEntries) {
+          FileMode oldMode = de.getOldMode();
+          FileMode newMode = de.getNewMode();
+          if ((oldMode != null && oldMode == FileMode.GITLINK)
+              || (newMode != null && newMode == FileMode.GITLINK)) {
+            return true;
+          }
+        }
+      }
+    }
+    return false;
+  }
+
+  private Map<String, String> getSubmodules(RefUpdate event,
+      Project.NameKey project) throws RepositoryNotFoundException, IOException {
+    HashMap<String, String> submodules = new HashMap<>();
+    try (Repository repo = repoManager.openRepository(project)) {
+      try (RevWalk walk = new RevWalk(repo);
+          SubmoduleWalk sw = new SubmoduleWalk(repo)) {
+        RevCommit commit =
+            walk.parseCommit(repo.resolve(event.getNewObjectId()));
+        sw.setTree(commit.getTree());
+        sw.setRootTree(commit.getTree());
+        while (sw.next()) {
+          submodules.put(
+              normalizePath(event.getProjectName(), sw.getModulesUrl(), false),
+              sw.getObjectId().name());
+        }
+      } catch (ConfigInvalidException e) {
+        log.warn("Invalid .gitmodules configuration while parsing "
+            + event.getProjectName());
+      }
+    }
+    return submodules;
+  }
+
+  private void updateProjects(String project, String branch,
+      Map<String, String> projects) {
+    String canonicalProject = getCanonicalProject(project);
+    List<Usage> uses = Usage.fetchByProject(canonicalProject, branch);
+    for (Usage use : uses) {
+      if (!projects.containsKey(use.getDestination())) {
+        // No longer exists; delete.
+        use.delete();
+      } else {
+        // Update SHA1 here.
+        use.setRef(projects.get(use.getDestination()));
+        use.save();
+        projects.remove(use.getDestination());
+      }
+    }
+    // At this point, submodules only contains new elements.
+    // Create them.
+    for (String key : projects.keySet()) {
+      Usage use = new Usage(canonicalProject, branch, key, projects.get(key));
+      use.save();
+    }
+  }
+
+  private String getCanonicalProject(String project) {
+    String canonicalProject =
+        String.format("https://%s/%s", serverName, project);
+    try {
+      URL url = new URL(canonicalProject);
+      canonicalProject = url.getHost() + url.getPath();
+    } catch (MalformedURLException e) {
+      log.warn("Could not parse project as URL: " + canonicalProject);
+    }
+    return canonicalProject;
+  }
+
+  private String normalizePath(String project, String destination,
+      boolean isManifest) {
+    String originalProject =
+        isManifest ? project.substring(0, project.lastIndexOf(":")) : project;
+
+    // Handle relative and absolute paths on the same server
+    if (destination.startsWith("/")) {
+      if (serverName != null) {
+        destination = serverName + destination;
+      } else {
+        log.warn("Could not parse absolute path; canonicalWebUrl not set");
+      }
+    } else if (destination.startsWith(".")) {
+      if (serverName != null) {
+        Path path = Paths.get(String.format("/%s/%s", project, destination));
+        destination = serverName + path.normalize().toString();
+      } else {
+        log.warn("Could not parse relative path; canonicalWebUrl not set");
+      }
+    } else if (!destination.matches("^[^:]+://.*")) {
+      if (serverName != null) {
+        destination = serverName + "/" + originalProject + "/" + destination;
+      } else {
+        log.warn("Could not parse relative path; canonicalWebURl not set");
+      }
+    }
+
+    try {
+      // Replace the protocol with a known scheme, to avoid angering URL
+      destination = destination.replaceFirst("^[^:]+://", "");
+      URL url = new URL("https://" + destination);
+      destination = url.getHost() + url.getPath();
+    } catch (MalformedURLException e) {
+      log.warn("Could not parse destination as URL: " + destination);
+    }
+    return destination;
+  }
+}