manifest_xml: initial support for <superproject>

At most one superproject may be specified. It will be used
to specify the URL of superproject.

It would have 3 attributes: remote, name, and default.
Only "name" is required while the others have reasonable defaults.

<remote name="superproject-url" review="<url>" />
<superproject remote="superproject-url" name="platform/superproject"/>

TODO: This CL only implements the parsing logic and further work
will be in followup CLs.

Tested the code with the following commands.

$ ./run_tests tests/test_manifest_xml.py
$ ./run_tests -v

Bug: https://crbug.com/gerrit/13709
Tested-by: Raman Tenneti <rtenneti@google.com>
Change-Id: I5b4bba02c8b59601c754cf6b5e4d07a1e16ce167
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/292982
Reviewed-by: Mike Frysinger <vapier@google.com>
diff --git a/docs/manifest-format.md b/docs/manifest-format.md
index ca385ba..8e5e287 100644
--- a/docs/manifest-format.md
+++ b/docs/manifest-format.md
@@ -29,6 +29,7 @@
                       project*,
                       extend-project*,
                       repo-hooks?,
+                      superproject?,
                       include*)>
 
   <!ELEMENT notice (#PCDATA)>
@@ -98,6 +99,10 @@
   <!ATTLIST repo-hooks in-project CDATA #REQUIRED>
   <!ATTLIST repo-hooks enabled-list CDATA #REQUIRED>
 
+  <!ELEMENT superproject (EMPTY)>
+  <!ATTLIST superproject name    CDATA #REQUIRED>
+  <!ATTLIST superproject remote  IDREF #IMPLIED>
+
   <!ELEMENT include EMPTY>
   <!ATTLIST include name   CDATA #REQUIRED>
   <!ATTLIST include groups CDATA #IMPLIED>
@@ -377,6 +382,28 @@
 
 Attribute `enabled-list`: List of hooks to use, whitespace or comma separated.
 
+### Element superproject
+
+***
+ *Note*: This is currently a WIP.
+***
+
+NB: See the [git superprojects documentation](
+https://en.wikibooks.org/wiki/Git/Submodules_and_Superprojects) for background
+information.
+
+This element is used to specify the URL of the superproject. It has "name" and
+"remote" as atrributes. Only "name" is required while the others have
+reasonable defaults. At most one superproject may be specified.
+Attempting to redefine it will fail to parse.
+
+Attribute `name`: A unique name for the superproject. This attribute has the
+same meaning as project's name attribute. See the
+[element project](#element-project) for more information.
+
+Attribute `remote`: Name of a previously defined remote element.
+If not supplied the remote given by the default element is used.
+
 ### Element include
 
 This element provides the capability of including another manifest
diff --git a/manifest_xml.py b/manifest_xml.py
index 9b7a81b..eb8a98c 100644
--- a/manifest_xml.py
+++ b/manifest_xml.py
@@ -463,6 +463,19 @@
                      ' '.join(self._repo_hooks_project.enabled_repo_hooks))
       root.appendChild(e)
 
+    if self._superproject:
+      root.appendChild(doc.createTextNode(''))
+      e = doc.createElement('superproject')
+      e.setAttribute('name', self._superproject['name'])
+      remoteName = None
+      if d.remote:
+        remoteName = d.remote.name
+      remote = self._superproject.get('remote')
+      if not d.remote or remote.orig_name != remoteName:
+        remoteName = remote.orig_name
+        e.setAttribute('remote', remoteName)
+      root.appendChild(e)
+
     return doc
 
   def ToDict(self, **kwargs):
@@ -473,6 +486,7 @@
         'default',
         'manifest-server',
         'repo-hooks',
+        'superproject',
     }
     # Elements that may be repeated.
     MULTI_ELEMENTS = {
@@ -545,6 +559,11 @@
     return self._repo_hooks_project
 
   @property
+  def superproject(self):
+    self._Load()
+    return self._superproject
+
+  @property
   def notice(self):
     self._Load()
     return self._notice
@@ -591,6 +610,7 @@
     self._remotes = {}
     self._default = None
     self._repo_hooks_project = None
+    self._superproject = {}
     self._notice = None
     self.branch = None
     self._manifest_server = None
@@ -793,6 +813,23 @@
 
         # Store the enabled hooks in the Project object.
         self._repo_hooks_project.enabled_repo_hooks = enabled_repo_hooks
+      if node.nodeName == 'superproject':
+        name = self._reqatt(node, 'name')
+        # There can only be one superproject.
+        if self._superproject.get('name'):
+          raise ManifestParseError(
+              'duplicate superproject in %s' %
+              (self.manifestFile))
+        self._superproject['name'] = name
+        remote_name = node.getAttribute('remote')
+        if not remote_name:
+          remote = self._default.remote
+        else:
+          remote = self._get_remote(node)
+        if remote is None:
+          raise ManifestParseError("no remote for superproject %s within %s" %
+                                   (name, self.manifestFile))
+        self._superproject['remote'] = remote.ToRemoteSpec(name)
       if node.nodeName == 'remove-project':
         name = self._reqatt(node, 'name')
 
diff --git a/tests/test_manifest_xml.py b/tests/test_manifest_xml.py
index d53ea56..e4adf3c 100644
--- a/tests/test_manifest_xml.py
+++ b/tests/test_manifest_xml.py
@@ -221,6 +221,65 @@
     self.assertEqual(manifest.repo_hooks_project.name, 'repohooks')
     self.assertEqual(manifest.repo_hooks_project.enabled_repo_hooks, ['a', 'b'])
 
+  def test_superproject(self):
+    """Check superproject settings."""
+    manifest = self.getXmlManifest("""
+<manifest>
+  <remote name="test-remote" fetch="http://localhost" />
+  <default remote="test-remote" revision="refs/heads/main" />
+  <superproject name="superproject"/>
+</manifest>
+""")
+    self.assertEqual(manifest.superproject['name'], 'superproject')
+    self.assertEqual(manifest.superproject['remote'].name, 'test-remote')
+    self.assertEqual(
+        manifest.ToXml().toxml(),
+        '<?xml version="1.0" ?><manifest>' +
+        '<remote name="test-remote" fetch="http://localhost"/>' +
+        '<default remote="test-remote" revision="refs/heads/main"/>' +
+        '<superproject name="superproject"/>' +
+        '</manifest>')
+
+  def test_superproject_with_remote(self):
+    """Check superproject settings."""
+    manifest = self.getXmlManifest("""
+<manifest>
+  <remote name="default-remote" fetch="http://localhost" />
+  <remote name="test-remote" fetch="http://localhost" />
+  <default remote="default-remote" revision="refs/heads/main" />
+  <superproject name="superproject" remote="test-remote"/>
+</manifest>
+""")
+    self.assertEqual(manifest.superproject['name'], 'superproject')
+    self.assertEqual(manifest.superproject['remote'].name, 'test-remote')
+    self.assertEqual(
+        manifest.ToXml().toxml(),
+        '<?xml version="1.0" ?><manifest>' +
+        '<remote name="default-remote" fetch="http://localhost"/>' +
+        '<remote name="test-remote" fetch="http://localhost"/>' +
+        '<default remote="default-remote" revision="refs/heads/main"/>' +
+        '<superproject name="superproject" remote="test-remote"/>' +
+        '</manifest>')
+
+  def test_superproject_with_defalut_remote(self):
+    """Check superproject settings."""
+    manifest = self.getXmlManifest("""
+<manifest>
+  <remote name="default-remote" fetch="http://localhost" />
+  <default remote="default-remote" revision="refs/heads/main" />
+  <superproject name="superproject" remote="default-remote"/>
+</manifest>
+""")
+    self.assertEqual(manifest.superproject['name'], 'superproject')
+    self.assertEqual(manifest.superproject['remote'].name, 'default-remote')
+    self.assertEqual(
+        manifest.ToXml().toxml(),
+        '<?xml version="1.0" ?><manifest>' +
+        '<remote name="default-remote" fetch="http://localhost"/>' +
+        '<default remote="default-remote" revision="refs/heads/main"/>' +
+        '<superproject name="superproject"/>' +
+        '</manifest>')
+
   def test_project_group(self):
     """Check project group settings."""
     manifest = self.getXmlManifest("""