emacs: xml parsing rework.

Parsing XML within Emacs was too slow (over a minute). So we added a
utility Python script which parses the repo manifest faster. During
this rework, we also discovered that we need dest-branch in addition
to project name to identify a project path in a repo manifest.

BUG=chromium:1102923
TEST=Evaluated and called functions in IELM

Change-Id: Iecdb65d868e39b9d4695c31230f5d137f2e6e383
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/dev-util/+/2295702
Tested-by: Aaron Massey <aaronmassey@chromium.org>
Commit-Queue: Aaron Massey <aaronmassey@chromium.org>
Reviewed-by: Jack Rosenthal <jrosenth@chromium.org>
diff --git a/contrib/emacs/manifest_parser.py b/contrib/emacs/manifest_parser.py
new file mode 100755
index 0000000..f5eb452
--- /dev/null
+++ b/contrib/emacs/manifest_parser.py
@@ -0,0 +1,78 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Copyright 2020 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""This is a utility script for parsing repo manifest files used by gerrit.el"""
+
+import xml.parsers.expat as xml
+import sys
+import argparse
+import pathlib
+
+def parse_manifest_projects_to_lisp_alist(manifest_path):
+    """Parse manifest xml to Lisp alist.
+
+    Any project without a dest-branch attribute is skipped.
+
+    Args:
+        manifest_path: The path to a trusted repo manifest file to parse project elements.
+
+    Returns:
+        Lisp readable alist with elements of the form ((name . dest-branch) . path)
+
+    Raises:
+        ExpatError: An error occured when attempting to parse.
+    """
+
+    assoc_list_entries = []
+
+    def _project_elem_handler(name, attrs):
+        """XML element handler collecting project elements to form a Lisp alist.
+
+        Args:
+            name: The name of the handled xml element.
+            attrs: A dictionary of the handled xml element's attributes.
+        """
+        if name == 'project':
+            project_path = attrs['path']
+            project_name = attrs['name']
+            dest_branch = attrs['dest-branch'] if 'dest-branch' in attrs else None
+            if not dest_branch:
+                # We skip anything without a dest-branch
+                return
+            # We don't want the refs/heads/ prefix of dest-branch
+            dest_branch = dest_branch.replace('refs/heads/', '')
+
+            key = '("{}" . "{}")'.format(project_name, dest_branch)
+            value = '"{}"'.format(project_path)
+
+            assoc_list_entries.append('({} . {})'.format(key, value))
+
+    p = xml.ParserCreate()
+    p.StartElementHandler = _project_elem_handler
+    with open(manifest_path, 'rb') as manifest_fd:
+        p.ParseFile(manifest_fd)
+        return '({})'.format(''.join(assoc_list_entries))
+
+
+def main(argv):
+    """main."""
+    arg_parser = argparse.ArgumentParser()
+    arg_parser.add_argument('repo_manifest_path',
+                            type=pathlib.Path,
+                            help='System path to repo manifest xml file.')
+    args = arg_parser.parse_args(argv)
+
+    try:
+        print(parse_manifest_projects_to_lisp_alist(args.repo_manifest_path))
+        return 0
+
+    except xml.ExpatError as err:
+        print('XML Parsing Error:', err)
+        return 1
+
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv[1:]))