# Copyright (C) 2020 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. """Compares two repo manifest xml files. Checks to see if the manifests contain same projects. And if those projects contain the same attributes, linkfile elements and copyfile elements. """ import argparse import sys import textwrap from typing import Set import xml.etree.ElementTree as ET import dataclasses from treble.split import xml_diff Element = ET.Element Change = xml_diff.Change ChangeMap = xml_diff.ChangeMap _SINGLE_NODE_ELEMENTS = ('default', 'manifest-server', 'repo-hooks', 'include') _INDENT = (' ' * 2) @dataclasses.dataclass class ProjectChanges: """A collection of changes between project elements. Attributes: attributes: A ChangeMap of attributes changes. Keyed by attribute name. linkfiles: A ChangeMap of linkfile elements changes. Keyed by dest. copyfiles: A ChangeMap of copyfile elements changes. Keyed by dest. """ attributes: ChangeMap = dataclasses.field(default_factory=ChangeMap) linkfiles: ChangeMap = dataclasses.field(default_factory=ChangeMap) copyfiles: ChangeMap = dataclasses.field(default_factory=ChangeMap) def __bool__(self): return bool(self.attributes) or bool(self.linkfiles) or bool(self.copyfiles) def __repr__(self): if not self: return 'No changes' ret_str = '' if self.attributes: ret_str += 'Attributes:\n' ret_str += textwrap.indent(str(self.attributes), _INDENT) if self.linkfiles: ret_str += 'Link Files:\n' ret_str += textwrap.indent(str(self.linkfiles), _INDENT) if self.copyfiles: ret_str += 'Copy Files:\n' ret_str += textwrap.indent(str(self.copyfiles), _INDENT) return ret_str @dataclasses.dataclass class ManifestChanges: """A collection of changes between manifests. Attributes: projects: A ChangeMap of changes to project elements. Keyed by project path. remotes: A ChangeMap of changes to remote elements. Keyed by remote name. other: A ChangeMap of changes to other elements. Keyed by element tag. """ projects: ChangeMap = dataclasses.field(default_factory=ChangeMap) remotes: ChangeMap = dataclasses.field(default_factory=ChangeMap) other: ChangeMap = dataclasses.field(default_factory=ChangeMap) def has_changes(self): return self.projects or self.remotes or self.other def __repr__(self): ret_str = 'Project Changes:\n' ret_str += (textwrap.indent(str(self.projects) + '\n', _INDENT) if self.projects else _INDENT + 'No changes found.\n\n') ret_str += 'Remote Changes:\n' ret_str += (textwrap.indent(str(self.remotes) + '\n', _INDENT) if self.remotes else _INDENT + 'No changes found.\n\n') ret_str += 'Other Changes:\n' ret_str += (textwrap.indent(str(self.other) + '\n', _INDENT) if self.other else _INDENT + 'No changes found.\n\n') return ret_str def subelement_file_changes(tag: str, p1: Element, p2: Element) -> ChangeMap: """Get the changes copyfile or linkfile elements between two project elements. Arguments: tag: The tag of the element. p1: the xml element for the base project. p2: the xml element for the new roject. Returns: A ChangeMap of copyfile or linkfile changes. Keyed by dest attribute. """ return xml_diff.compare_subelements( tag=tag, p1=p1, p2=p2, ignored_attrs=set(), key_fn=lambda x: x.get('dest'), diff_fn=xml_diff.attribute_changes) def project_changes(p1: Element, p2: Element, ignored_attrs: Set[str]) -> ProjectChanges: """Get the changes between two project elements. Arguments: p1: the xml element for the base project. p2: the xml element for the new project. ignored_attrs: a set of attribute names to ignore changes. Returns: A ProjectChanges object of the changes. """ return ProjectChanges( attributes=xml_diff.attribute_changes(p1, p2, ignored_attrs), linkfiles=subelement_file_changes('linkfile', p1, p2), copyfiles=subelement_file_changes('copyfile', p1, p2)) def compare_single_node_elements(manifest_e1: Element, manifest_e2: Element, ignored_attrs: Set[str]) -> ChangeMap: """Get the changes between single element nodes such as in a manifest. Arguments: manifest_e1: the xml element for the base manifest. manifest_e2: the xml element for the new manifest. ignored_attrs: a set of attribute names to ignore changes. Returns: A ChangeMap of changes. Keyed by elements tag name. """ changes = ChangeMap() for tag in _SINGLE_NODE_ELEMENTS: e1 = manifest_e1.find(tag) e2 = manifest_e2.find(tag) if e1 is None and e2 is None: continue elif e1 is None: changes.added[tag] = xml_diff.element_string(e2) elif e2 is None: changes.removed[tag] = xml_diff.element_string(e1) else: attr_changes = xml_diff.attribute_changes(e1, e2, ignored_attrs) if attr_changes: changes.modified[tag] = attr_changes return changes def compare_remote_elements(manifest_e1: Element, manifest_e2: Element, ignored_attrs: Set[str]) -> ChangeMap: """Get the changes to remote elements between two manifests. Arguments: manifest_e1: the xml element for the base manifest. manifest_e2: the xml element for the new manifest. ignored_attrs: a set of attribute names to ignore changes. Returns: A ChangeMap of changes to remote elements. Keyed by name attribute. """ return xml_diff.compare_subelements( tag='remote', p1=manifest_e1, p2=manifest_e2, ignored_attrs=ignored_attrs, key_fn=lambda x: x.get('name'), diff_fn=xml_diff.attribute_changes) def compare_project_elements(manifest_e1, manifest_e2, ignored_attrs: Set[str]) -> ChangeMap: """Get the changes to project elements between two manifests. Arguments: manifest_e1: the xml element for the base manifest. manifest_e2: the xml element for the new manifest. ignored_attrs: a set of attribute names to ignore changes. Returns: A ChangeMap of changes to project elements. Keyed by path/name attribute. """ # Ignore path attribute since it's already keyed on that value and avoid false # detection when path == name on one element and path == None on the other. project_ignored_attrs = ignored_attrs | set(['path']) return xml_diff.compare_subelements( tag='project', p1=manifest_e1, p2=manifest_e2, ignored_attrs=project_ignored_attrs, key_fn=lambda x: x.get('path', x.get('name')), diff_fn=project_changes) def compare_manifest_elements(manifest_e1, manifest_e2, ignored_attrs: Set[str]) -> ManifestChanges: """Get the changes between two manifests xml elements. Arguments: manifest_e1: the xml element for the base manifest. manifest_e2: the xml element for the new manifest. ignored_attrs: a set of attribute names to ignore changes. Returns: A ManifestChanges. """ return ManifestChanges( projects=compare_project_elements(manifest_e1, manifest_e2, ignored_attrs), remotes=compare_remote_elements(manifest_e1, manifest_e2, ignored_attrs), other=compare_single_node_elements(manifest_e1, manifest_e2, ignored_attrs)) def compare_manifest_files(manifest_a: str, manifest_b: str, ignored_attrs: Set[str]) -> ManifestChanges: """Get the changes between two manifests files. Arguments: manifest_a: Path to the base manifest xml file. manifest_b: Path to the manifest xml file to compare against. ignored_attrs: a set of attribute names to ignore changes. Returns: A ManifestChanges. """ e1 = ET.parse(manifest_a).getroot() e2 = ET.parse(manifest_b).getroot() return compare_manifest_elements( manifest_e1=e1, manifest_e2=e2, ignored_attrs=ignored_attrs) def main(): parser = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument( '--ignored_attributes', type=str, help='A comma separated list of attributes to ignore when comparing ' + 'project elements.') parser.add_argument('manifest_a', help='Path to the base manifest xml file.') parser.add_argument( 'manifest_b', help='Path to the manifest xml file to compare against.') args = parser.parse_args() ignored_attributes = set( args.ignored_attributes.split(',')) if args.ignored_attributes else set() changes = compare_manifest_files(args.manifest_a, args.manifest_b, ignored_attributes) print(changes) if changes: sys.exit(1) if __name__ == '__main__': main()