diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/RepoTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/RepoTest.java
new file mode 100644
index 000000000..bfff14d9d
--- /dev/null
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/RepoTest.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2014 Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.pgm;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.junit.JGitTestUtil;
+import org.eclipse.jgit.lib.CLIRepositoryTestCase;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Before;
+import org.junit.Test;
+
+public class RepoTest extends CLIRepositoryTestCase {
+ private Repository defaultDb;
+ private Repository notDefaultDb;
+ private Repository groupADb;
+ private Repository groupBDb;
+
+ private String rootUri;
+ private String defaultUri;
+ private String notDefaultUri;
+ private String groupAUri;
+ private String groupBUri;
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+
+ defaultDb = createWorkRepository();
+ Git git = new Git(defaultDb);
+ JGitTestUtil.writeTrashFile(defaultDb, "hello.txt", "world");
+ git.add().addFilepattern("hello.txt").call();
+ git.commit().setMessage("Initial commit").call();
+
+ notDefaultDb = createWorkRepository();
+ git = new Git(notDefaultDb);
+ JGitTestUtil.writeTrashFile(notDefaultDb, "world.txt", "hello");
+ git.add().addFilepattern("world.txt").call();
+ git.commit().setMessage("Initial commit").call();
+
+ groupADb = createWorkRepository();
+ git = new Git(groupADb);
+ JGitTestUtil.writeTrashFile(groupADb, "a.txt", "world");
+ git.add().addFilepattern("a.txt").call();
+ git.commit().setMessage("Initial commit").call();
+
+ groupBDb = createWorkRepository();
+ git = new Git(groupBDb);
+ JGitTestUtil.writeTrashFile(groupBDb, "b.txt", "world");
+ git.add().addFilepattern("b.txt").call();
+ git.commit().setMessage("Initial commit").call();
+
+ resolveRelativeUris();
+ }
+
+ @Test
+ public void testAddRepoManifest() throws Exception {
+ StringBuilder xmlContent = new StringBuilder();
+ xmlContent.append("\n")
+ .append("")
+ .append("")
+ .append("")
+ .append("")
+ .append("")
+ .append("")
+ .append("")
+ .append("");
+ writeTrashFile("manifest.xml", xmlContent.toString());
+ StringBuilder cmd = new StringBuilder("git repo --base-uri=\"")
+ .append(rootUri)
+ .append("\" --groups=\"all,-a\" \"")
+ .append(db.getWorkTree().getAbsolutePath())
+ .append("/manifest.xml\"");
+ execute(cmd.toString());
+
+ File file = new File(db.getWorkTree(), "foo/hello.txt");
+ assertFalse("\"all,-a\" doesn't have foo", file.exists());
+ file = new File(db.getWorkTree(), "bar/world.txt");
+ assertTrue("\"all,-a\" has bar", file.exists());
+ file = new File(db.getWorkTree(), "a/a.txt");
+ assertFalse("\"all,-a\" doesn't have a", file.exists());
+ file = new File(db.getWorkTree(), "b/b.txt");
+ assertTrue("\"all,-a\" has have b", file.exists());
+ }
+
+ private void resolveRelativeUris() {
+ // Find the longest common prefix ends with "/" as rootUri.
+ defaultUri = defaultDb.getDirectory().toURI().toString();
+ notDefaultUri = notDefaultDb.getDirectory().toURI().toString();
+ groupAUri = groupADb.getDirectory().toURI().toString();
+ groupBUri = groupBDb.getDirectory().toURI().toString();
+ int start = 0;
+ while (start <= defaultUri.length()) {
+ int newStart = defaultUri.indexOf('/', start + 1);
+ String prefix = defaultUri.substring(0, newStart);
+ if (!notDefaultUri.startsWith(prefix) ||
+ !groupAUri.startsWith(prefix) ||
+ !groupBUri.startsWith(prefix)) {
+ start++;
+ rootUri = defaultUri.substring(0, start);
+ defaultUri = defaultUri.substring(start);
+ notDefaultUri = notDefaultUri.substring(start);
+ groupAUri = groupAUri.substring(start);
+ groupBUri = groupBUri.substring(start);
+ return;
+ }
+ start = newStart;
+ }
+ }
+}
diff --git a/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF b/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF
index 2b1d34cbd..b052e0467 100644
--- a/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF
@@ -17,6 +17,7 @@ Import-Package: org.apache.commons.compress.archivers;version="[1.3,2.0)",
org.eclipse.jgit.diff;version="[3.4.0,3.5.0)",
org.eclipse.jgit.dircache;version="[3.4.0,3.5.0)",
org.eclipse.jgit.errors;version="[3.4.0,3.5.0)",
+ org.eclipse.jgit.gitrepo;version="[3.4.0,3.5.0)",
org.eclipse.jgit.internal.storage.file;version="[3.4.0,3.5.0)",
org.eclipse.jgit.internal.storage.pack;version="[3.4.0,3.5.0)",
org.eclipse.jgit.lib;version="[3.4.0,3.5.0)",
diff --git a/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin b/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin
index 2ca6009cd..e1b05491b 100644
--- a/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin
+++ b/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin
@@ -24,6 +24,7 @@ org.eclipse.jgit.pgm.MergeBase
org.eclipse.jgit.pgm.Push
org.eclipse.jgit.pgm.ReceivePack
org.eclipse.jgit.pgm.Reflog
+org.eclipse.jgit.pgm.Repo
org.eclipse.jgit.pgm.Reset
org.eclipse.jgit.pgm.RevList
org.eclipse.jgit.pgm.RevParse
diff --git a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
index 372f1c1bf..7d5b87fdd 100644
--- a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
+++ b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
@@ -231,6 +231,7 @@ usage_archive=zip up files from the named tree
usage_archiveFormat=archive format. Currently supported formats: 'tar', 'zip', 'tgz', 'tbz2', 'txz'
usage_archiveOutput=output file to write the archive to
usage_archivePrefix=string to prepend to each pathname in the archive
+usage_baseUri=the base URI of the repo manifest file. e.g. https://android.googlesource.com/platform/
usage_blameLongRevision=show long revision
usage_blameRange=annotate only the given range
usage_blameRawTimestamp=show raw timestamp
@@ -277,6 +278,7 @@ usage_forceCheckout=when switching branches, proceed even if the index or the wo
usage_forceCreateBranchEvenExists=force create branch even exists
usage_forceReplacingAnExistingTag=force replacing an existing tag
usage_getAndSetOptions=Get and set repository or global options
+usage_groups=Restrict manifest projects to ones with specified group(s), use "-" for excluding [default|all|G1,G2,G3|G4,-G5,-G6]
usage_hostnameOrIpToListenOn=hostname (or ip) to listen on
usage_indexFileFormatToCreate=index file format to create
usage_ignoreWhitespace=ignore all whitespace
@@ -300,7 +302,9 @@ usage_noRenames=disable rename detection
usage_noShowStandardNotes=Disable showing notes from the standard /refs/notes/commits branch
usage_onlyMatchAgainstAlreadyTrackedFiles=Only match against already tracked files in the index rather than the working tree
usage_outputFile=Output file
+usage_parseRepoManifest=Parse a repo manifest file and add submodules
usage_path=path
+usage_pathToXml=path to the repo manifest XML file
usage_performFsckStyleChecksOnReceive=perform fsck style checks on receive
usage_portNumberToListenOn=port number to listen on
usage_printOnlyBranchesThatContainTheCommit=print only branches that contain the commit
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Repo.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Repo.java
new file mode 100644
index 000000000..9b191e679
--- /dev/null
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Repo.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2014, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.pgm;
+
+import org.eclipse.jgit.gitrepo.RepoCommand;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+
+@Command(common = true, usage = "usage_parseRepoManifest")
+class Repo extends TextBuiltin {
+
+ @Option(name = "--base-uri", aliases = { "-u" }, usage = "usage_baseUri")
+ private String uri;
+
+ @Option(name = "--groups", aliases = { "-g" }, usage = "usage_groups")
+ private String groups = "default"; //$NON-NLS-1$
+
+ @Argument(required = true, usage = "usage_pathToXml")
+ private String path;
+
+ @Override
+ protected void run() throws Exception {
+ new RepoCommand(db)
+ .setURI(uri)
+ .setPath(path)
+ .setGroups(groups)
+ .call();
+ }
+}
diff --git a/org.eclipse.jgit.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.test/META-INF/MANIFEST.MF
index 01e3e7d52..cda3fd181 100644
--- a/org.eclipse.jgit.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.test/META-INF/MANIFEST.MF
@@ -18,6 +18,7 @@ Import-Package: com.googlecode.javaewah;version="[0.7.9,0.8.0)",
org.eclipse.jgit.errors;version="[3.4.0,3.5.0)",
org.eclipse.jgit.events;version="[3.4.0,3.5.0)",
org.eclipse.jgit.fnmatch;version="[3.4.0,3.5.0)",
+ org.eclipse.jgit.gitrepo;version="[3.4.0,3.5.0)",
org.eclipse.jgit.ignore;version="[3.4.0,3.5.0)",
org.eclipse.jgit.internal;version="[3.4.0,3.5.0)",
org.eclipse.jgit.internal.storage.dfs;version="[3.4.0,3.5.0)",
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java
new file mode 100644
index 000000000..27d322079
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2014, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.gitrepo;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.junit.JGitTestUtil;
+import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Test;
+
+public class RepoCommandTest extends RepositoryTestCase {
+
+ private Repository defaultDb;
+ private Repository notDefaultDb;
+ private Repository groupADb;
+ private Repository groupBDb;
+
+ private String rootUri;
+ private String defaultUri;
+ private String notDefaultUri;
+ private String groupAUri;
+ private String groupBUri;
+
+ public void setUp() throws Exception {
+ super.setUp();
+
+ defaultDb = createWorkRepository();
+ Git git = new Git(defaultDb);
+ JGitTestUtil.writeTrashFile(defaultDb, "hello.txt", "world");
+ git.add().addFilepattern("hello.txt").call();
+ git.commit().setMessage("Initial commit").call();
+
+ notDefaultDb = createWorkRepository();
+ git = new Git(notDefaultDb);
+ JGitTestUtil.writeTrashFile(notDefaultDb, "world.txt", "hello");
+ git.add().addFilepattern("world.txt").call();
+ git.commit().setMessage("Initial commit").call();
+
+ groupADb = createWorkRepository();
+ git = new Git(groupADb);
+ JGitTestUtil.writeTrashFile(groupADb, "a.txt", "world");
+ git.add().addFilepattern("a.txt").call();
+ git.commit().setMessage("Initial commit").call();
+
+ groupBDb = createWorkRepository();
+ git = new Git(groupBDb);
+ JGitTestUtil.writeTrashFile(groupBDb, "b.txt", "world");
+ git.add().addFilepattern("b.txt").call();
+ git.commit().setMessage("Initial commit").call();
+
+ resolveRelativeUris();
+ }
+
+ @Test
+ public void testAddRepoManifest() throws Exception {
+ StringBuilder xmlContent = new StringBuilder();
+ xmlContent.append("\n")
+ .append("")
+ .append("")
+ .append("")
+ .append("")
+ .append("");
+ writeTrashFile("manifest.xml", xmlContent.toString());
+ RepoCommand command = new RepoCommand(db);
+ command.setPath(db.getWorkTree().getAbsolutePath() + "/manifest.xml")
+ .setURI(rootUri)
+ .call();
+ File hello = new File(db.getWorkTree(), "foo/hello.txt");
+ assertTrue("submodule was checked out", hello.exists());
+ BufferedReader reader = new BufferedReader(new FileReader(hello));
+ String content = reader.readLine();
+ reader.close();
+ assertEquals("submodule content is as expected.", "world", content);
+ }
+
+ @Test
+ public void testRepoManifestGroups() throws Exception {
+ StringBuilder xmlContent = new StringBuilder();
+ xmlContent.append("\n")
+ .append("")
+ .append("")
+ .append("")
+ .append("")
+ .append("")
+ .append("")
+ .append("")
+ .append("");
+
+ // default should have foo, a & b
+ Repository localDb = createWorkRepository();
+ JGitTestUtil.writeTrashFile(localDb, "manifest.xml", xmlContent.toString());
+ RepoCommand command = new RepoCommand(localDb);
+ command.setPath(localDb.getWorkTree().getAbsolutePath() + "/manifest.xml")
+ .setURI(rootUri)
+ .call();
+ File file = new File(localDb.getWorkTree(), "foo/hello.txt");
+ assertTrue("default has foo", file.exists());
+ file = new File(localDb.getWorkTree(), "bar/world.txt");
+ assertFalse("default doesn't have bar", file.exists());
+ file = new File(localDb.getWorkTree(), "a/a.txt");
+ assertTrue("default has a", file.exists());
+ file = new File(localDb.getWorkTree(), "b/b.txt");
+ assertTrue("default has b", file.exists());
+
+ // all,-a should have bar & b
+ localDb = createWorkRepository();
+ JGitTestUtil.writeTrashFile(localDb, "manifest.xml", xmlContent.toString());
+ command = new RepoCommand(localDb);
+ command.setPath(localDb.getWorkTree().getAbsolutePath() + "/manifest.xml")
+ .setURI(rootUri)
+ .setGroups("all,-a")
+ .call();
+ file = new File(localDb.getWorkTree(), "foo/hello.txt");
+ assertFalse("\"all,-a\" doesn't have foo", file.exists());
+ file = new File(localDb.getWorkTree(), "bar/world.txt");
+ assertTrue("\"all,-a\" has bar", file.exists());
+ file = new File(localDb.getWorkTree(), "a/a.txt");
+ assertFalse("\"all,-a\" doesn't have a", file.exists());
+ file = new File(localDb.getWorkTree(), "b/b.txt");
+ assertTrue("\"all,-a\" has have b", file.exists());
+ }
+
+ @Test
+ public void testRepoManifestCopyfile() throws Exception {
+ Repository localDb = createWorkRepository();
+ StringBuilder xmlContent = new StringBuilder();
+ xmlContent.append("\n")
+ .append("")
+ .append("")
+ .append("")
+ .append("")
+ .append("")
+ .append("")
+ .append("");
+ JGitTestUtil.writeTrashFile(localDb, "manifest.xml", xmlContent.toString());
+ RepoCommand command = new RepoCommand(localDb);
+ command.setPath(localDb.getWorkTree().getAbsolutePath() + "/manifest.xml")
+ .setURI(rootUri)
+ .call();
+ // The original file should exist
+ File hello = new File(localDb.getWorkTree(), "foo/hello.txt");
+ assertTrue("The original file exists", hello.exists());
+ BufferedReader reader = new BufferedReader(new FileReader(hello));
+ String content = reader.readLine();
+ reader.close();
+ assertEquals("The original file has expected content", "world", content);
+ // The dest file should also exist
+ hello = new File(localDb.getWorkTree(), "Hello");
+ assertTrue("The destination file exists", hello.exists());
+ reader = new BufferedReader(new FileReader(hello));
+ content = reader.readLine();
+ reader.close();
+ assertEquals("The destination file has expected content", "world", content);
+ }
+
+ private void resolveRelativeUris() {
+ // Find the longest common prefix ends with "/" as rootUri.
+ defaultUri = defaultDb.getDirectory().toURI().toString();
+ notDefaultUri = notDefaultDb.getDirectory().toURI().toString();
+ groupAUri = groupADb.getDirectory().toURI().toString();
+ groupBUri = groupBDb.getDirectory().toURI().toString();
+ int start = 0;
+ while (start <= defaultUri.length()) {
+ int newStart = defaultUri.indexOf('/', start + 1);
+ String prefix = defaultUri.substring(0, newStart);
+ if (!notDefaultUri.startsWith(prefix) ||
+ !groupAUri.startsWith(prefix) ||
+ !groupBUri.startsWith(prefix)) {
+ start++;
+ rootUri = defaultUri.substring(0, start);
+ defaultUri = defaultUri.substring(start);
+ notDefaultUri = notDefaultUri.substring(start);
+ groupAUri = groupAUri.substring(start);
+ groupBUri = groupBUri.substring(start);
+ return;
+ }
+ start = newStart;
+ }
+ }
+}
diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF
index e1b91380a..1fa0ad9cf 100644
--- a/org.eclipse.jgit/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit/META-INF/MANIFEST.MF
@@ -45,6 +45,9 @@ Export-Package: org.eclipse.jgit.api;version="3.4.0";
org.eclipse.jgit.events;version="3.4.0";
uses:="org.eclipse.jgit.lib",
org.eclipse.jgit.fnmatch;version="3.4.0",
+ org.eclipse.jgit.gitrepo;version="3.4.0";
+ uses:="org.eclipse.jgit.api,
+ org.eclipse.jgit.lib",
org.eclipse.jgit.ignore;version="3.4.0",
org.eclipse.jgit.internal;version="3.4.0";x-friends:="org.eclipse.jgit.test,org.eclipse.jgit.http.test",
org.eclipse.jgit.internal.storage.dfs;version="3.4.0";x-friends:="org.eclipse.jgit.test",
diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/gitrepo/internal/RepoText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/gitrepo/internal/RepoText.properties
new file mode 100644
index 000000000..29aa51ccd
--- /dev/null
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/gitrepo/internal/RepoText.properties
@@ -0,0 +1,5 @@
+copyFileFailed=Error occurred during execution of copyfile rule.
+errorNoDefault=Error: no default remote in file {0}.
+errorParsingManifestFile=Error occurred during parsing manifest file {0}.
+invalidManifest=Invalid manifest.
+repoCommitMessage=Added repo manifest.
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java
new file mode 100644
index 000000000..475fbacaf
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright (C) 2014, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.gitrepo;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.channels.FileChannel;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.jgit.api.AddCommand;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.GitCommand;
+import org.eclipse.jgit.api.SubmoduleAddCommand;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.gitrepo.internal.RepoText;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+import org.xml.sax.helpers.DefaultHandler;
+import org.xml.sax.helpers.XMLReaderFactory;
+
+/**
+ * A class used to execute a repo command.
+ *
+ * This will parse a repo XML manifest, convert it into .gitmodules file and the
+ * repository config file.
+ *
+ * @see git-repo project page
+ * @since 3.4
+ */
+public class RepoCommand extends GitCommand {
+
+ private String path;
+ private String uri;
+ private String groups;
+
+ private Git git;
+ private ProgressMonitor monitor;
+
+ private static class CopyFile {
+ final String src;
+ final String dest;
+ final String relativeDest;
+
+ CopyFile(Repository repo, String path, String src, String dest) {
+ this.src = repo.getWorkTree() + "/" + path + "/" + src; //$NON-NLS-1$ //$NON-NLS-2$
+ this.relativeDest = dest;
+ this.dest = repo.getWorkTree() + "/" + dest; //$NON-NLS-1$
+ }
+
+ void copy() throws IOException {
+ FileInputStream input = new FileInputStream(src);
+ try {
+ FileOutputStream output = new FileOutputStream(dest);
+ try {
+ FileChannel channel = input.getChannel();
+ output.getChannel().transferFrom(channel, 0, channel.size());
+ } finally {
+ output.close();
+ }
+ } finally {
+ input.close();
+ }
+ }
+ }
+
+ private static class Project {
+ final String name;
+ final String path;
+ final Set groups;
+ final List copyfiles;
+
+ Project(String name, String path, String groups) {
+ this.name = name;
+ this.path = path;
+ this.groups = new HashSet();
+ if (groups != null && groups.length() > 0)
+ this.groups.addAll(Arrays.asList(groups.split(","))); //$NON-NLS-1$
+ copyfiles = new ArrayList();
+ }
+
+ void addCopyFile(CopyFile copyfile) {
+ copyfiles.add(copyfile);
+ }
+ }
+
+ private static class XmlManifest extends DefaultHandler {
+ private final RepoCommand command;
+ private final String filename;
+ private final String baseUrl;
+ private final Map remotes;
+ private final List projects;
+ private final Set plusGroups;
+ private final Set minusGroups;
+ private String defaultRemote;
+ private Project currentProject;
+
+ XmlManifest(RepoCommand command, String filename, String baseUrl, String groups) {
+ this.command = command;
+ this.filename = filename;
+ this.baseUrl = baseUrl;
+ remotes = new HashMap();
+ projects = new ArrayList();
+ plusGroups = new HashSet();
+ minusGroups = new HashSet();
+ if (groups == null || groups.length() == 0 || groups.equals("default")) { //$NON-NLS-1$
+ // default means "all,-notdefault"
+ minusGroups.add("notdefault"); //$NON-NLS-1$
+ } else {
+ for (String group : groups.split(",")) { //$NON-NLS-1$
+ if (group.startsWith("-")) //$NON-NLS-1$
+ minusGroups.add(group.substring(1));
+ else
+ plusGroups.add(group);
+ }
+ }
+ }
+
+ void read() throws IOException {
+ final XMLReader xr;
+ try {
+ xr = XMLReaderFactory.createXMLReader();
+ } catch (SAXException e) {
+ throw new IOException(JGitText.get().noXMLParserAvailable);
+ }
+ xr.setContentHandler(this);
+ final FileInputStream in = new FileInputStream(filename);
+ try {
+ xr.parse(new InputSource(in));
+ } catch (SAXException e) {
+ IOException error = new IOException(MessageFormat.format(
+ RepoText.get().errorParsingManifestFile, filename));
+ error.initCause(e);
+ throw error;
+ } finally {
+ in.close();
+ }
+ }
+
+ @Override
+ public void startElement(
+ String uri,
+ String localName,
+ String qName,
+ Attributes attributes) throws SAXException {
+ if ("project".equals(qName)) { //$NON-NLS-1$
+ currentProject = new Project( //$NON-NLS-1$
+ attributes.getValue("name"), //$NON-NLS-1$
+ attributes.getValue("path"), //$NON-NLS-1$
+ attributes.getValue("groups")); //$NON-NLS-1$
+ } else if ("remote".equals(qName)) { //$NON-NLS-1$
+ remotes.put(attributes.getValue("name"), //$NON-NLS-1$
+ attributes.getValue("fetch")); //$NON-NLS-1$
+ } else if ("default".equals(qName)) { //$NON-NLS-1$
+ defaultRemote = attributes.getValue("remote"); //$NON-NLS-1$
+ } else if ("copyfile".equals(qName)) { //$NON-NLS-1$
+ if (currentProject == null)
+ throw new SAXException(RepoText.get().invalidManifest);
+ currentProject.addCopyFile(new CopyFile(
+ command.repo,
+ currentProject.path,
+ attributes.getValue("src"), //$NON-NLS-1$
+ attributes.getValue("dest"))); //$NON-NLS-1$
+ }
+ }
+
+ @Override
+ public void endElement(
+ String uri,
+ String localName,
+ String qName) throws SAXException {
+ if ("project".equals(qName)) { //$NON-NLS-1$
+ projects.add(currentProject);
+ currentProject = null;
+ }
+ }
+
+ @Override
+ public void endDocument() throws SAXException {
+ if (defaultRemote == null) {
+ throw new SAXException(MessageFormat.format(
+ RepoText.get().errorNoDefault, filename));
+ }
+ final String remoteUrl;
+ try {
+ URI uri = new URI(String.format("%s/%s/", baseUrl, remotes.get(defaultRemote))); //$NON-NLS-1$
+ remoteUrl = uri.normalize().toString();
+ } catch (URISyntaxException e) {
+ throw new SAXException(e);
+ }
+ for (Project proj : projects) {
+ if (inGroups(proj)) {
+ String url = remoteUrl + proj.name;
+ command.addSubmodule(url, proj.path);
+ for (CopyFile copyfile : proj.copyfiles) {
+ try {
+ copyfile.copy();
+ } catch (IOException e) {
+ throw new SAXException(
+ RepoText.get().copyFileFailed, e);
+ }
+ AddCommand add = command.git
+ .add()
+ .addFilepattern(copyfile.relativeDest);
+ try {
+ add.call();
+ } catch (GitAPIException e) {
+ throw new SAXException(e);
+ }
+ }
+ }
+ }
+ }
+
+ boolean inGroups(Project proj) {
+ for (String group : minusGroups) {
+ if (proj.groups.contains(group)) {
+ // minus groups have highest priority.
+ return false;
+ }
+ }
+ if (plusGroups.isEmpty() || plusGroups.contains("all")) { //$NON-NLS-1$
+ // empty plus groups means "all"
+ return true;
+ }
+ for (String group : plusGroups) {
+ if (proj.groups.contains(group))
+ return true;
+ }
+ return false;
+ }
+ }
+
+ private static class ManifestErrorException extends GitAPIException {
+ ManifestErrorException(Throwable cause) {
+ super(RepoText.get().invalidManifest, cause);
+ }
+ }
+
+ /**
+ * @param repo
+ */
+ public RepoCommand(final Repository repo) {
+ super(repo);
+ }
+
+ /**
+ * Set path to the manifest XML file
+ *
+ * @param path
+ * (with /
as separator)
+ * @return this command
+ */
+ public RepoCommand setPath(final String path) {
+ this.path = path;
+ return this;
+ }
+
+ /**
+ * Set base URI of the pathes inside the XML
+ *
+ * @param uri
+ * @return this command
+ */
+ public RepoCommand setURI(final String uri) {
+ this.uri = uri;
+ return this;
+ }
+
+ /**
+ * Set groups to sync
+ *
+ * @param groups groups separated by comma, examples: default|all|G1,-G2,-G3
+ * @return this command
+ */
+ public RepoCommand setGroups(final String groups) {
+ this.groups = groups;
+ return this;
+ }
+
+ /**
+ * The progress monitor associated with the clone operation. By default,
+ * this is set to NullProgressMonitor
+ *
+ * @see org.eclipse.jgit.lib.NullProgressMonitor
+ * @param monitor
+ * @return this command
+ */
+ public RepoCommand setProgressMonitor(final ProgressMonitor monitor) {
+ this.monitor = monitor;
+ return this;
+ }
+
+ @Override
+ public RevCommit call() throws GitAPIException {
+ checkCallable();
+ if (path == null || path.length() == 0)
+ throw new IllegalArgumentException(JGitText.get().pathNotConfigured);
+ if (uri == null || uri.length() == 0)
+ throw new IllegalArgumentException(JGitText.get().uriNotConfigured);
+
+ git = new Git(repo);
+ XmlManifest manifest = new XmlManifest(this, path, uri, groups);
+ try {
+ manifest.read();
+ } catch (IOException e) {
+ throw new ManifestErrorException(e);
+ }
+
+ return git
+ .commit()
+ .setMessage(RepoText.get().repoCommitMessage)
+ .call();
+ }
+
+ private void addSubmodule(String url, String name) throws SAXException {
+ SubmoduleAddCommand add = git
+ .submoduleAdd()
+ .setPath(name)
+ .setURI(url);
+ if (monitor != null)
+ add.setProgressMonitor(monitor);
+ try {
+ add.call();
+ } catch (GitAPIException e) {
+ throw new SAXException(e);
+ }
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/internal/RepoText.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/internal/RepoText.java
new file mode 100644
index 000000000..1313fff0d
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/internal/RepoText.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2014, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.gitrepo.internal;
+
+import org.eclipse.jgit.nls.NLS;
+import org.eclipse.jgit.nls.TranslationBundle;
+
+/**
+ * Translation bundle for repo command
+ */
+public class RepoText extends TranslationBundle {
+
+ /**
+ * @return an instance of this translation bundle
+ */
+ public static RepoText get() {
+ return NLS.getBundleFor(RepoText.class);
+ }
+
+ // @formatter:off
+ /***/ public String copyFileFailed;
+ /***/ public String errorNoDefault;
+ /***/ public String errorParsingManifestFile;
+ /***/ public String invalidManifest;
+ /***/ public String repoCommitMessage;
+}