make repo urls relative for jiri supermanifest

Change-Id: I03f2cd61a34a6f9ac518fc2ac095c59aa0e79f21
diff --git a/src/main/java/com/googlesource/gerrit/plugins/supermanifest/JiriUpdater.java b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/JiriUpdater.java
index 3e92349..7d8426e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/supermanifest/JiriUpdater.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/JiriUpdater.java
@@ -6,6 +6,7 @@
 import java.io.IOException;
 import java.net.URI;
 import java.text.MessageFormat;
+import java.util.StringJoiner;
 import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.api.errors.JGitInternalException;
@@ -13,6 +14,7 @@
 import org.eclipse.jgit.dircache.DirCacheBuilder;
 import org.eclipse.jgit.dircache.DirCacheEntry;
 import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.gitrepo.internal.RepoText;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.CommitBuilder;
@@ -47,7 +49,11 @@
   }
 
   private void updateSubmodules(
-      Repository repo, String targetRef, JiriProjects projects, GerritRemoteReader reader)
+      Repository repo,
+      String targetRef,
+      URI targetURI,
+      JiriProjects projects,
+      GerritRemoteReader reader)
       throws IOException, GitAPIException {
     DirCache index = DirCache.newInCore();
     DirCacheBuilder builder = index.builder();
@@ -95,9 +101,20 @@
           }
         }
 
-        nameUri = URI.create(nameUri).toString();
+        URI submodUrl = URI.create(nameUri);
+
+        //check if repo exists locally then relativize its URL
+        try {
+          String repoName = submodUrl.getPath();
+          while (repoName.startsWith("/")) {
+            repoName = repoName.substring(1);
+          }
+          reader.openRepository(repoName);
+          submodUrl = relativize(targetURI, URI.create(repoName));
+        } catch (RepositoryNotFoundException e) {
+        }
         cfg.setString("submodule", path, "path", path);
-        cfg.setString("submodule", path, "url", nameUri);
+        cfg.setString("submodule", path, "url", submodUrl.toString());
 
         // create gitlink
         DirCacheEntry dcEntry = new DirCacheEntry(path);
@@ -155,6 +172,73 @@
     }
   }
 
+  private static final String SLASH = "/";
+  /*
+   * Copied from https://github.com/eclipse/jgit/blob/e9fb111182b55cc82c530d82f13176c7a85cd958/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java#L729
+   */
+  static URI relativize(URI current, URI target) {
+    // We only handle bare paths for now.
+    if (!target.toString().equals(target.getPath())) {
+      return target;
+    }
+    if (!current.toString().equals(current.getPath())) {
+      return target;
+    }
+
+    String cur = current.normalize().getPath();
+    String dest = target.normalize().getPath();
+
+    if (cur.startsWith(SLASH) != dest.startsWith(SLASH)) {
+      return target;
+    }
+
+    while (cur.startsWith(SLASH)) {
+      cur = cur.substring(1);
+    }
+    while (dest.startsWith(SLASH)) {
+      dest = dest.substring(1);
+    }
+
+    if (cur.indexOf('/') == -1 || dest.indexOf('/') == -1) {
+      // Avoid having to special-casing in the next two ifs.
+      String prefix = "prefix/";
+      cur = prefix + cur;
+      dest = prefix + dest;
+    }
+
+    if (!cur.endsWith(SLASH)) {
+      // The current file doesn't matter.
+      int lastSlash = cur.lastIndexOf('/');
+      cur = cur.substring(0, lastSlash);
+    }
+    String destFile = "";
+    if (!dest.endsWith(SLASH)) {
+      // We always have to provide the destination file.
+      int lastSlash = dest.lastIndexOf('/');
+      destFile = dest.substring(lastSlash + 1, dest.length());
+      dest = dest.substring(0, dest.lastIndexOf('/'));
+    }
+
+    String[] cs = cur.split(SLASH);
+    String[] ds = dest.split(SLASH);
+
+    int common = 0;
+    while (common < cs.length && common < ds.length && cs[common].equals(ds[common])) {
+      common++;
+    }
+
+    StringJoiner j = new StringJoiner(SLASH);
+    for (int i = common; i < cs.length; i++) {
+      j.add("..");
+    }
+    for (int i = common; i < ds.length; i++) {
+      j.add(ds[i]);
+    }
+
+    j.add(destFile);
+    return URI.create(j.toString());
+  }
+
   @Override
   public void update(GerritRemoteReader reader, ConfigEntry c, String srcRef)
       throws IOException, GitAPIException, ConfigInvalidException {
@@ -162,6 +246,7 @@
     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);
