manifest_xml: initial support for <contactinfo>

It will be used to let manifest authors self-register contact info.
This element can be repeated, and any later entries will clobber
earlier ones. This would allow manifest authors who extend
manifests to specify their own contact info.

It would have 1 required attribute: bugurl.
"bugurl" specifies the URL to file a bug against the manifest owner.

<contactinfo bugurl="bug-url"/>

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: [google internal] b/186220520.
Change-Id: I47e765ba2dab5cdf850191129f4d4cd6b803f451
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/305203
Tested-by: Raman Tenneti <rtenneti@google.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
diff --git a/docs/manifest-format.md b/docs/manifest-format.md
index da83d0d..0752a8c 100644
--- a/docs/manifest-format.md
+++ b/docs/manifest-format.md
@@ -31,6 +31,7 @@
                       extend-project*,
                       repo-hooks?,
                       superproject?,
+                      contactinfo?,
                       include*)>
 
   <!ELEMENT notice (#PCDATA)>
@@ -100,10 +101,13 @@
   <!ATTLIST repo-hooks in-project CDATA #REQUIRED>
   <!ATTLIST repo-hooks enabled-list CDATA #REQUIRED>
 
-  <!ELEMENT superproject (EMPTY)>
+  <!ELEMENT superproject EMPTY>
   <!ATTLIST superproject name    CDATA #REQUIRED>
   <!ATTLIST superproject remote  IDREF #IMPLIED>
 
+  <!ELEMENT contactinfo EMPTY>
+  <!ATTLIST contactinfo bugurl  CDATA #REQUIRED>
+
   <!ELEMENT include EMPTY>
   <!ATTLIST include name   CDATA #REQUIRED>
   <!ATTLIST include groups CDATA #IMPLIED>
@@ -405,7 +409,7 @@
 ### Element superproject
 
 ***
- *Note*: This is currently a WIP.
+*Note*: This is currently a WIP.
 ***
 
 NB: See the [git superprojects documentation](
@@ -424,6 +428,19 @@
 Attribute `remote`: Name of a previously defined remote element.
 If not supplied the remote given by the default element is used.
 
+### Element contactinfo
+
+***
+*Note*: This is currently a WIP.
+***
+
+This element is used to let manifest authors self-register contact info.
+It has "bugurl" as a required atrribute. This element can be repeated,
+and any later entries will clobber earlier ones. This would allow manifest
+authors who extend manifests to specify their own contact info.
+
+Attribute `bugurl`: The URL to file a bug against the manifest owner.
+
 ### Element include
 
 This element provides the capability of including another manifest
diff --git a/manifest_xml.py b/manifest_xml.py
index 73556a5..e1d630b 100644
--- a/manifest_xml.py
+++ b/manifest_xml.py
@@ -479,6 +479,12 @@
         e.setAttribute('remote', remoteName)
       root.appendChild(e)
 
+    if self._contactinfo:
+      root.appendChild(doc.createTextNode(''))
+      e = doc.createElement('contactinfo')
+      e.setAttribute('bugurl', self._contactinfo['bugurl'])
+      root.appendChild(e)
+
     return doc
 
   def ToDict(self, **kwargs):
@@ -490,6 +496,7 @@
         'manifest-server',
         'repo-hooks',
         'superproject',
+        'contactinfo',
     }
     # Elements that may be repeated.
     MULTI_ELEMENTS = {
@@ -566,6 +573,11 @@
     return self._superproject
 
   @property
+  def contactinfo(self):
+    self._Load()
+    return self._contactinfo
+
+  @property
   def notice(self):
     self._Load()
     return self._notice
@@ -634,6 +646,7 @@
     self._default = None
     self._repo_hooks_project = None
     self._superproject = {}
+    self._contactinfo = {}
     self._notice = None
     self.branch = None
     self._manifest_server = None
@@ -876,6 +889,10 @@
           raise ManifestParseError("no remote for superproject %s within %s" %
                                    (name, self.manifestFile))
         self._superproject['remote'] = remote.ToRemoteSpec(name)
+      if node.nodeName == 'contactinfo':
+        bugurl = self._reqatt(node, 'bugurl')
+        # This element can be repeated, later entries will clobber earlier ones.
+        self._contactinfo['bugurl'] = bugurl
       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 e78d85c..bfdf366 100644
--- a/tests/test_manifest_xml.py
+++ b/tests/test_manifest_xml.py
@@ -255,10 +255,10 @@
     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"/>' +
+        '<?xml version="1.0" ?><manifest>'
+        '<remote name="test-remote" fetch="http://localhost"/>'
+        '<default remote="test-remote" revision="refs/heads/main"/>'
+        '<superproject name="superproject"/>'
         '</manifest>')
 
 
@@ -409,10 +409,10 @@
     project.SetRevisionId('ABCDEF')
     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"/>' +
-        '<project name="test-name" revision="ABCDEF"/>' +
+        '<?xml version="1.0" ?><manifest>'
+        '<remote name="default-remote" fetch="http://localhost"/>'
+        '<default remote="default-remote" revision="refs/heads/main"/>'
+        '<project name="test-name" revision="ABCDEF"/>'
         '</manifest>')
 
   def test_trailing_slash(self):
@@ -517,10 +517,10 @@
     self.assertEqual(manifest.superproject['remote'].url, 'http://localhost/superproject')
     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"/>' +
+        '<?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_remote(self):
@@ -538,11 +538,11 @@
     self.assertEqual(manifest.superproject['remote'].url, 'http://localhost/platform/superproject')
     self.assertEqual(
         manifest.ToXml().toxml(),
-        '<?xml version="1.0" ?><manifest>' +
-        '<remote name="default-remote" fetch="http://localhost"/>' +
-        '<remote name="superproject-remote" fetch="http://localhost"/>' +
-        '<default remote="default-remote" revision="refs/heads/main"/>' +
-        '<superproject name="platform/superproject" remote="superproject-remote"/>' +
+        '<?xml version="1.0" ?><manifest>'
+        '<remote name="default-remote" fetch="http://localhost"/>'
+        '<remote name="superproject-remote" fetch="http://localhost"/>'
+        '<default remote="default-remote" revision="refs/heads/main"/>'
+        '<superproject name="platform/superproject" remote="superproject-remote"/>'
         '</manifest>')
 
   def test_defalut_remote(self):
@@ -558,8 +558,25 @@
     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"/>' +
+        '<?xml version="1.0" ?><manifest>'
+        '<remote name="default-remote" fetch="http://localhost"/>'
+        '<default remote="default-remote" revision="refs/heads/main"/>'
+        '<superproject name="superproject"/>'
         '</manifest>')
+
+
+class ContactinfoElementTests(ManifestParseTestCase):
+  """Tests for <contactinfo>."""
+
+  def test_contactinfo(self):
+    """Check contactinfo settings."""
+    bugurl = 'http://localhost/contactinfo'
+    manifest = self.getXmlManifest(f"""
+<manifest>
+  <contactinfo bugurl="{bugurl}"/>
+</manifest>
+""")
+    self.assertEqual(manifest.contactinfo['bugurl'], bugurl)
+    self.assertEqual(
+        manifest.ToXml().toxml(),
+        f"""<?xml version="1.0" ?><manifest><contactinfo bugurl="{bugurl}"/></manifest>""")