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