]> source.dussan.org Git - jgit.git/commitdiff
Refactor to expose ManifestParser. 16/48416/2
authorYuxuan 'fishy' Wang <fishywang@google.com>
Thu, 21 May 2015 23:52:32 +0000 (16:52 -0700)
committerYuxuan 'fishy' Wang <fishywang@google.com>
Fri, 22 May 2015 18:08:52 +0000 (11:08 -0700)
The repo xml manifest parser used in RepoCommand could also be useful for
others, so refactor to make it public.

Also this breaks backward compatibility slightly.

Change-Id: I5001bd2fe77541109fe32dbe2597a065e6ad585e
Signed-off-by: Yuxuan 'fishy' Wang <fishywang@google.com>
org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/ManifestParserTest.java [new file with mode: 0644]
org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java
org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java
org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java [new file with mode: 0644]

diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/ManifestParserTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/ManifestParserTest.java
new file mode 100644 (file)
index 0000000..1005b39
--- /dev/null
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2015, 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.assertTrue;
+
+import java.io.StringBufferInputStream;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.junit.Test;
+
+public class ManifestParserTest {
+
+       @Test
+       public void testManifestParser() throws Exception {
+               String baseUrl = "https://git.google.com/";
+               StringBuilder xmlContent = new StringBuilder();
+               Set<String> results = new HashSet<String>();
+               xmlContent.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
+                       .append("<manifest>")
+                       .append("<remote name=\"remote1\" fetch=\".\" />")
+                       .append("<default revision=\"master\" remote=\"remote1\" />")
+                       .append("<project path=\"foo\" name=\"")
+                       .append("foo")
+                       .append("\" groups=\"a,test\" />")
+                       .append("<project path=\"bar\" name=\"")
+                       .append("bar")
+                       .append("\" groups=\"notdefault\" />")
+                       .append("<project path=\"foo/a\" name=\"")
+                       .append("a")
+                       .append("\" groups=\"a\" />")
+                       .append("<project path=\"b\" name=\"")
+                       .append("b")
+                       .append("\" groups=\"b\" />")
+                       .append("</manifest>");
+
+               ManifestParser parser = new ManifestParser(
+                               null, null, "master", baseUrl, null, null);
+               parser.read(new StringBufferInputStream(xmlContent.toString()));
+               // Unfiltered projects should have them all.
+               results.clear();
+               results.add("foo");
+               results.add("bar");
+               results.add("foo/a");
+               results.add("b");
+               for (RepoProject proj : parser.getProjects()) {
+                       String msg = String.format(
+                                       "project \"%s\" should be included in unfiltered projects",
+                                       proj.path);
+                       assertTrue(msg, results.contains(proj.path));
+                       results.remove(proj.path);
+               }
+               assertTrue(
+                               "Unfiltered projects shouldn't contain any unexpected results",
+                               results.isEmpty());
+               // Filtered projects should have foo & b
+               results.clear();
+               results.add("foo");
+               results.add("b");
+               for (RepoProject proj : parser.getFilteredProjects()) {
+                       String msg = String.format(
+                                       "project \"%s\" should be included in filtered projects",
+                                       proj.path);
+                       assertTrue(msg, results.contains(proj.path));
+                       results.remove(proj.path);
+               }
+               assertTrue(
+                               "Filtered projects shouldn't contain any unexpected results",
+                               results.isEmpty());
+       }
+}
index 3d86cfd5bb067a1aa647f697b5a90cca0542ec7b..0caf9c6c27d7f93d28f312ddbe39a248bf473a79 100644 (file)
@@ -49,6 +49,8 @@ import static org.junit.Assert.assertTrue;
 import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileReader;
