# # Copyright (C) 2023 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the 'License'); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an 'AS IS' BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # """Manifest discovery and parsing. The repo manifest format is documented at https://gerrit.googlesource.com/git-repo/+/master/docs/manifest-format.md. This module doesn't implement the full spec, since we only need a few properties. """ from __future__ import annotations from dataclasses import dataclass from pathlib import Path from xml.etree import ElementTree def find_manifest_xml_for_tree(root: Path) -> Path: """Returns the path to the manifest XML file for the tree.""" repo_path = root / ".repo/manifests/default.xml" if repo_path.exists(): return repo_path raise FileNotFoundError(f"Could not find manifest at {repo_path}") @dataclass(frozen=True) class Project: """Data for a manifest field. https://gerrit.googlesource.com/git-repo/+/master/docs/manifest-format.md#element-project """ path: str remote: str revision: str @staticmethod def from_xml_node( node: ElementTree.Element, default_remote: str, default_revision: str ) -> Project: """Parses a Project from the given XML node.""" try: # Path is optional, defaults to project name per manifest spec path = x if (x := node.attrib.get("path")) is not None else node.attrib["name"] except KeyError as ex: raise RuntimeError( f" element missing required name attribute: {node}" ) from ex return Project( path, node.attrib.get("remote", default_remote), node.attrib.get("revision", default_revision), ) class ManifestParser: # pylint: disable=too-few-public-methods """Parser for the repo manifest.xml.""" def __init__(self, xml_path: Path) -> None: self.xml_path = xml_path def parse(self) -> Manifest: """Parses the manifest.xml file and returns a Manifest.""" root = ElementTree.parse(self.xml_path) defaults = root.findall("./default") if len(defaults) != 1: raise RuntimeError( f"Expected exactly one element, found {len(defaults)}" ) default_node = defaults[0] try: default_revision = default_node.attrib["revision"] default_remote = default_node.attrib["remote"] except KeyError as ex: raise RuntimeError(" element missing required attribute") from ex return Manifest( self.xml_path, [ Project.from_xml_node(p, default_remote, default_revision) for p in root.findall("./project") ], ) class Manifest: """The manifest data for a repo tree. https://gerrit.googlesource.com/git-repo/+/master/docs/manifest-format.md """ def __init__(self, path: Path, projects: list[Project]) -> None: self.path = path self.projects_by_path = {p.path: p for p in projects} @staticmethod def for_tree(root: Path) -> Manifest: """Constructs a Manifest for the tree at `root`.""" return ManifestParser(find_manifest_xml_for_tree(root)).parse() def project_with_path(self, path: str) -> Project: """Returns the Project with the given path, or raises KeyError.""" return self.projects_by_path[path]