| # |
| # 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 <project /> 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"<project /> 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 <default /> 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("<default /> 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] |