+import java.util.HashSet;
+import java.util.Set;
 
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.junit.JGitTestUtil;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java
new file mode 100644 (file)
index 0000000..2d50938
--- /dev/null
@@ -0,0 +1,351 @@
+/*
+ * Copyright (C) 2015, 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.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.gitrepo.RepoProject.CopyFile;
+import org.eclipse.jgit.gitrepo.internal.RepoText;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.Repository;
+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;
+
+/**
+ * Repo XML manifest parser.
+ *
+ * @see <a href="https://code.google.com/p/git-repo/">git-repo project page</a>
+ * @since 4.0
+ */
+public class ManifestParser extends DefaultHandler {
+       private final String filename;
+       private final String baseUrl;
+       private final String defaultBranch;
+       private final Repository rootRepo;
+       private final Map<String, String> remotes;
+       private final Set<String> plusGroups;
+       private final Set<String> minusGroups;
+       private List<RepoProject> projects;
+       private List<RepoProject> filteredProjects;
+       private String defaultRemote;
+       private String defaultRevision;
+       private IncludedFileReader includedReader;
+       private int xmlInRead;
+       private RepoProject currentProject;
+
+       /**
+        * A callback to read included xml files.
+        */
+       public interface IncludedFileReader {
+               /**
+                * Read a file from the same base dir of the manifest xml file.
+                *
+                * @param path
+                *            The relative path to the file to read
+                * @return the {@code InputStream} of the file.
+                * @throws GitAPIException
+                * @throws IOException
+                */
+               public InputStream readIncludeFile(String path)
+                               throws GitAPIException, IOException;
+       }
+
+       /**
+        * @param includedReader
+        * @param filename
+        * @param defaultBranch
+        * @param baseUrl
+        * @param groups
+        * @param rootRepo
+        */
+       public ManifestParser(IncludedFileReader includedReader, String filename,
+                       String defaultBranch, String baseUrl, String groups,
+                       Repository rootRepo) {
+               this.includedReader = includedReader;
+               this.filename = filename;
+               this.defaultBranch = defaultBranch;
+               this.rootRepo = rootRepo;
+
+               // Strip trailing /s to match repo behavior.
+               int lastIndex = baseUrl.length() - 1;
+               while (lastIndex >= 0 && baseUrl.charAt(lastIndex) == '/')
+                       lastIndex--;
+               this.baseUrl = baseUrl.substring(0, lastIndex + 1);
+
+               plusGroups = new HashSet<String>();
+               minusGroups = new HashSet<String>();
+               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);
+                       }
+               }
+
+               remotes = new HashMap<String, String>();
+               projects = new ArrayList<RepoProject>();
+               filteredProjects = new ArrayList<RepoProject>();
+       }
+
+       /**
+        * Read the xml file.
+        *
+        * @param inputStream
+        */
+       public void read(InputStream inputStream) throws IOException {
+               xmlInRead++;
+               final XMLReader xr;
+               try {
+                       xr = XMLReaderFactory.createXMLReader();
+               } catch (SAXException e) {
+                       throw new IOException(JGitText.get().noXMLParserAvailable);
+               }
+               xr.setContentHandler(this);
+               try {
+                       xr.parse(new InputSource(inputStream));
+               } catch (SAXException e) {
+                       IOException error = new IOException(
+                                               RepoText.get().errorParsingManifestFile);
+                       error.initCause(e);
+                       throw error;
+               }
+       }
+
+       @Override
+       public void startElement(
+                       String uri,
+                       String localName,
+                       String qName,
+                       Attributes attributes) throws SAXException {
+               if ("project".equals(qName)) { //$NON-NLS-1$
+                       currentProject = new RepoProject(
+                                       attributes.getValue("name"), //$NON-NLS-1$
+                                       attributes.getValue("path"), //$NON-NLS-1$
+                                       attributes.getValue("revision"), //$NON-NLS-1$
+                                       attributes.getValue("remote"), //$NON-NLS-1$
+                                       attributes.getValue("groups")); //$NON-NLS-1$
+               } else if ("remote".equals(qName)) { //$NON-NLS-1$
+                       String alias = attributes.getValue("alias"); //$NON-NLS-1$
+                       String fetch = attributes.getValue("fetch"); //$NON-NLS-1$
+                       remotes.put(attributes.getValue("name"), fetch); //$NON-NLS-1$
+                       if (alias != null)
+                               remotes.put(alias, fetch);
+               } else if ("default".equals(qName)) { //$NON-NLS-1$
+                       defaultRemote = attributes.getValue("remote"); //$NON-NLS-1$
+                       defaultRevision = attributes.getValue("revision"); //$NON-NLS-1$
+                       if (defaultRevision == null)
+                               defaultRevision = defaultBranch;
+               } else if ("copyfile".equals(qName)) { //$NON-NLS-1$
+                       if (currentProject == null)
+                               throw new SAXException(RepoText.get().invalidManifest);
+                       currentProject.addCopyFile(new CopyFile(
+                                               rootRepo,
+                                               currentProject.path,
+                                               attributes.getValue("src"), //$NON-NLS-1$
+                                               attributes.getValue("dest"))); //$NON-NLS-1$
+               } else if ("include".equals(qName)) { //$NON-NLS-1$
+                       String name = attributes.getValue("name"); //$NON-NLS-1$
+                       InputStream is = null;
+                       if (includedReader != null) {
+                               try {
+                                       is = includedReader.readIncludeFile(name);
+                               } catch (Exception e) {
+                                       throw new SAXException(MessageFormat.format(
+                                                       RepoText.get().errorIncludeFile, name), e);
+                               }
+                       } else if (filename != null) {
+                               int index = filename.lastIndexOf('/');
+                               String path = filename.substring(0, index + 1) + name;
+                               try {
+                                       is = new FileInputStream(path);
+                               } catch (IOException e) {
+                                       throw new SAXException(MessageFormat.format(
+                                                       RepoText.get().errorIncludeFile, path), e);
+                               }
+                       }
+                       if (is == null) {
+                               throw new SAXException(
+                                               RepoText.get().errorIncludeNotImplemented);
+                       }
+                       try {
+                               read(is);
+                       } catch (IOException e) {
+                               throw new SAXException(e);
+                       }
+               }
+       }
+
+       @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 {
+               xmlInRead--;
+               if (xmlInRead != 0)
+                       return;
+
+               // Only do the following after we finished reading everything.
+               Map<String, String> remoteUrls = new HashMap<String, String>();
+               URI baseUri;
+               try {
+                       baseUri = new URI(baseUrl);
+               } catch (URISyntaxException e) {
+                       throw new SAXException(e);
+               }
+               for (RepoProject proj : projects) {
+                       String remote = proj.remote;
+                       if (remote == null) {
+                               if (defaultRemote == null) {
+                                       if (filename != null)
+                                               throw new SAXException(MessageFormat.format(
+                                                               RepoText.get().errorNoDefaultFilename,
+                                                               filename));
+                                       else
+                                               throw new SAXException(
+                                                               RepoText.get().errorNoDefault);
+                               }
+                               remote = defaultRemote;
+                       }
+                       String remoteUrl = remoteUrls.get(remote);
+                       if (remoteUrl == null) {
+                               remoteUrl = baseUri.resolve(remotes.get(remote)).toString();
+                               if (!remoteUrl.endsWith("/")) //$NON-NLS-1$
+                                       remoteUrl = remoteUrl + "/"; //$NON-NLS-1$
+                               remoteUrls.put(remote, remoteUrl);
+                       }
+                       proj.setUrl(remoteUrl + proj.name)
+                                       .setDefaultRevision(defaultRevision);
+               }
+
+               filteredProjects.addAll(projects);
+               removeNotInGroup();
+               removeOverlaps();
+       }
+
+       /**
+        * Getter for projects.
+        */
+       public List<RepoProject> getProjects() {
+               return projects;
+       }
+
+       /**
+        * Getter for filterdProjects.
+        */
+       public List<RepoProject> getFilteredProjects() {
+               return filteredProjects;
+       }
+
+       /** Remove projects that are not in our desired groups. */
+       void removeNotInGroup() {
+               Iterator<RepoProject> iter = filteredProjects.iterator();
+               while (iter.hasNext())
+                       if (!inGroups(iter.next()))
+                               iter.remove();
+       }
+
+       /** Remove projects that sits in a subdirectory of any other project. */
+       void removeOverlaps() {
+               Collections.sort(filteredProjects);
+               Iterator<RepoProject> iter = filteredProjects.iterator();
+               if (!iter.hasNext())
+                       return;
+               RepoProject last = iter.next();
+               while (iter.hasNext()) {
+                       RepoProject p = iter.next();
+                       if (last.isAncestorOf(p))
+                               iter.remove();
+                       else
+                               last = p;
+               }
+       }
+
+       boolean inGroups(RepoProject 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;
+       }
+}
index 0cf5c7e48f735f6c8cb1e9300b054bca80f2ddf4..e8a9e18f130821c404be31ce0a73db046599bf0e 100644 (file)
@@ -44,22 +44,13 @@ package org.eclipse.jgit.gitrepo;
 
 import java.io.File;
 import java.io.FileInputStream;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 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.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.GitCommand;
