Abstract away update method to support more tools

Abstract away update method inside an interface so that other tools
similar to repo can be supported.

Change-Id: I4964e0f9f89b4a02ce1cd7bb2cdcffb6cf24f585
diff --git a/src/main/java/com/googlesource/gerrit/plugins/supermanifest/ConfigEntry.java b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/ConfigEntry.java
new file mode 100644
index 0000000..ddb2a4f
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/ConfigEntry.java
@@ -0,0 +1,194 @@
+// Copyright (C) 2017 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.supermanifest;
+
+import static com.google.gerrit.reviewdb.client.RefNames.REFS_HEADS;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.reviewdb.client.Project;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Objects;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Repository;
+
+enum ToolType {
+  Repo
+}
+
+class ConfigEntry {
+  public static final String SECTION_NAME = "superproject";
+
+  Project.NameKey srcRepoKey;
+  String srcRef;
+  URI baseUri;
+  ToolType toolType;
+  String xmlPath;
+  Project.NameKey destRepoKey;
+  boolean recordSubmoduleLabels;
+
+  // destBranch can be "*" in which case srcRef is ignored.
+  String destBranch;
+
+  ConfigEntry(Config cfg, String name) throws ConfigInvalidException {
+    String[] parts = name.split(":");
+    if (parts.length != 2) {
+      throw new ConfigInvalidException(
+          String.format("pluginName '%s' must have form REPO:BRANCH", name));
+    }
+
+    String destRepo = parts[0];
+    String destRef = parts[1];
+
+    if (!destRef.startsWith(REFS_HEADS)) {
+      throw new ConfigInvalidException(
+          String.format("invalid destination '%s'. Must specify refs/heads/", destRef));
+    }
+
+    if (destRef.contains("*") && !destRef.equals(REFS_HEADS + "*")) {
+      throw new ConfigInvalidException(
+          String.format("invalid destination '%s'. Use just '*' for all branches.", destRef));
+    }
+
+    String srcRepo = cfg.getString(SECTION_NAME, name, "srcRepo");
+    if (srcRepo == null) {
+      throw new ConfigInvalidException(String.format("entry %s did not specify srcRepo", name));
+    }
+
+    // TODO(hanwen): sanity check repo names.
+    srcRepoKey = new Project.NameKey(srcRepo);
+
+    String toolType = Strings.nullToEmpty(cfg.getString(SECTION_NAME, name, "toolType"));
+
+    switch (toolType) {
+      case "":
+      case "repo":
+        this.toolType = ToolType.Repo;
+        break;
+      default:
+        throw new ConfigInvalidException(
+            String.format("entry %s has invalid toolType: %s", name, toolType));
+    }
+
+    if (destRef.equals(REFS_HEADS + "*")) {
+      srcRef = "";
+    } else {
+      if (!Repository.isValidRefName(destRef)) {
+        throw new ConfigInvalidException(String.format("destination branch '%s' invalid", destRef));
+      }
+
+      srcRef = cfg.getString(SECTION_NAME, name, "srcRef");
+      if (!Repository.isValidRefName(srcRef)) {
+        throw new ConfigInvalidException(String.format("source ref '%s' invalid", srcRef));
+      }
+
+      if (srcRef == null) {
+        throw new ConfigInvalidException(String.format("entry %s did not specify srcRef", name));
+      }
+    }
+
+    xmlPath = cfg.getString(SECTION_NAME, name, "srcPath");
+    if (xmlPath == null) {
+      throw new ConfigInvalidException(String.format("entry %s did not specify srcPath", name));
+    }
+
+    destRepoKey = new Project.NameKey(destRepo);
+
+    // The external format is chosen so we can support copying over tags as well.
+    destBranch = destRef.substring(REFS_HEADS.length());
+
+    recordSubmoduleLabels = cfg.getBoolean(SECTION_NAME, name, "recordSubmoduleLabels", false);
+
+    try {
+      // http://foo/platform/manifest => http://foo/platform/
+      baseUri = new URI(srcRepoKey.toString()).resolve("");
+    } catch (URISyntaxException exception) {
+      throw new ConfigInvalidException("could not build src URL", exception);
+    }
+  }
+
+  public String src() {
+    String src = srcRef;
+    if (destBranch.equals("*")) {
+      src = "*";
+    }
+    return srcRepoKey + ":" + src + ":" + xmlPath;
+  }
+
+  public String dest() {
+    return destRepoKey + ":" + destBranch;
+  }
+
+  @Override
+  public String toString() {
+    return String.format("%s => %s", src(), dest());
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+
+    ConfigEntry that = (ConfigEntry) o;
+    if (!destRepoKey.equals(that.destRepoKey)) return false;
+    return destBranch.equals(that.destBranch);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(destRepoKey, destBranch);
+  }
+
+  /** @return the srcRepoKey */
+  public Project.NameKey getSrcRepoKey() {
+    return srcRepoKey;
+  }
+
+  /** @return the srcRef */
+  public String getSrcRef() {
+    return srcRef;
+  }
+
+  /** @return the baseUri */
+  public URI getBaseUri() {
+    return baseUri;
+  }
+
+  /** @return the toolType */
+  public ToolType getToolType() {
+    return toolType;
+  }
+
+  /** @return the xmlPath */
+  public String getXmlPath() {
+    return xmlPath;
+  }
+
+  /** @return the destRepoKey */
+  public Project.NameKey getDestRepoKey() {
+    return destRepoKey;
+  }
+
+  /** @return the recordSubmoduleLabels */
+  public boolean isRecordSubmoduleLabels() {
+    return recordSubmoduleLabels;
+  }
+
+  /** @return the destBranch */
+  public String getDestBranch() {
+    return destBranch;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/supermanifest/RepoUpdater.java b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/RepoUpdater.java
new file mode 100644
index 0000000..c385eee
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/RepoUpdater.java
@@ -0,0 +1,83 @@
+// Copyright (C) 2017 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.supermanifest;
+
+import static com.google.gerrit.reviewdb.client.RefNames.REFS_HEADS;
+
+import com.googlesource.gerrit.plugins.supermanifest.SuperManifestRefUpdatedListener.GerritRemoteReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import org.eclipse.jgit.gitrepo.ManifestParser;
+import org.eclipse.jgit.gitrepo.RepoCommand;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+
+class RepoUpdater implements SubModuleUpdater {
+  PersonIdent serverIdent;
+
+  public RepoUpdater(PersonIdent serverIdent, URI canonicalWebUrl) {
+    this.serverIdent = serverIdent;
+  }
+
+  @Override
+  public void update(GerritRemoteReader reader, ConfigEntry c, String srcRef) throws Exception {
+    Repository destRepo = reader.openRepository(c.getDestRepoKey().toString());
+    Repository srcRepo = reader.openRepository(c.getSrcRepoKey().toString());
+
+    RepoCommand cmd = new RepoCommand(destRepo);
+
+    if (c.getDestBranch().equals("*")) {
+      cmd.setTargetBranch(srcRef.substring(REFS_HEADS.length()));
+    } else {
+      cmd.setTargetBranch(c.getDestBranch());
+    }
+
+    InputStream manifestStream =
+        new ByteArrayInputStream(Utils.readBlob(srcRepo, srcRef + ":" + c.getXmlPath()));
+
+    cmd.setAuthor(serverIdent)
+        .setRecordRemoteBranch(true)
+        .setRecordSubmoduleLabels(c.isRecordSubmoduleLabels())
+        .setInputStream(manifestStream)
+        .setRecommendShallow(true)
+        .setRemoteReader(reader)
+        .setTargetURI(c.getDestRepoKey().toString())
+        .setURI(c.getBaseUri().toString());
+
+    // Must setup a included file reader; the default is to read the file from the filesystem
+    // otherwise, which would leak data from the serving machine.
+    cmd.setIncludedFileReader(new GerritIncludeReader(srcRepo, srcRef));
+
+    cmd.call();
+  }
+
+  private static class GerritIncludeReader implements ManifestParser.IncludedFileReader {
+    private final Repository repo;
+    private final String ref;
+
+    GerritIncludeReader(Repository repo, String ref) {
+      this.repo = repo;
+      this.ref = ref;
+    }
+
+    @Override
+    public InputStream readIncludeFile(String path) throws IOException {
+      String blobRef = ref + ":" + path;
+      return new ByteArrayInputStream(Utils.readBlob(repo, blobRef));
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/supermanifest/SubModuleUpdater.java b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/SubModuleUpdater.java
new file mode 100644
index 0000000..421beae
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/SubModuleUpdater.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2017 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.supermanifest;
+
+import com.googlesource.gerrit.plugins.supermanifest.SuperManifestRefUpdatedListener.GerritRemoteReader;
+
+interface SubModuleUpdater {
+
+  /** Reads manifest and generates sub modules */
+  void update(GerritRemoteReader reader, ConfigEntry c, String srcRef) throws Exception;
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestModule.java b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestModule.java
index 066a603..fd65725 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestModule.java
@@ -22,8 +22,7 @@
 import com.google.inject.AbstractModule;
 
 public class SuperManifestModule extends AbstractModule {
-  SuperManifestModule() {
-  }
+  SuperManifestModule() {}
 
   @Override
   protected void configure() {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestRefUpdatedListener.java b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestRefUpdatedListener.java
index 4a14b8e..9f83d76 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestRefUpdatedListener.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestRefUpdatedListener.java
@@ -30,10 +30,10 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import java.io.ByteArrayInputStream;
 import java.io.Closeable;
 import java.io.IOException;
-import java.io.InputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.ArrayList;
@@ -41,23 +41,18 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.Set;
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.api.errors.InvalidRemoteException;
 import org.eclipse.jgit.api.errors.RefNotFoundException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.errors.RevisionSyntaxException;
-import org.eclipse.jgit.gitrepo.ManifestParser;
 import org.eclipse.jgit.gitrepo.RepoCommand;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -66,11 +61,10 @@
  * changes, it will trigger an update of the associated superproject.
  */
 @Singleton
-class SuperManifestRefUpdatedListener implements GitReferenceUpdatedListener, LifecycleListener {
+public class SuperManifestRefUpdatedListener
+    implements GitReferenceUpdatedListener, LifecycleListener {
   private static final Logger log = LoggerFactory.getLogger(SuperManifestRefUpdatedListener.class);
 
-  private static final String SECTION_NAME = "superproject";
-
   private final GitRepositoryManager repoManager;
   private final URI canonicalWebUrl;
   private final PluginConfigFactory cfgFactory;
@@ -119,127 +113,6 @@
     log.info(canonicalWebUrl + " : " + String.format(formatStr, args));
   }
 
-  private static byte[] readBlob(Repository repo, String idStr) throws IOException {
-    try (ObjectReader reader = repo.newObjectReader()) {
-      ObjectId id = repo.resolve(idStr);
-      if (id == null) {
-        throw new RevisionSyntaxException(
-            String.format("repo %s does not have %s", repo.toString(), idStr), idStr);
-      }
-      return reader.open(id).getCachedBytes(Integer.MAX_VALUE);
-    }
-  }
-
-  private static class ConfigEntry {
-    Project.NameKey srcRepoKey;
-    String srcRef;
-    URI baseUri;
-    String xmlPath;
-    Project.NameKey destRepoKey;
-    boolean recordSubmoduleLabels;
-
-    // destBranch can be "*" in which case srcRef is ignored.
-    String destBranch;
-
-    ConfigEntry(Config cfg, String name) throws ConfigInvalidException {
-      String[] parts = name.split(":");
-      if (parts.length != 2) {
-        throw new ConfigInvalidException(
-            String.format("pluginName '%s' must have form REPO:BRANCH", name));
-      }
-
-      String destRepo = parts[0];
-      String destRef = parts[1];
-
-      if (!destRef.startsWith(REFS_HEADS)) {
-        throw new ConfigInvalidException(
-            String.format("invalid destination '%s'. Must specify refs/heads/", destRef));
-      }
-
-      if (destRef.contains("*") && !destRef.equals(REFS_HEADS + "*")) {
-        throw new ConfigInvalidException(
-            String.format("invalid destination '%s'. Use just '*' for all branches.", destRef));
-      }
-
-      String srcRepo = cfg.getString(SECTION_NAME, name, "srcRepo");
-      if (srcRepo == null) {
-        throw new ConfigInvalidException(String.format("entry %s did not specify srcRepo", name));
-      }
-
-      // TODO(hanwen): sanity check repo names.
-      srcRepoKey = new Project.NameKey(srcRepo);
-
-      if (destRef.equals(REFS_HEADS + "*")) {
-        srcRef = "";
-      } else {
-        if (!Repository.isValidRefName(destRef)) {
-          throw new ConfigInvalidException(
-              String.format("destination branch '%s' invalid", destRef));
-        }
-
-        srcRef = cfg.getString(SECTION_NAME, name, "srcRef");
-        if (!Repository.isValidRefName(srcRef)) {
-          throw new ConfigInvalidException(String.format("source ref '%s' invalid", srcRef));
-        }
-
-        if (srcRef == null) {
-          throw new ConfigInvalidException(String.format("entry %s did not specify srcRef", name));
-        }
-      }
-
-      xmlPath = cfg.getString(SECTION_NAME, name, "srcPath");
-      if (xmlPath == null) {
-        throw new ConfigInvalidException(String.format("entry %s did not specify srcPath", name));
-      }
-
-      destRepoKey = new Project.NameKey(destRepo);
-
-      // The external format is chosen so we can support copying over tags as well.
-      destBranch = destRef.substring(REFS_HEADS.length());
-
-      recordSubmoduleLabels = cfg.getBoolean(SECTION_NAME, name, "recordSubmoduleLabels", false);
-
-      try {
-        // http://foo/platform/manifest => http://foo/platform/
-        baseUri = new URI(srcRepoKey.toString()).resolve("");
-      } catch (URISyntaxException exception) {
-        throw new ConfigInvalidException("could not build src URL", exception);
-      }
-    }
-
-    public String src() {
-      String src = srcRef;
-      if (destBranch.equals("*")) {
-        src = "*";
-      }
-      return srcRepoKey + ":" + src + ":" + xmlPath;
-    }
-
-    public String dest() {
-      return destRepoKey + ":" + destBranch;
-    }
-
-    @Override
-    public String toString() {
-      return String.format("%s => %s", src(), dest());
-    }
-
-    @Override
-    public boolean equals(Object o) {
-      if (this == o) return true;
-      if (o == null || getClass() != o.getClass()) return false;
-
-      ConfigEntry that = (ConfigEntry) o;
-      if (!destRepoKey.equals(that.destRepoKey)) return false;
-      return destBranch.equals(that.destBranch);
-    }
-
-    @Override
-    public int hashCode() {
-      return Objects.hash(destRepoKey, destBranch);
-    }
-  }
-
   /*
      [superproject "submodules:refs/heads/nyc"]
         srcRepo = platforms/manifest
@@ -260,11 +133,11 @@
     Set<String> sources = new HashSet<>();
 
     for (String sect : cfg.getSections()) {
-      if (!sect.equals(SECTION_NAME)) {
+      if (!sect.equals(ConfigEntry.SECTION_NAME)) {
         warn("%s.config: ignoring invalid section %s", name, sect);
       }
     }
-    for (String subsect : cfg.getSubsections(SECTION_NAME)) {
+    for (String subsect : cfg.getSubsections(ConfigEntry.SECTION_NAME)) {
       try {
         ConfigEntry configEntry = new ConfigEntry(cfg, subsect);
         if (destinations.contains(configEntry.srcRepoKey.get())
@@ -359,8 +232,21 @@
       }
 
       try {
-        update(c, event.getRefName());
-      } catch (IOException | GitAPIException e) {
+        SubModuleUpdater subModuleUpdater;
+        switch (c.getToolType()) {
+          case Repo:
+            subModuleUpdater = new RepoUpdater(serverIdent, canonicalWebUrl);
+            break;
+          default:
+            throw new ConfigInvalidException(
+                String.format("invalid toolType: %s", c.getToolType().name()));
+        }
+        try (GerritRemoteReader reader = new GerritRemoteReader()) {
+          subModuleUpdater.update(reader, c, event.getRefName());
+        }
+      } catch (
+          Exception
+              e) { //catch all exceptions as gerrit doesn't print stack trace for thrown Exception
         // We only want the trace up to here. We could recurse into the exception, but this at least
         // trims the very common jgit.gitrepo.RepoCommand.RemoteUnavailableException.
         StackTraceElement here = Thread.currentThread().getStackTrace()[1];
@@ -370,7 +256,10 @@
         // feedback to. We log the error, but it would be nice if we could surface these logs
         // somewhere.  Perhaps we could store these as commits in some special branch (but in
         // what repo?).
-        error("update for %s (ref %s) failed: %s", c.toString(), event.getRefName(), e);
+        StringWriter sw = new StringWriter();
+        PrintWriter pw = new PrintWriter(sw);
+        e.printStackTrace(pw);
+        error("update for %s (ref %s) failed: %s", c.toString(), event.getRefName(), sw);
       }
     }
   }
@@ -393,57 +282,8 @@
     return trimmed.toArray(new StackTraceElement[trimmed.size()]);
   }
 
-  private static class GerritIncludeReader implements ManifestParser.IncludedFileReader {
-    private final Repository repo;
-    private final String ref;
-
-    GerritIncludeReader(Repository repo, String ref) {
-      this.repo = repo;
-      this.ref = ref;
-    }
-
-    @Override
-    public InputStream readIncludeFile(String path) throws IOException {
-      String blobRef = ref + ":" + path;
-      return new ByteArrayInputStream(readBlob(repo, blobRef));
-    }
-  }
-
-  private void update(ConfigEntry c, String srcRef) throws IOException, GitAPIException {
-    try (GerritRemoteReader reader = new GerritRemoteReader()) {
-      Repository destRepo = reader.openRepository(c.destRepoKey.toString());
-      Repository srcRepo = reader.openRepository(c.srcRepoKey.toString());
-
-      RepoCommand cmd = new RepoCommand(destRepo);
-
-      if (c.destBranch.equals("*")) {
-        cmd.setTargetBranch(srcRef.substring(REFS_HEADS.length()));
-      } else {
-        cmd.setTargetBranch(c.destBranch);
-      }
-
-      InputStream manifestStream =
-          new ByteArrayInputStream(readBlob(srcRepo, srcRef + ":" + c.xmlPath));
-
-      cmd.setAuthor(serverIdent)
-          .setRecordRemoteBranch(true)
-          .setRecordSubmoduleLabels(c.recordSubmoduleLabels)
-          .setInputStream(manifestStream)
-          .setRecommendShallow(true)
-          .setRemoteReader(reader)
-          .setTargetURI(c.destRepoKey.toString())
-          .setURI(c.baseUri.toString());
-
-      // Must setup a included file reader; the default is to read the file from the filesystem
-      // otherwise, which would leak data from the serving machine.
-      cmd.setIncludedFileReader(new GerritIncludeReader(srcRepo, srcRef));
-
-      RevCommit commit = cmd.call();
-    }
-  }
-
   // GerritRemoteReader is for injecting Gerrit's Git implementation into JGit.
-  private class GerritRemoteReader implements RepoCommand.RemoteReader, Closeable {
+  class GerritRemoteReader implements RepoCommand.RemoteReader, Closeable {
     private final Map<String, Repository> repos;
 
     GerritRemoteReader() {
@@ -492,10 +332,10 @@
         throws GitAPIException, IOException {
       Repository repo;
       repo = openRepository(repoName);
-      return readBlob(repo, ref + ":" + path);
+      return Utils.readBlob(repo, ref + ":" + path);
     }
 
-    private Repository openRepository(String name) throws IOException {
+    public Repository openRepository(String name) throws IOException {
       name = urlToRepoKey(canonicalWebUrl, name);
       if (repos.containsKey(name)) {
         return repos.get(name);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/supermanifest/Utils.java b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/Utils.java
new file mode 100644
index 0000000..056a398
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/Utils.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2017 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.supermanifest;
+
+import java.io.IOException;
+import org.eclipse.jgit.errors.RevisionSyntaxException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
+
+class Utils {
+  public static byte[] readBlob(Repository repo, String idStr) throws IOException {
+    try (ObjectReader reader = repo.newObjectReader()) {
+      ObjectId id = repo.resolve(idStr);
+      if (id == null) {
+        throw new RevisionSyntaxException(
+            String.format("repo %s does not have %s", repo.toString(), idStr), idStr);
+      }
+      return reader.open(id).getCachedBytes(Integer.MAX_VALUE);
+    }
+  }
+}
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
index b013c09..9321096 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -9,12 +9,16 @@
    srcRepo = platforms/manifest
    srcRef = refs/heads/nyc
    srcPath = manifest.xml
+   toolType = repo
 ```
 
 this configures a repository called `submodules` to have a branch
 `nyc`, for which the contents corresponds to the manifest file
 `manifest.xml` on branch `refs/heads/nyc` in project `platforms/manifest`.
 
+valid value(s) for `toolType` right now is `repo`. It can be left blank to 
+default to `repo`.
+
 For the destination branch, you may also specify `*` to copy all
 branches in the manifest repository.