Add support for jiri manifest

Change-Id: I8b1a2c63e472b15253939df15617ef362919a064
diff --git a/src/main/java/com/googlesource/gerrit/plugins/supermanifest/ConfigEntry.java b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/ConfigEntry.java
index ddb2a4f..2d4e5ec 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/supermanifest/ConfigEntry.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/ConfigEntry.java
@@ -25,10 +25,6 @@
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Repository;
 
-enum ToolType {
-  Repo
-}
-
 class ConfigEntry {
   public static final String SECTION_NAME = "superproject";
 
@@ -78,6 +74,9 @@
       case "repo":
         this.toolType = ToolType.Repo;
         break;
+      case "jiri":
+        this.toolType = ToolType.Jiri;
+        break;
       default:
         throw new ConfigInvalidException(
             String.format("entry %s has invalid toolType: %s", name, toolType));
@@ -134,7 +133,7 @@
 
   @Override
   public String toString() {
-    return String.format("%s => %s", src(), dest());
+    return String.format("%s (%s) => %s", src(), toolType, dest());
   }
 
   @Override
@@ -191,4 +190,9 @@
   public String getDestBranch() {
     return destBranch;
   }
+
+  enum ToolType {
+    Repo,
+    Jiri
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/supermanifest/JiriManifest.java b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/JiriManifest.java
new file mode 100644
index 0000000..3c68ba1
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/JiriManifest.java
@@ -0,0 +1,97 @@
+// Copyright (C) 2017 Google Inc
+//
+// 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 javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+
+/** Refer https://fuchsia.googlesource.com/jiri/+/HEAD/manifest.md for manifest specification. */
+@XmlRootElement(name = "manifest")
+class JiriManifest {
+  @XmlElement public Imports imports;
+
+  @XmlElement public JiriProjects projects;
+
+  public JiriManifest() {
+    imports = new Imports();
+    projects = new JiriProjects();
+  }
+
+  @Override
+  public String toString() {
+    StringBuffer buf = new StringBuffer("\nmanifest:\n");
+    buf.append(StringUtil.addTab(imports.toString()));
+    buf.append(StringUtil.addTab(projects.toString()));
+    return buf.toString();
+  }
+
+  static class LocalImport {
+    @XmlAttribute(required = true)
+    private String file;
+
+    @Override
+    public String toString() {
+      return file;
+    }
+
+    /** @return the file */
+    public String getFile() {
+      return file;
+    }
+  }
+
+  static class Import {
+    // Don't need all fields, as we don't use it
+  }
+
+  static class Imports {
+    @XmlElement(name = "import")
+    private Import[] imports;
+
+    @XmlElement(name = "localimport")
+    private LocalImport[] localImports;
+
+    public Imports() {
+      this.imports = new Import[0];
+      this.localImports = new LocalImport[0];
+    }
+
+    @Override
+    public String toString() {
+      StringBuffer buf = new StringBuffer("");
+      if (imports.length > 0) {
+        buf.append("import: " + imports.length + "\n");
+      }
+      if (localImports.length > 0) {
+        buf.append("localImports:\n");
+        for (LocalImport l : localImports) {
+          buf.append(StringUtil.addTab(l.toString()));
+        }
+      }
+      return buf.toString();
+    }
+
+    /** @return the imports */
+    public Import[] getImports() {
+      return imports;
+    }
+
+    /** @return the localImports */
+    public LocalImport[] getLocalImports() {
+      return localImports;
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/supermanifest/JiriManifestParser.java b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/JiriManifestParser.java
new file mode 100644
index 0000000..411e5b4
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/JiriManifestParser.java
@@ -0,0 +1,83 @@
+// Copyright (C) 2017 Google Inc
+//
+// 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.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Queue;
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Unmarshaller;
+import org.eclipse.jgit.lib.Repository;
+
+class JiriManifestParser {
+  public static JiriProjects GetProjects(Repository repo, String ref, String manifest)
+      throws Exception {
+    Queue<String> q = new LinkedList<>();
+    q.add(manifest);
+    HashSet<String> processedFiles = new HashSet<>();
+    HashMap<String, JiriProjects.Project> projectMap = new HashMap<>();
+
+    while (q.size() != 0) {
+      String file = q.remove();
+      if (processedFiles.contains(file)) {
+        continue;
+      }
+      processedFiles.add(file);
+      JiriManifest m = parseManifest(repo, ref, file);
+      if (m.imports.getImports().length != 0) {
+        throw new Exception(
+            String.format("Manifest %s contains remote imports which are not supported", file));
+      }
+
+      for (JiriProjects.Project project : m.projects.getProjects()) {
+        project.fillDefault();
+        if (projectMap.containsKey(project.Key())) {
+          if (!projectMap.get(project.Key()).equals(project))
+            throw new Exception(
+                String.format(
+                    "Duplicate conflicting project %s in manifest %s\n%s\n%s",
+                    project.Key(),
+                    file,
+                    project.toString(),
+                    projectMap.get(project.Key()).toString()));
+        } else {
+          projectMap.put(project.Key(), project);
+        }
+      }
+
+      URI parentURI = new URI(file);
+      for (JiriManifest.LocalImport l : m.imports.getLocalImports()) {
+        q.add(parentURI.resolve(l.getFile()).getPath());
+      }
+    }
+    return new JiriProjects(projectMap.values().toArray(new JiriProjects.Project[0]));
+  }
+
+  private static JiriManifest parseManifest(Repository repo, String ref, String file)
+      throws JAXBException, IOException {
+    byte b[] = Utils.readBlob(repo, ref + ":" + file);
+    JAXBContext jc = JAXBContext.newInstance(JiriManifest.class);
+    Unmarshaller unmarshaller = jc.createUnmarshaller();
+    InputStream is = new ByteArrayInputStream(b);
+    JiriManifest manifest = (JiriManifest) unmarshaller.unmarshal(is);
+    return manifest;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/supermanifest/JiriProjects.java b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/JiriProjects.java
new file mode 100644
index 0000000..663a6af
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/JiriProjects.java
@@ -0,0 +1,187 @@
+// Copyright (C) 2017 Google Inc
+//
+// 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.util.Arrays;
+import java.util.Comparator;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
+
+class JiriProjects {
+  @XmlElement(name = "project")
+  private Project[] projects;
+
+  public JiriProjects() {
+    projects = new Project[0];
+  }
+
+  /** @return the projects */
+  public Project[] getProjects() {
+    return projects;
+  }
+
+  public void sortByPath() {
+    Arrays.sort(projects, new SortbyPath());
+  }
+
+  public String toSubmodules() {
+    StringBuffer buf = new StringBuffer();
+    sortByPath();
+    for (Project p : projects) {
+      buf.append(p.toSubmodules());
+      buf.append("\n");
+    }
+    return buf.toString();
+  }
+
+  public JiriProjects(Project[] projects) {
+    this.projects = projects;
+  }
+
+  @Override
+  public String toString() {
+    StringBuffer buf = new StringBuffer();
+    if (projects.length > 0) {
+      buf.append("projects:\n");
+      for (Project p : projects) {
+        buf.append(StringUtil.addTab(p.toString()));
+      }
+    }
+    return buf.toString();
+  }
+
+  static class Project {
+    @XmlAttribute(required = true)
+    private String name;
+
+    @XmlAttribute(required = true)
+    private String path;
+
+    @XmlAttribute(required = true)
+    private String remote;
+
+    @XmlAttribute private String remotebranch;
+
+    @XmlAttribute private String revision;
+
+    @XmlAttribute private int historydepth;
+
+    /** @return the name */
+    public String getName() {
+      return name;
+    }
+
+    /** @return the path */
+    public String getPath() {
+      return path;
+    }
+
+    /** @return the remote */
+    public String getRemote() {
+      return remote;
+    }
+
+    /** @return the historydepth */
+    public int getHistorydepth() {
+      return historydepth;
+    }
+
+    public String getRef() {
+      if (!revision.isEmpty()) {
+        return revision;
+      }
+      return remotebranch;
+    }
+
+    public Project() {
+      name = "";
+      path = "";
+      remote = "";
+      remotebranch = "";
+      revision = "";
+      historydepth = 0;
+    }
+
+    public void fillDefault() {
+      if (remotebranch.isEmpty()) {
+        remotebranch = "master";
+      }
+    }
+
+    @Override
+    public String toString() {
+      return String.format(
+          "project:\n\tname: %s\n\tpath: %s\n\tremote: %s\n\tremotebranch: %s\n\trevision: %s",
+          name, path, remote, remotebranch, revision);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      Project p = (Project) obj;
+
+      if (!name.equals(p.name)) {
+        return false;
+      }
+      if (!path.equals(p.path)) {
+        return false;
+      }
+      if (!remote.equals(p.remote)) {
+        return false;
+      }
+      if (!remotebranch.equals(p.remotebranch)) {
+        return false;
+      }
+      if (!revision.equals(p.revision)) {
+        return false;
+      }
+
+      return true;
+    }
+
+    public String toSubmodules() {
+      StringBuffer buf = new StringBuffer(String.format("[submodule \"%s\"]", name));
+      buf.append("\n\tpath = " + path);
+      buf.append("\n\turl = " + remote);
+      String branch = "";
+      if (!remotebranch.isEmpty()) {
+        branch = remotebranch;
+      }
+
+      if (!revision.isEmpty()) {
+        branch = revision;
+      }
+
+      if (!branch.isEmpty()) {
+        buf.append("\n\tbranch = " + branch);
+      }
+
+      buf.append("\n");
+      return buf.toString();
+    }
+
+    public String Key() {
+      return name + "=" + remote;
+    }
+  }
+
+  static class SortbyPath implements Comparator<Project> {
+    @Override
+    public int compare(Project a, Project b) {
+      String p1 = StringUtil.stripAndaddCharsAtEnd(a.getPath(), "/");
+      String p2 = StringUtil.stripAndaddCharsAtEnd(b.getPath(), "/");
+      return p1.compareTo(p2);
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/supermanifest/JiriUpdater.java b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/JiriUpdater.java
new file mode 100644
index 0000000..21fcbef
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/JiriUpdater.java
@@ -0,0 +1,168 @@
+package com.googlesource.gerrit.plugins.supermanifest;
+
+import static com.google.gerrit.reviewdb.client.RefNames.REFS_HEADS;
+import static com.google.gerrit.reviewdb.client.RefNames.REFS_TAGS;
+
+import com.googlesource.gerrit.plugins.supermanifest.SuperManifestRefUpdatedListener.GerritRemoteReader;
+import java.net.URI;
+import java.text.MessageFormat;
+import java.util.Map;
+import org.eclipse.jgit.api.LsRemoteCommand;
+import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
+import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.dircache.DirCache;
+import org.eclipse.jgit.dircache.DirCacheBuilder;
+import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.gitrepo.internal.RepoText;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.RefUpdate.Result;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class JiriUpdater implements SubModuleUpdater {
+  PersonIdent serverIdent;
+  URI canonicalWebUrl;
+
+  public JiriUpdater(PersonIdent serverIdent, URI canonicalWebUrl) {
+    this.serverIdent = serverIdent;
+    this.canonicalWebUrl = canonicalWebUrl;
+  }
+
+  private static final Logger log = LoggerFactory.getLogger(SuperManifestRefUpdatedListener.class);
+
+  private void warn(String formatStr, Object... args) {
+    // The docs claim that log.warn() uses format strings, but it doesn't seem to work, so we do it
+    // explicitly.
+    log.warn(canonicalWebUrl + " : " + String.format(formatStr, args));
+  }
+
+  private void updateSubmodules(
+      Repository repo, String targetRef, JiriProjects projects, GerritRemoteReader reader)
+      throws Exception {
+
+    DirCache index = DirCache.newInCore();
+    DirCacheBuilder builder = index.builder();
+    ObjectInserter inserter = repo.newObjectInserter();
+    try (RevWalk rw = new RevWalk(repo)) {
+      Config cfg = new Config();
+      projects.sortByPath();
+      String parent = null;
+      for (JiriProjects.Project proj : projects.getProjects()) {
+        String path = proj.getPath();
+        String nameUri = proj.getRemote();
+        if (parent != null) {
+          String p1 = StringUtil.stripAndaddCharsAtEnd(path, "/");
+          String p2 = StringUtil.stripAndaddCharsAtEnd(parent, "/");
+          if (p1.startsWith(p2)) {
+            warn(
+                "Skipping project %s(%s) as git doesn't support nested submodules",
+                proj.getName(), path);
+            continue;
+          }
+        }
+
+        ObjectId objectId;
+        String ref = proj.getRef();
+
+        if (ObjectId.isId(ref)) {
+          objectId = ObjectId.fromString(ref);
+        } else {
+          objectId = reader.sha1(nameUri, ref);
+          if (objectId == null) {
+            warn("failed to get ref '%s' for '%s', skipping", ref, nameUri);
+            continue;
+          }
+        }
+
+        // can be branch or tag
+        cfg.setString("submodule", path, "branch", ref);
+
+        if (proj.getHistorydepth() > 0) {
+          cfg.setBoolean("submodule", path, "shallow", true);
+          if (proj.getHistorydepth() != 1) {
+            warn(
+                "Project %s(%s) has historydepth other than 1. Submodule only support shallow of depth 1.",
+                proj.getName(), proj.getPath());
+          }
+        }
+
+        nameUri = URI.create(nameUri).toString();
+        cfg.setString("submodule", path, "path", path);
+        cfg.setString("submodule", path, "url", nameUri);
+
+        // create gitlink
+        DirCacheEntry dcEntry = new DirCacheEntry(path);
+        dcEntry.setObjectId(objectId);
+        dcEntry.setFileMode(FileMode.GITLINK);
+        builder.add(dcEntry);
+        parent = path;
+      }
+
+      String content = cfg.toText();
+
+      // create a new DirCacheEntry for .gitmodules file.
+      final DirCacheEntry dcEntry = new DirCacheEntry(Constants.DOT_GIT_MODULES);
+      ObjectId objectId =
+          inserter.insert(Constants.OBJ_BLOB, content.getBytes(Constants.CHARACTER_ENCODING));
+      dcEntry.setObjectId(objectId);
+      dcEntry.setFileMode(FileMode.REGULAR_FILE);
+      builder.add(dcEntry);
+
+      builder.finish();
+      ObjectId treeId = index.writeTree(inserter);
+
+      // Create a Commit object, populate it and write it
+      ObjectId headId = repo.resolve(targetRef + "^{commit}");
+      CommitBuilder commit = new CommitBuilder();
+      commit.setTreeId(treeId);
+      if (headId != null) commit.setParentIds(headId);
+      commit.setAuthor(serverIdent);
+      commit.setCommitter(serverIdent);
+      commit.setMessage(RepoText.get().repoCommitMessage);
+
+      ObjectId commitId = inserter.insert(commit);
+      inserter.flush();
+
+      RefUpdate ru = repo.updateRef(targetRef);
+      ru.setNewObjectId(commitId);
+      ru.setExpectedOldObjectId(headId != null ? headId : ObjectId.zeroId());
+      Result rc = ru.update(rw);
+
+      switch (rc) {
+        case NEW:
+        case FORCED:
+        case FAST_FORWARD:
+          // Successful. Do nothing.
+          break;
+        case REJECTED:
+        case LOCK_FAILURE:
+          throw new ConcurrentRefUpdateException(
+              MessageFormat.format(JGitText.get().cannotLock, targetRef), ru.getRef(), rc);
+        default:
+          throw new JGitInternalException(
+              MessageFormat.format(
+                  JGitText.get().updatingRefFailed, targetRef, commitId.name(), rc));
+      }
+    }
+  }
+
+  @Override
+  public void update(GerritRemoteReader reader, ConfigEntry c, String srcRef) throws Exception {
+    Repository srcRepo = reader.openRepository(c.getSrcRepoKey().toString());
+    Repository destRepo = reader.openRepository(c.getDestRepoKey().toString());
+    JiriProjects projects = JiriManifestParser.GetProjects(srcRepo, srcRef, c.getXmlPath());
+    String targetRef = c.getDestBranch().equals("*") ? srcRef : REFS_HEADS + c.getDestBranch();
+    updateSubmodules(destRepo, targetRef, projects, reader);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/supermanifest/StringUtil.java b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/StringUtil.java
new file mode 100644
index 0000000..278a2b1
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/StringUtil.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2017 Google Inc
+//
+// 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 org.apache.commons.lang.StringUtils;
+
+class StringUtil {
+  public static String addTab(String str) {
+    StringBuffer buf = new StringBuffer("");
+    String arr[] = str.split("\n");
+    for (String s : arr) {
+      if (!s.trim().isEmpty()) {
+        buf.append("\t" + s + " \n");
+      }
+    }
+    return buf.toString();
+  }
+
+  public static String stripAndaddCharsAtEnd(String str, String chs) {
+    StringUtils.stripEnd(str, chs);
+    return str + chs;
+  }
+}
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 9f83d76..ed153d6 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestRefUpdatedListener.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestRefUpdatedListener.java
@@ -237,6 +237,9 @@
           case Repo:
             subModuleUpdater = new RepoUpdater(serverIdent, canonicalWebUrl);
             break;
+          case Jiri:
+            subModuleUpdater = new JiriUpdater(serverIdent, canonicalWebUrl);
+            break;
           default:
             throw new ConfigInvalidException(
                 String.format("invalid toolType: %s", c.getToolType().name()));
diff --git a/src/test/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestIT.java b/src/test/java/com/googlesource/gerrit/plugins/supermanifest/JiriSuperManifestIT.java
similarity index 66%
copy from src/test/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestIT.java
copy to src/test/java/com/googlesource/gerrit/plugins/supermanifest/JiriSuperManifestIT.java
index d9dc131..8556150 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/supermanifest/JiriSuperManifestIT.java
@@ -15,7 +15,6 @@
 package com.googlesource.gerrit.plugins.supermanifest;
 
 import static com.google.common.truth.Truth.assertThat;
-import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.junit.Assert.fail;
 
 import com.google.gerrit.acceptance.GitUtil;
@@ -24,25 +23,22 @@
 import com.google.gerrit.acceptance.TestPlugin;
 import com.google.gerrit.extensions.api.projects.BranchApi;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.NameKey;
 import com.google.gerrit.reviewdb.client.RefNames;
-import java.net.URI;
 import java.util.Arrays;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
-import org.eclipse.jgit.lib.BlobBasedConfig;
-import org.eclipse.jgit.lib.Config;
 import org.junit.Test;
 
 @TestPlugin(
   name = "supermanifest",
   sysModule = "com.googlesource.gerrit.plugins.supermanifest.SuperManifestModule"
 )
-public class SuperManifestIT extends LightweightPluginDaemonTest {
-  Project.NameKey[] testRepoKeys;
+public class JiriSuperManifestIT extends LightweightPluginDaemonTest {
+  NameKey[] testRepoKeys;
 
   void setupTestRepos(String prefix) throws Exception {
-    testRepoKeys = new Project.NameKey[2];
+    testRepoKeys = new NameKey[2];
     for (int i = 0; i < 2; i++) {
       testRepoKeys[i] = createProject(prefix + i);
 
@@ -71,10 +67,10 @@
     setupTestRepos("project");
 
     // Make sure the manifest exists so the configuration loads successfully.
-    Project.NameKey manifestKey = createProject("manifest");
+    NameKey manifestKey = createProject("manifest");
     TestRepository<InMemoryRepository> manifestRepo = cloneProject(manifestKey, admin);
 
-    Project.NameKey superKey = createProject("superproject");
+    NameKey superKey = createProject("superproject");
     cloneProject(superKey, admin);
 
     pushConfig(
@@ -85,23 +81,23 @@
             + manifestKey.get()
             + "\n"
             + "  srcRef = refs/heads/srcbranch\n"
-            + "  srcPath = default.xml\n");
+            + "  srcPath = default\n"
+            + "  toolType = jiri\n");
 
-    String remoteXml = "  <remote name=\"origin\" fetch=\"" + canonicalWebUrl.get() + "\" />\n";
-    String defaultXml = "  <default remote=\"origin\" revision=\"refs/heads/master\" />\n";
     // XML change will trigger commit to superproject.
     String xml =
         "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
-            + "<manifest>\n"
-            + remoteXml
-            + defaultXml
-            + "  <project name=\""
+            + "<manifest>\n<projects>\n"
+            + "<project name=\""
+            + testRepoKeys[0].get()
+            + "\" remote=\""
+            + canonicalWebUrl.get()
             + testRepoKeys[0].get()
             + "\" path=\"project1\" />\n"
-            + "</manifest>\n";
+            + "</projects>\n</manifest>\n";
 
     pushFactory
-        .create(db, admin.getIdent(), manifestRepo, "Subject", "default.xml", xml)
+        .create(db, admin.getIdent(), manifestRepo, "Subject", "default", xml)
         .to("refs/heads/srcbranch")
         .assertOkStatus();
 
@@ -116,19 +112,23 @@
 
     xml =
         "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
-            + "<manifest>\n"
-            + remoteXml
-            + defaultXml
+            + "<manifest>\n<projects>\n"
             + "  <project name=\""
             + testRepoKeys[0].get()
+            + "\" remote=\""
+            + canonicalWebUrl.get()
+            + testRepoKeys[0].get()
             + "\" path=\"project1\" />\n"
             + "  <project name=\""
             + testRepoKeys[1].get()
+            + "\" remote=\""
+            + canonicalWebUrl.get()
+            + testRepoKeys[1].get()
             + "\" path=\"project2\" />\n"
-            + "</manifest>\n";
+            + "</projects>\n</manifest>\n";
 
     pushFactory
-        .create(db, admin.getIdent(), manifestRepo, "Subject", "default.xml", xml)
+        .create(db, admin.getIdent(), manifestRepo, "Subject", "default", xml)
         .to("refs/heads/srcbranch")
         .assertOkStatus();
 
@@ -144,21 +144,23 @@
             + manifestKey.get()
             + "\n"
             + "  srcRef = refs/heads/srcbranch\n"
-            + "  srcPath = default.xml\n");
+            + "  srcPath = default\n"
+            + "  toolType = jiri\n");
 
     // Push another XML change; this should trigger a commit using the new config.
     xml =
         "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
-            + "<manifest>\n"
-            + remoteXml
-            + defaultXml
+            + "<manifest>\n<projects>\n"
             + "  <project name=\""
             + testRepoKeys[1].get()
+            + "\" remote=\""
+            + canonicalWebUrl.get()
+            + testRepoKeys[1].get()
             + "\" path=\"project3\" />\n"
-            + "</manifest>\n";
+            + "</projects>\n</manifest>\n";
 
     pushFactory
-        .create(db, admin.getIdent(), manifestRepo, "Subject", "default.xml", xml)
+        .create(db, admin.getIdent(), manifestRepo, "Subject", "default", xml)
         .to("refs/heads/srcbranch")
         .assertOkStatus();
 
@@ -198,10 +200,10 @@
     setupTestRepos("project");
 
     // Make sure the manifest exists so the configuration loads successfully.
-    Project.NameKey manifestKey = createProject("manifest");
+    NameKey manifestKey = createProject("manifest");
     TestRepository<InMemoryRepository> manifestRepo = cloneProject(manifestKey, admin);
 
-    Project.NameKey superKey = createProject("superproject");
+    NameKey superKey = createProject("superproject");
     cloneProject(superKey, admin);
 
     pushConfig(
@@ -212,39 +214,39 @@
             + manifestKey.get()
             + "\n"
             + "  srcRef = blablabla\n"
-            + "  srcPath = default.xml\n");
-
-    String remoteXml = "  <remote name=\"origin\" fetch=\"" + canonicalWebUrl.get() + "\" />\n";
-    String originXml = "  <default remote=\"origin\" revision=\"refs/heads/master\" />\n";
+            + "  srcPath = default\n"
+            + "  toolType = jiri\n");
 
     // XML change will trigger commit to superproject.
     String xml =
         "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
-            + "<manifest>\n"
-            + remoteXml
-            + originXml
+            + "<manifest>\n<projects>\n"
             + "  <project name=\""
             + testRepoKeys[0].get()
+            + "\" remote=\""
+            + canonicalWebUrl.get()
+            + testRepoKeys[0].get()
             + "\" path=\"project1\" />\n"
-            + "</manifest>\n";
+            + "</projects>\n</manifest>\n";
 
     pushFactory
-        .create(db, admin.getIdent(), manifestRepo, "Subject", "default.xml", xml)
+        .create(db, admin.getIdent(), manifestRepo, "Subject", "default", xml)
         .to("refs/heads/src1")
         .assertOkStatus();
 
     xml =
         "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
-            + "<manifest>\n"
-            + remoteXml
-            + originXml
+            + "<manifest>\n<projects>\n"
             + "  <project name=\""
             + testRepoKeys[1].get()
+            + "\" remote=\""
+            + canonicalWebUrl.get()
+            + testRepoKeys[1].get()
             + "\" path=\"project2\" />\n"
-            + "</manifest>\n";
+            + "</projects>\n</manifest>\n";
 
     pushFactory
-        .create(db, admin.getIdent(), manifestRepo, "Subject", "default.xml", xml)
+        .create(db, admin.getIdent(), manifestRepo, "Subject", "default", xml)
         .to("refs/heads/src2")
         .assertOkStatus();
 
@@ -271,37 +273,36 @@
   public void manifestIncludesOtherManifest() throws Exception {
     setupTestRepos("project");
 
-    String remoteXml = "  <remote name=\"origin\" fetch=\"" + canonicalWebUrl.get() + "\" />\n";
-    String originXml = "  <default remote=\"origin\" revision=\"refs/heads/master\" />\n";
     String xml =
         "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
-            + "<manifest>\n"
-            + remoteXml
-            + originXml
-            + "  <project name=\""
+            + "<manifest>\n<projects>\n"
+            + "<project name=\""
+            + testRepoKeys[0].get()
+            + "\" remote=\""
+            + canonicalWebUrl.get()
             + testRepoKeys[0].get()
             + "\" path=\"project1\" />\n"
-            + "</manifest>\n";
+            + "</projects>\n</manifest>\n";
 
-    Project.NameKey manifestKey = createProject("manifest");
+    NameKey manifestKey = createProject("manifest");
     TestRepository<InMemoryRepository> manifestRepo = cloneProject(manifestKey, admin);
     pushFactory
-        .create(db, admin.getIdent(), manifestRepo, "Subject", "default.xml", xml)
+        .create(db, admin.getIdent(), manifestRepo, "Subject", "default", xml)
         .to("refs/heads/master")
         .assertOkStatus();
 
     String superXml =
         "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
-            + "<manifest>"
-            + "  <include name=\"default.xml\"/>"
-            + "</manifest>";
+            + "<manifest>\n<imports>\n"
+            + "  <localimport file=\"default\"/>"
+            + "</imports>\n</manifest>";
 
     pushFactory
-        .create(db, admin.getIdent(), manifestRepo, "Subject", "super.xml", superXml)
+        .create(db, admin.getIdent(), manifestRepo, "Subject", "super", superXml)
         .to("refs/heads/master")
         .assertOkStatus();
 
-    Project.NameKey superKey = createProject("superproject");
+    NameKey superKey = createProject("superproject");
     cloneProject(superKey, admin);
 
     pushConfig(
@@ -312,13 +313,14 @@
             + manifestKey.get()
             + "\n"
             + "  srcRef = refs/heads/master\n"
-            + "  srcPath = super.xml\n");
+            + "  srcPath = super\n"
+            + "  toolType = jiri\n");
 
     // Push a change to the source branch. We intentionally change the included XML file
     // (rather than the one mentioned in srcPath), to double check that we don't try to be too
-    // smart about eliding nops.
+    // smart about eluding nops.
     pushFactory
-        .create(db, admin.getIdent(), manifestRepo, "Subject", "default.xml", xml + " ")
+        .create(db, admin.getIdent(), manifestRepo, "Subject", "default", xml + " ")
         .to("refs/heads/master")
         .assertOkStatus();
 
@@ -327,82 +329,63 @@
   }
 
   @Test
-  public void relativeFetch() throws Exception {
-    // Test the setup that Android uses, where the "fetch" field is relative to the location of the
-    // manifest repo.
-    setupTestRepos("platform/project");
+  public void remoteImportFails() throws Exception {
+    setupTestRepos("project");
 
-    // The test framework adds more cruft to the prefix.
-    String realPrefix = testRepoKeys[0].get().split("/")[0];
+    String xml =
+        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+            + "<manifest>\n<projects>\n"
+            + "<project name=\""
+            + testRepoKeys[0].get()
+            + "\" remote=\""
+            + canonicalWebUrl.get()
+            + testRepoKeys[0].get()
+            + "\" path=\"project1\" />\n"
+            + "</projects>\n</manifest>\n";
 
-    Project.NameKey manifestKey = createProject(realPrefix + "/manifest");
-    TestRepository<InMemoryRepository> manifestRepo = cloneProject(manifestKey, admin);
+    NameKey manifestKey = createProject("manifest");
+    NameKey superKey = createProject("superproject");
 
-    Project.NameKey superKey = createProject("superproject");
-
-    TestRepository<InMemoryRepository> superRepo = cloneProject(superKey, admin);
+    cloneProject(superKey, admin);
 
     pushConfig(
         "[superproject \""
             + superKey.get()
-            + ":refs/heads/destbranch\"]\n"
+            + ":refs/heads/master\"]\n"
             + "  srcRepo = "
             + manifestKey.get()
             + "\n"
-            + "  srcRef = refs/heads/srcbranch\n"
-            + "  srcPath = default.xml\n");
-
-    String url = canonicalWebUrl.get();
-    String remoteXml = "  <remote name=\"origin\" fetch=\"..\" review=\"" + url + "\" />\n";
-    String defaultXml = "  <default remote=\"origin\" revision=\"refs/heads/master\" />\n";
-
-    String xml =
-        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
-            + "<manifest>\n"
-            + remoteXml
-            + defaultXml
-            + "  <project name=\""
-            + testRepoKeys[0].get()
-            + "\" path=\"path1\" />\n"
-            + "</manifest>\n";
+            + "  srcRef = refs/heads/master\n"
+            + "  srcPath = super\n"
+            + "  toolType = jiri\n");
+    TestRepository<InMemoryRepository> manifestRepo = cloneProject(manifestKey, admin);
     pushFactory
-        .create(db, admin.getIdent(), manifestRepo, "Subject", "default.xml", xml)
-        .to("refs/heads/srcbranch")
+        .create(db, admin.getIdent(), manifestRepo, "Subject", "default", xml)
+        .to("refs/heads/master")
         .assertOkStatus();
 
-    BranchApi branch = gApi.projects().name(superKey.get()).branch("refs/heads/destbranch");
-    assertThat(branch.file("path1").getContentType()).isEqualTo("x-git/gitlink; charset=UTF-8");
+    String superXml =
+        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+            + "<manifest>\n<imports>\n"
+            + "<import manifest=\""
+            + manifestKey.get()
+            + "\" name=\"default\" remote=\""
+            + canonicalWebUrl.get()
+            + manifestKey.get()
+            + "\"/>"
+            + "</imports>\n</manifest>";
 
-    Config base = new Config();
-    BlobBasedConfig cfg =
-        new BlobBasedConfig(base, branch.file(".gitmodules").asString().getBytes(UTF_8));
+    pushFactory
+        .create(db, admin.getIdent(), manifestRepo, "Subject", "super", superXml)
+        .to("refs/heads/master")
+        .assertOkStatus();
 
-    String subUrl = cfg.getString("submodule", "path1", "url");
-
-    // URL is valid.
-    URI.create(subUrl);
-
-    // The suburls must be interpreted as relative to the parent project as a directory, i.e.
-    // to go from superproject/ to platform/project0, you have to do ../platform/project0
-
-    // URL is clean.
-    assertThat(subUrl).isEqualTo("../" + realPrefix + "/project0");
+    BranchApi branch = gApi.projects().name(superKey.get()).branch("refs/heads/master");
+    try {
+      branch.file("project1");
+      fail("wanted exception");
+    } catch (ResourceNotFoundException e) {
+      // all fine.
+    }
   }
-
-  @Test
-  public void testToRepoKey() {
-    URI base = URI.create("https://gerrit-review.googlesource.com");
-    assertThat(
-            SuperManifestRefUpdatedListener.urlToRepoKey(
-                base, "https://gerrit-review.googlesource.com/repo"))
-        .isEqualTo("repo");
-    assertThat(SuperManifestRefUpdatedListener.urlToRepoKey(base, "repo")).isEqualTo("repo");
-    assertThat(
-            SuperManifestRefUpdatedListener.urlToRepoKey(
-                URI.create("https://gerrit-review.googlesource.com/"),
-                "https://gerrit-review.googlesource.com/repo"))
-        .isEqualTo("repo");
-  }
-
-  // TODO - should add tests for all the error handling in configuration parsing?
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestIT.java b/src/test/java/com/googlesource/gerrit/plugins/supermanifest/RepoSuperManifestIT.java
similarity index 99%
rename from src/test/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestIT.java
rename to src/test/java/com/googlesource/gerrit/plugins/supermanifest/RepoSuperManifestIT.java
index d9dc131..6775998 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/supermanifest/RepoSuperManifestIT.java
@@ -38,7 +38,7 @@
   name = "supermanifest",
   sysModule = "com.googlesource.gerrit.plugins.supermanifest.SuperManifestModule"
 )
-public class SuperManifestIT extends LightweightPluginDaemonTest {
+public class RepoSuperManifestIT extends LightweightPluginDaemonTest {
   Project.NameKey[] testRepoKeys;
 
   void setupTestRepos(String prefix) throws Exception {