@@ -70,6 +61,8 @@ 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.ManifestParser.IncludedFileReader;
+import org.eclipse.jgit.gitrepo.RepoProject.CopyFile;
 import org.eclipse.jgit.gitrepo.internal.RepoText;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.CommitBuilder;
@@ -89,12 +82,6 @@ import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.util.FileUtils;
-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.
@@ -124,7 +111,7 @@ public class RepoCommand extends GitCommand<RevCommit> {
        private InputStream inputStream;
        private IncludedFileReader includedReader;
 
-       private List<Project> bareProjects;
+       private List<RepoProject> bareProjects;
        private Git git;
        private ProgressMonitor monitor;
 
@@ -221,341 +208,6 @@ public class RepoCommand extends GitCommand<RevCommit> {
                }
        }
 
-       /**
-        * A callback to read included xml files.
-        *
-        * @since 3.5
-        */
-       public interface IncludedFileReader {
-               /**
-                * Read a file from the same base dir of the manifest xml file.
-                *
-                * @param path
-                *            The relative path to the file to read
-                * @return the {@code InputStream} of the file.
-                * @throws GitAPIException
-                * @throws IOException
-                */
-               public InputStream readIncludeFile(String path)
-                               throws GitAPIException, IOException;
-       }
-
-       private static class CopyFile {
-               final Repository repo;
-               final String path;
-               final String src;
-               final String dest;
-
-               CopyFile(Repository repo, String path, String src, String dest) {
-                       this.repo = repo;
-                       this.path = path;
-                       this.src = src;
-                       this.dest = dest;
-               }
-
-               void copy() throws IOException {
-                       File srcFile = new File(repo.getWorkTree(),
-                                       path + "/" + src); //$NON-NLS-1$
-                       File destFile = new File(repo.getWorkTree(), dest);
-                       FileInputStream input = new FileInputStream(srcFile);
-                       try {
-                               FileOutputStream output = new FileOutputStream(destFile);
-                               try {
-                                       FileChannel channel = input.getChannel();
-                                       output.getChannel().transferFrom(channel, 0, channel.size());
-                               } finally {
-                                       output.close();
-                               }
-                       } finally {
-                               input.close();
-                       }
-               }
-       }
-
-       private static class Project implements Comparable<Project> {
-               final String name;
-               final String path;
-               final String revision;
-               final String remote;
-               final Set<String> groups;
-               final List<CopyFile> copyfiles;
-
-               Project(String name, String path, String revision,
-                               String remote, String groups) {
-                       this.name = name;
-                       if (path != null)
-                               this.path = path;
-                       else
-                               this.path = name;
-                       this.revision = revision;
-                       this.remote = remote;
-                       this.groups = new HashSet<String>();
-                       if (groups != null && groups.length() > 0)
-                               this.groups.addAll(Arrays.asList(groups.split(","))); //$NON-NLS-1$
-                       copyfiles = new ArrayList<CopyFile>();
-               }
-
-               void addCopyFile(CopyFile copyfile) {
-                       copyfiles.add(copyfile);
-               }
-
-               String getPathWithSlash() {
-                       if (path.endsWith("/")) //$NON-NLS-1$
-                               return path;
-                       else
-                               return path + "/"; //$NON-NLS-1$
-               }
-
-               boolean isAncestorOf(Project that) {
-                       return that.getPathWithSlash().startsWith(this.getPathWithSlash());
-               }
-
-               @Override
-               public boolean equals(Object o) {
-                       if (o instanceof Project) {
-                               Project that = (Project) o;
-                               return this.getPathWithSlash().equals(that.getPathWithSlash());
-                       }
-                       return false;
-               }
-
-               @Override
-               public int hashCode() {
-                       return this.getPathWithSlash().hashCode();
-               }
-
-               public int compareTo(Project that) {
-                       return this.getPathWithSlash().compareTo(that.getPathWithSlash());
-               }
-       }
-
-       private static class XmlManifest extends DefaultHandler {
-               private final RepoCommand command;
-               private final String filename;
-               private final String baseUrl;
-               private final Map<String, String> remotes;
-               private final Set<String> plusGroups;
-               private final Set<String> minusGroups;
-               private List<Project> projects;
-               private String defaultRemote;
-               private String defaultRevision;
-               private IncludedFileReader includedReader;
-               private int xmlInRead;
-               private Project currentProject;
-
-               XmlManifest(RepoCommand command, IncludedFileReader includedReader,
-                               String filename, String baseUrl, String groups) {
-                       this.command = command;
-                       this.includedReader = includedReader;
-                       this.filename = filename;
-
-                       // Strip trailing /s to match repo behavior.
-                       int lastIndex = baseUrl.length() - 1;
-                       while (lastIndex >= 0 && baseUrl.charAt(lastIndex) == '/')
-                               lastIndex--;
-                       this.baseUrl = baseUrl.substring(0, lastIndex + 1);
-
-                       remotes = new HashMap<String, String>();
-                       projects = new ArrayList<Project>();
-                       plusGroups = new HashSet<String>();
-                       minusGroups = new HashSet<String>();
-                       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(InputStream inputStream) throws IOException {
-                       xmlInRead++;
-                       final XMLReader xr;
-                       try {
-                               xr = XMLReaderFactory.createXMLReader();
-                       } catch (SAXException e) {
-                               throw new IOException(JGitText.get().noXMLParserAvailable);
-                       }
-                       xr.setContentHandler(this);
-                       try {
-                               xr.parse(new InputSource(inputStream));
-                       } catch (SAXException e) {
-                               IOException error = new IOException(
-                                                       RepoText.get().errorParsingManifestFile);
-                               error.initCause(e);
-                               throw error;
-                       }
-               }
-
-               @Override
-               public void startElement(
-                               String uri,
-                               String localName,
-                               String qName,
-                               Attributes attributes) throws SAXException {
-                       if ("project".equals(qName)) { //$NON-NLS-1$
-                               currentProject = new Project(
-                                               attributes.getValue("name"), //$NON-NLS-1$
-                                               attributes.getValue("path"), //$NON-NLS-1$
-                                               attributes.getValue("revision"), //$NON-NLS-1$
-                                               attributes.getValue("remote"), //$NON-NLS-1$
-                                               attributes.getValue("groups")); //$NON-NLS-1$
-                       } else if ("remote".equals(qName)) { //$NON-NLS-1$
-                               String alias = attributes.getValue("alias"); //$NON-NLS-1$
-                               String fetch = attributes.getValue("fetch"); //$NON-NLS-1$
-                               remotes.put(attributes.getValue("name"), fetch); //$NON-NLS-1$
-                               if (alias != null)
-                                       remotes.put(alias, fetch);
-                       } else if ("default".equals(qName)) { //$NON-NLS-1$
-                               defaultRemote = attributes.getValue("remote"); //$NON-NLS-1$
-                               defaultRevision = attributes.getValue("revision"); //$NON-NLS-1$
-                               if (defaultRevision == null)
-                                       defaultRevision = command.branch;
-                       } 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$
-                       } else if ("include".equals(qName)) { //$NON-NLS-1$
-                               String name = attributes.getValue("name"); //$NON-NLS-1$
-                               InputStream is = null;
-                               if (includedReader != null) {
-                                       try {
-                                               is = includedReader.readIncludeFile(name);
-                                       } catch (Exception e) {
-                                               throw new SAXException(MessageFormat.format(
-                                                               RepoText.get().errorIncludeFile, name), e);
-                                       }
-                               } else if (filename != null) {
-                                       int index = filename.lastIndexOf('/');
-                                       String path = filename.substring(0, index + 1) + name;
-                                       try {
-                                               is = new FileInputStream(path);
-                                       } catch (IOException e) {
-                                               throw new SAXException(MessageFormat.format(
-                                                               RepoText.get().errorIncludeFile, path), e);
-                                       }
-                               }
-                               if (is == null) {
-                                       throw new SAXException(
-                                                       RepoText.get().errorIncludeNotImplemented);
-                               }
-                               try {
-                                       read(is);
-                               } catch (IOException e) {
-                                       throw new SAXException(e);
-                               }
-                       }
-               }
-
-               @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 {
-                       xmlInRead--;
-                       if (xmlInRead != 0)
-                               return;
-
-                       // Only do the following after we finished reading everything.
-                       removeNotInGroup();
-                       removeOverlaps();
-
-                       Map<String, String> remoteUrls = new HashMap<String, String>();
-                       URI baseUri;
-                       try {
-                               baseUri = new URI(baseUrl);
-                       } catch (URISyntaxException e) {
-                               throw new SAXException(e);
-                       }
-                       for (Project proj : projects) {
-                               String remote = proj.remote;
-                               if (remote == null) {
-                                       if (defaultRemote == null) {
-                                               if (filename != null)
-                                                       throw new SAXException(MessageFormat.format(
-                                                                       RepoText.get().errorNoDefaultFilename,
-                                                                       filename));
-                                               else
-                                                       throw new SAXException(
-                                                                       RepoText.get().errorNoDefault);
-                                       }
-                                       remote = defaultRemote;
-                               }
-                               String remoteUrl = remoteUrls.get(remote);
-                               if (remoteUrl == null) {
-                                       remoteUrl = baseUri.resolve(remotes.get(remote)).toString();
-                                       if (!remoteUrl.endsWith("/")) //$NON-NLS-1$
-                                               remoteUrl = remoteUrl + "/"; //$NON-NLS-1$
-                                       remoteUrls.put(remote, remoteUrl);
-                               }
-
-                               command.addSubmodule(remoteUrl + proj.name,
-                                               proj.path,
-                                               proj.revision == null
-                                                               ? defaultRevision : proj.revision,
-                                               proj.copyfiles);
-                       }
-               }
-
-               /** Remove projects that are not in our desired groups. */
-               void removeNotInGroup() {
-                       Iterator<Project> iter = projects.iterator();
-                       while (iter.hasNext())
-                               if (!inGroups(iter.next()))
-                                       iter.remove();
-               }
-
-               /** Remove projects that sits in a subdirectory of any other project. */
-               void removeOverlaps() {
-                       Collections.sort(projects);
-                       Iterator<Project> iter = projects.iterator();
-                       if (!iter.hasNext())
-                               return;
-                       Project last = iter.next();
-                       while (iter.hasNext()) {
-                               Project p = iter.next();
-                               if (last.isAncestorOf(p))
-                                       iter.remove();
-                               else
-                                       last = p;
-                       }
-               }
-
-               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;
-               }
-       }
-
        @SuppressWarnings("serial")
        private static class ManifestErrorException extends GitAPIException {
                ManifestErrorException(Throwable cause) {
@@ -715,7 +367,7 @@ public class RepoCommand extends GitCommand<RevCommit> {
                        }
 
                        if (repo.isBare()) {
-                               bareProjects = new ArrayList<Project>();
+                               bareProjects = new ArrayList<RepoProject>();
                                if (author == null)
                                        author = new PersonIdent(repo);
                                if (callback == null)
@@ -723,11 +375,17 @@ public class RepoCommand extends GitCommand<RevCommit> {
                        } else
                                git = new Git(repo);
 
-                       XmlManifest manifest = new XmlManifest(
-                                       this, includedReader, path, uri, groups);
+                       ManifestParser parser = new ManifestParser(
+                                       includedReader, path, branch, uri, groups, repo);
                        try {
-                               manifest.read(inputStream);
-                       } catch (IOException e) {
+                               parser.read(inputStream);
+                               for (RepoProject proj : parser.getFilteredProjects()) {
+                                       addSubmodule(proj.url,
+                                                       proj.path,
+                                                       proj.getRevision(),
+                                                       proj.copyfiles);
+                               }
+                       } catch (GitAPIException | IOException e) {
                                throw new ManifestErrorException(e);
                        }
                } finally {
@@ -745,7 +403,7 @@ public class RepoCommand extends GitCommand<RevCommit> {
                        ObjectInserter inserter = repo.newObjectInserter();
                        try (RevWalk rw = new RevWalk(repo)) {
                                Config cfg = new Config();
-                               for (Project proj : bareProjects) {
+                               for (RepoProject proj : bareProjects) {
                                        String name = proj.path;
                                        String nameUri = proj.name;
                                        cfg.setString("submodule", name, "path", name); //$NON-NLS-1$ //$NON-NLS-2$
@@ -835,9 +493,9 @@ public class RepoCommand extends GitCommand<RevCommit> {
        }
 
        private void addSubmodule(String url, String name, String revision,
-                       List<CopyFile> copyfiles) throws SAXException {
+                       List<CopyFile> copyfiles) throws GitAPIException, IOException {
                if (repo.isBare()) {
-                       Project proj = new Project(url, name, revision, null, null);
+                       RepoProject proj = new RepoProject(url, name, revision, null, null);
                        proj.copyfiles.addAll(copyfiles);
                        bareProjects.add(proj);
                } else {
@@ -848,24 +506,18 @@ public class RepoCommand extends GitCommand<RevCommit> {
                        if (monitor != null)
                                add.setProgressMonitor(monitor);
 
-                       try {
-                               Repository subRepo = add.call();
-                               if (revision != null) {
-                                       try (Git sub = new Git(subRepo)) {
-                                               sub.checkout().setName(findRef(revision, subRepo))
-                                                               .call();
-                                       }
-                                       subRepo.close();
-                                       git.add().addFilepattern(name).call();
-                               }
-                               for (CopyFile copyfile : copyfiles) {
-                                       copyfile.copy();
-                                       git.add().addFilepattern(copyfile.dest).call();
+                       Repository subRepo = add.call();
+                       if (revision != null) {
+                               try (Git sub = new Git(subRepo)) {
+                                       sub.checkout().setName(findRef(revision, subRepo))
+                                                       .call();
                                }
-                       } catch (GitAPIException e) {
-                               throw new SAXException(e);
-                       } catch (IOException e) {
-                               throw new SAXException(e);
+                               subRepo.close();
+                               git.add().addFilepattern(name).call();
+                       }
+                       for (CopyFile copyfile : copyfiles) {
+                               copyfile.copy();
+                               git.add().addFilepattern(copyfile.dest).call();
                        }
                }
        }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java
new file mode 100644 (file)
index 0000000..c4648ab
--- /dev/null
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2015, 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.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.channels.FileChannel;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * The representation of a repo sub project.
+ *
+ * @see <a href="https://code.google.com/p/git-repo/">git-repo project page</a>
+ * @since 4.0
+ */
+public class RepoProject implements Comparable<RepoProject> {
+       final String name;
+       final String path;
+       final String revision;
+       final String remote;
+       final Set<String> groups;
+       final List<CopyFile> copyfiles;
+       String url;
+       String defaultRevision;
+
+       /**
+        * The representation of a copy file configuration.
+        */
+       public static class CopyFile {
+               final Repository repo;
+               final String path;
+               final String src;
+               final String dest;
+
+               /**
+                * @param repo
+                * @param path
+                *            the path of the project containing this copyfile config.
+                * @param src
+                * @param dest
+                */
+               public CopyFile(Repository repo, String path, String src, String dest) {
+                       this.repo = repo;
+                       this.path = path;
+                       this.src = src;
+                       this.dest = dest;
+               }
+
+               /**
+                * Do the copy file action.
+                */
+               public void copy() throws IOException {
+                       File srcFile = new File(repo.getWorkTree(),
+                                       path + "/" + src); //$NON-NLS-1$
+                       File destFile = new File(repo.getWorkTree(), dest);
+                       FileInputStream input = new FileInputStream(srcFile);
+                       try {
+                               FileOutputStream output = new FileOutputStream(destFile);
+                               try {
+                                       FileChannel channel = input.getChannel();
+                                       output.getChannel().transferFrom(channel, 0, channel.size());
+                               } finally {
+                                       output.close();
+                               }
+                       } finally {
+                               input.close();
+                       }
+               }
+       }
+
+       /**
+        * @param name
+        * @param path
+        * @param revision
+        * @param remote
+        * @param groups
+        */
+       public RepoProject(String name, String path, String revision,
+                       String remote, String groups) {
+               this.name = name;
+               if (path != null)
+                       this.path = path;
+               else
+                       this.path = name;
+               this.revision = revision;
+               this.remote = remote;
+               this.groups = new HashSet<String>();
+               if (groups != null && groups.length() > 0)
+                       this.groups.addAll(Arrays.asList(groups.split(","))); //$NON-NLS-1$
+               copyfiles = new ArrayList<CopyFile>();
+       }
+
+       /**
+        * Set the url of the sub repo.
+        *
+        * @param url
+        * @return this for chaining.
+        */
+       public RepoProject setUrl(String url) {
+               this.url = url;
+               return this;
+       }
+
+       /**
+        * Set the default revision for the sub repo.
+        *
+        * @param defaultRevision
+        * @return this for chaining.
+        */
+       public RepoProject setDefaultRevision(String defaultRevision) {
+               this.defaultRevision = defaultRevision;
+               return this;
+       }
+
+       /**
+        * Get the revision of the sub repo.
+        *
+        * @return revision if set, or default revision.
+        */
+       public String getRevision() {
+               return revision == null ? defaultRevision : revision;
+       }
+
+       /**
+        * Add a copy file configuration.
+        *
+        * @param copyfile
+        */
+       public void addCopyFile(CopyFile copyfile) {
+               copyfiles.add(copyfile);
+       }
+
+       String getPathWithSlash() {
+               if (path.endsWith("/")) //$NON-NLS-1$
+                       return path;
+               else
+                       return path + "/"; //$NON-NLS-1$
+       }
+
+       /**
+        * Check if this sub repo is the ancestor of another sub repo.
+        */
+       public boolean isAncestorOf(RepoProject that) {
+               return that.getPathWithSlash().startsWith(this.getPathWithSlash());
+       }
+
+       @Override
+       public boolean equals(Object o) {
+               if (o instanceof RepoProject) {
+                       RepoProject that = (RepoProject) o;
+                       return this.getPathWithSlash().equals(that.getPathWithSlash());
+               }
+               return false;
+       }
+
+       @Override
+       public int hashCode() {
+               return this.getPathWithSlash().hashCode();
+       }
+
+       @Override
+       public int compareTo(RepoProject that) {
+               return this.getPathWithSlash().compareTo(that.getPathWithSlash());
+       }
+}
+