blob: 824b561d04ee5ba9dd702e518e4b3e0cf4f3a393 [file] [log] [blame]
# -*- coding: utf-8 -*-
# Copyright 2018 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Common functions for interacting with repo manifest XML files."""
from __future__ import print_function
from xml.etree import cElementTree as ElementTree
class Error(Exception):
"""An error related to repo manifests."""
class InvalidManifest(Error):
"""The manifest data has invalid values or structure."""
class UnsupportedFeature(Error):
"""The manifest data uses features that are not supported by this code."""
class Manifest(object):
"""Manifest represents the contents of a repo manifest XML file."""
# https://chromium.googlesource.com/external/repo/+/master/docs/manifest-format.txt
def __init__(self, etree):
"""Initialize Manifest.
Args:
etree: An ElementTree object representing a manifest XML file.
"""
self._etree = etree
self._ValidateTree()
# These __*state__ pickle protocol methods are intended for multiprocessing.
def __getstate__(self):
"""Return picklable state for this Manifest."""
return ElementTree.tostring(self._etree.getroot())
def __setstate__(self, state):
"""Set the state from pickle for this Manifest."""
root = ElementTree.fromstring(state)
self._etree = ElementTree.ElementTree(root)
def _ValidateTree(self):
"""Raise Error if self._etree is not a valid manifest tree."""
root = self._etree.getroot()
if root is None:
raise InvalidManifest('no data')
if root.tag != 'manifest':
raise InvalidManifest('expected root <manifest>, got <%s>' % root.tag)
if self._etree.find('./project/project') is not None:
raise UnsupportedFeature('nested <project>')
for unsupported_tag in ('extend-project', 'remove-project', 'include'):
if self._etree.find(unsupported_tag) is not None:
raise UnsupportedFeature('<%s>' % unsupported_tag)
@classmethod
def FromFile(cls, source):
"""Parse XML into a Manifest.
Args:
source: A string path or file object containing XML data.
Raises:
Error: If source couldn't be parsed into a Manifest.
"""
return cls(ElementTree.parse(source))
@classmethod
def FromString(cls, data):
"""Parse XML into a Manifest.
Args:
data: A string containing XML data.
Raises:
Error: If data couldn't be parsed into a Manifest.
"""
root = ElementTree.fromstring(data)
return cls(ElementTree.ElementTree(root))
def Write(self, dest):
"""Write the Manifest as XML.
Args:
dest: A string path or file object to write to.
Raises:
Error: If the Manifest contains invalid data.
"""
self._ValidateTree()
self._etree.write(dest, encoding='UTF-8', xml_declaration=True)
def Default(self):
"""Return this Manifest's Default or an empty Default."""
default_element = self._etree.find('default')
if default_element is None:
default_element = ElementTree.fromstring('<default/>')
return Default(self, default_element)
def Remotes(self):
"""Yield a Remote for each <remote> element in the manifest."""
for remote_element in self._etree.iterfind('remote'):
yield Remote(self, remote_element)
def GetRemote(self, name):
"""Return the Remote with the given name.
Raises:
ValueError: If no Remote has the given name.
"""
for remote in self.Remotes():
if remote.name == name:
return remote
raise ValueError('no remote named %s' % name)
def Projects(self):
"""Yield a Project for each <project> element in the manifest."""
for project_element in self._etree.iterfind('project'):
yield Project(self, project_element)
def GetUniqueProject(self, name):
"""Return the unique Project with the given name.
Raises:
ValueError: If there is not exactly one Project with the given name.
"""
projects = [x for x in self.Projects() if x.name == name]
if len(projects) != 1:
raise ValueError('%s projects named %s' % (len(projects), name))
return projects[0]
class _ManifestElement(object):
"""Subclasses of _ManifestElement wrap Manifest child XML elements."""
ATTRS = ()
TAG = None
def __init__(self, manifest, element):
tag = self.TAG or self.__class__.__name__.lower()
if element.tag != tag:
raise ValueError('expected a <%s> but got a <%s>' % (tag, element.tag))
self._manifest = manifest
self._el = element
# These __*state__ pickle protocol methods are intended for multiprocessing.
def __getstate__(self):
"""Return picklable state for this element."""
return (self._manifest, ElementTree.tostring(self._el))
def __setstate__(self, state):
"""Set the state from pickle for this element."""
self._manifest, xml_data = state
self._el = ElementTree.fromstring(xml_data)
def _XMLAttrName(self, name):
"""Return the XML attr name for the given Python attr name."""
if name not in self.ATTRS:
raise AttributeError('%r has no attribute %r' %
(self.__class__.__name__, name))
return name.replace('_', '-')
def __getattr__(self, name):
return self._el.get(self._XMLAttrName(name))
def __setattr__(self, name, value):
if name.startswith('_'):
super(_ManifestElement, self).__setattr__(name, value)
else:
self._el.set(self._XMLAttrName(name), value)
def __str__(self):
s = self.__class__.__name__
if 'name' in self.ATTRS:
s = '%s %r' % (s, self.name)
return '<%s>' % s
def __repr__(self):
return ElementTree.tostring(self._el)
class Remote(_ManifestElement):
"""Remote represents a <remote> element of a Manifest."""
ATTRS = ('name', 'alias', 'fetch', 'pushurl', 'review', 'revision')
def GitName(self):
"""Return the git remote name for this Remote."""
return self.alias or self.name
def PushURL(self):
"""Return the effective push URL of this Remote."""
return self.pushurl or self.fetch
class Default(_ManifestElement):
"""Default represents a <default> element of a Manifest."""
ATTRS = ('remote', 'revision', 'dest_branch', 'sync_j', 'sync_c', 'sync_s')
class Project(_ManifestElement):
"""Project represents a <project> element of a Manifest."""
ATTRS = ('name', 'path', 'remote', 'revision', 'dest_branch', 'groups',
'sync_c', 'sync_s', 'upstream', 'clone_depth', 'force_path')
def Path(self):
"""Return the effective filesystem path of this Project."""
return self.path or self.name
def RemoteName(self):
"""Return the effective remote name of this Project."""
return self.remote or self._manifest.Default().remote
def Remote(self):
"""Return the Remote for this Project."""
return self._manifest.GetRemote(self.RemoteName())
def Revision(self):
"""Return the effective revision of this Project."""
return (self.revision or
self.Remote().revision or
self._manifest.Default().revision)