• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#
2# Copyright (C) 2023 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the 'License');
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#      http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an 'AS IS' BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#
16"""Manifest discovery and parsing.
17
18The repo manifest format is documented at
19https://gerrit.googlesource.com/git-repo/+/master/docs/manifest-format.md. This module
20doesn't implement the full spec, since we only need a few properties.
21"""
22from __future__ import annotations
23
24from dataclasses import dataclass
25from pathlib import Path
26from xml.etree import ElementTree
27
28
29def find_manifest_xml_for_tree(root: Path) -> Path:
30    """Returns the path to the manifest XML file for the tree."""
31    repo_path = root / ".repo/manifests/default.xml"
32    if repo_path.exists():
33        return repo_path
34    raise FileNotFoundError(f"Could not find manifest at {repo_path}")
35
36
37@dataclass(frozen=True)
38class Project:
39    """Data for a manifest <project /> field.
40
41    https://gerrit.googlesource.com/git-repo/+/master/docs/manifest-format.md#element-project
42    """
43
44    path: str
45    remote: str
46    revision: str
47
48    @staticmethod
49    def from_xml_node(
50        node: ElementTree.Element, default_remote: str, default_revision: str
51    ) -> Project:
52        """Parses a Project from the given XML node."""
53        try:
54            # Path is optional, defaults to project name per manifest spec
55            path = x if (x := node.attrib.get("path")) is not None else node.attrib["name"]
56        except KeyError as ex:
57            raise RuntimeError(
58                f"<project /> element missing required name attribute: {node}"
59            ) from ex
60
61        return Project(
62            path,
63            node.attrib.get("remote", default_remote),
64            node.attrib.get("revision", default_revision),
65        )
66
67
68class ManifestParser:  # pylint: disable=too-few-public-methods
69    """Parser for the repo manifest.xml."""
70
71    def __init__(self, xml_path: Path) -> None:
72        self.xml_path = xml_path
73
74    def parse(self) -> Manifest:
75        """Parses the manifest.xml file and returns a Manifest."""
76        root = ElementTree.parse(self.xml_path)
77        defaults = root.findall("./default")
78        if len(defaults) != 1:
79            raise RuntimeError(
80                f"Expected exactly one <default /> element, found {len(defaults)}"
81            )
82        default_node = defaults[0]
83        try:
84            default_revision = default_node.attrib["revision"]
85            default_remote = default_node.attrib["remote"]
86        except KeyError as ex:
87            raise RuntimeError("<default /> element missing required attribute") from ex
88
89        return Manifest(
90            self.xml_path,
91            [
92                Project.from_xml_node(p, default_remote, default_revision)
93                for p in root.findall("./project")
94            ],
95        )
96
97
98class Manifest:
99    """The manifest data for a repo tree.
100
101    https://gerrit.googlesource.com/git-repo/+/master/docs/manifest-format.md
102    """
103
104    def __init__(self, path: Path, projects: list[Project]) -> None:
105        self.path = path
106        self.projects_by_path = {p.path: p for p in projects}
107
108    @staticmethod
109    def for_tree(root: Path) -> Manifest:
110        """Constructs a Manifest for the tree at `root`."""
111        return ManifestParser(find_manifest_xml_for_tree(root)).parse()
112
113    def project_with_path(self, path: str) -> Project:
114        """Returns the Project with the given path, or raises KeyError."""
115        return self.projects_by_path[path]
116