+    updateSubmodules(
+        destRepo, targetRef, URI.create(c.getDestRepoKey().toString() + "/"), projects, reader);
   }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/supermanifest/JiriSuperManifestIT.java b/src/test/java/com/googlesource/gerrit/plugins/supermanifest/JiriSuperManifestIT.java
index 8556150..18d9d6d 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/supermanifest/JiriSuperManifestIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/supermanifest/JiriSuperManifestIT.java
@@ -15,6 +15,7 @@
 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,10 +25,14 @@
 import com.google.gerrit.extensions.api.projects.BranchApi;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.reviewdb.client.Project.NameKey;
+import com.google.gerrit.reviewdb.client.Project;
 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(
@@ -270,6 +275,79 @@
   }
 
   @Test
+  public void relativeFetch() throws Exception {
+    // Test that first party gerrit repos are represented by relative URLs in supermanifest and
+    // external repos by their absolute URLs.
+    setupTestRepos("platform/project");
+
+    String realPrefix = testRepoKeys[0].get().split("/")[0];
+
+    Project.NameKey manifestKey = createProject(realPrefix + "/manifest");
+    TestRepository<InMemoryRepository> manifestRepo = cloneProject(manifestKey, admin);
+
+    Project.NameKey superKey = createProject("superproject");
+    pushConfig(
+        "[superproject \""
+            + superKey.get()
+            + ":refs/heads/destbranch\"]\n"
+            + "  srcRepo = "
+            + manifestKey.get()
+            + "\n"
+            + "  srcRef = refs/heads/srcbranch\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<projects>\n"
+            + "<project name=\""
+            + testRepoKeys[0].get()
+            + "\" remote=\""
+            + canonicalWebUrl.get()
+            + testRepoKeys[0].get()
+            + "\" path=\"project1\" />\n"
+            + "<project name=\"external\""
+            + " remote=\"https://external/repo\""
+            + " revision=\"c438d02cdf08a08fe29550cb11cb6ae8190919f1\""
+            + " path=\"project2\" />\n"
+            + "</projects>\n</manifest>\n";
+
+    pushFactory
+        .create(db, admin.getIdent(), manifestRepo, "Subject", "default", xml)
+        .to("refs/heads/srcbranch")
+        .assertOkStatus();
+
+    BranchApi branch = gApi.projects().name(superKey.get()).branch("refs/heads/destbranch");
+    assertThat(branch.file("project1").getContentType()).isEqualTo("x-git/gitlink; charset=UTF-8");
+    assertThat(branch.file("project2").getContentType()).isEqualTo("x-git/gitlink; charset=UTF-8");
+
+    Config base = new Config();
+    BlobBasedConfig cfg =
+        new BlobBasedConfig(base, branch.file(".gitmodules").asString().getBytes(UTF_8));
+
+    String subUrl = cfg.getString("submodule", "project1", "url");
+
+    // URL is valid.
+    URI.create(subUrl);
+
+    // The suburl 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");
+
+    subUrl = cfg.getString("submodule", "project2", "url");
+
+    // URL is valid.
+    URI.create(subUrl);
+
+    // The suburl must be absolute as this is external repo
+
+    assertThat(subUrl).isEqualTo("https://external/repo");
+  }
+
+  @Test
   public void manifestIncludesOtherManifest() throws Exception {
     setupTestRepos("project");
 
@@ -388,4 +466,31 @@
       // all fine.
     }
   }
+
+  /*
+   * Copied test from https://github.com/eclipse/jgit/blob/e9fb111182b55cc82c530d82f13176c7a85cd958/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java#L1105
+   */
+  void testRelative(String a, String b, String want) {
+    String got = JiriUpdater.relativize(URI.create(a), URI.create(b)).toString();
+
+    if (!got.equals(want)) {
+      fail(String.format("relative('%s', '%s') = '%s', want '%s'", a, b, got, want));
+    }
+  }
+
+  @Test
+  public void relative() {
+    testRelative("a/b/", "a/", "../");
+    // Normalization:
+    testRelative("a/p/..//b/", "a/", "../");
+    testRelative("a/b", "a/", "");
+    testRelative("a/", "a/b/", "b/");
+    testRelative("a/", "a/b", "b");
+    testRelative("/a/b/c", "/b/c", "../../b/c");
+    testRelative("/abc", "bcd", "bcd");
+    testRelative("abc", "def", "def");
+    testRelative("abc", "/bcd", "/bcd");
+    testRelative("http://a", "a/b", "a/b");
+    testRelative("http://base.com/a/", "http://child.com/a/b", "http://child.com/a/b");
+  }
 }