Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(84)

Unified Diff: recipe_engine/third_party/setuptools/svn_utils.py

Issue 1344583003: Recipe package system. (Closed) Base URL: git@github.com:luci/recipes-py.git@master
Patch Set: Recompiled proto Created 5 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: recipe_engine/third_party/setuptools/svn_utils.py
diff --git a/recipe_engine/third_party/setuptools/svn_utils.py b/recipe_engine/third_party/setuptools/svn_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..6502fc98ea944b07daf65893c72f158759e9ce45
--- /dev/null
+++ b/recipe_engine/third_party/setuptools/svn_utils.py
@@ -0,0 +1,585 @@
+from __future__ import absolute_import
+
+import os
+import re
+import sys
+from distutils import log
+import xml.dom.pulldom
+import shlex
+import locale
+import codecs
+import unicodedata
+import warnings
+from setuptools.compat import unicode, PY2
+from setuptools.py31compat import TemporaryDirectory
+from xml.sax.saxutils import unescape
+
+try:
+ import urlparse
+except ImportError:
+ import urllib.parse as urlparse
+
+from subprocess import Popen as _Popen, PIPE as _PIPE
+
+#NOTE: Use of the command line options require SVN 1.3 or newer (December 2005)
+# and SVN 1.3 hasn't been supported by the developers since mid 2008.
+
+#subprocess is called several times with shell=(sys.platform=='win32')
+#see the follow for more information:
+# http://bugs.python.org/issue8557
+# http://stackoverflow.com/questions/5658622/
+# python-subprocess-popen-environment-path
+
+def _run_command(args, stdout=_PIPE, stderr=_PIPE, encoding=None, stream=0):
+ #regarding the shell argument, see: http://bugs.python.org/issue8557
+ try:
+ proc = _Popen(args, stdout=stdout, stderr=stderr,
+ shell=(sys.platform == 'win32'))
+
+ data = proc.communicate()[stream]
+ except OSError:
+ return 1, ''
+
+ #doubled checked and
+ data = decode_as_string(data, encoding)
+
+ #communciate calls wait()
+ return proc.returncode, data
+
+
+def _get_entry_schedule(entry):
+ schedule = entry.getElementsByTagName('schedule')[0]
+ return "".join([t.nodeValue
+ for t in schedule.childNodes
+ if t.nodeType == t.TEXT_NODE])
+
+
+def _get_target_property(target):
+ property_text = target.getElementsByTagName('property')[0]
+ return "".join([t.nodeValue
+ for t in property_text.childNodes
+ if t.nodeType == t.TEXT_NODE])
+
+
+def _get_xml_data(decoded_str):
+ if PY2:
+ #old versions want an encoded string
+ data = decoded_str.encode('utf-8')
+ else:
+ data = decoded_str
+ return data
+
+
+def joinpath(prefix, *suffix):
+ if not prefix or prefix == '.':
+ return os.path.join(*suffix)
+ return os.path.join(prefix, *suffix)
+
+def determine_console_encoding():
+ try:
+ #try for the preferred encoding
+ encoding = locale.getpreferredencoding()
+
+ #see if the locale.getdefaultlocale returns null
+ #some versions of python\platforms return US-ASCII
+ #when it cannot determine an encoding
+ if not encoding or encoding == "US-ASCII":
+ encoding = locale.getdefaultlocale()[1]
+
+ if encoding:
+ codecs.lookup(encoding) # make sure a lookup error is not made
+
+ except (locale.Error, LookupError):
+ encoding = None
+
+ is_osx = sys.platform == "darwin"
+ if not encoding:
+ return ["US-ASCII", "utf-8"][is_osx]
+ elif encoding.startswith("mac-") and is_osx:
+ #certain versions of python would return mac-roman as default
+ #OSX as a left over of earlier mac versions.
+ return "utf-8"
+ else:
+ return encoding
+
+_console_encoding = determine_console_encoding()
+
+def decode_as_string(text, encoding=None):
+ """
+ Decode the console or file output explicitly using getpreferredencoding.
+ The text paraemeter should be a encoded string, if not no decode occurs
+ If no encoding is given, getpreferredencoding is used. If encoding is
+ specified, that is used instead. This would be needed for SVN --xml
+ output. Unicode is explicitly put in composed NFC form.
+
+ --xml should be UTF-8 (SVN Issue 2938) the discussion on the Subversion
+ DEV List from 2007 seems to indicate the same.
+ """
+ #text should be a byte string
+
+ if encoding is None:
+ encoding = _console_encoding
+
+ if not isinstance(text, unicode):
+ text = text.decode(encoding)
+
+ text = unicodedata.normalize('NFC', text)
+
+ return text
+
+
+def parse_dir_entries(decoded_str):
+ '''Parse the entries from a recursive info xml'''
+ doc = xml.dom.pulldom.parseString(_get_xml_data(decoded_str))
+ entries = list()
+
+ for event, node in doc:
+ if event == 'START_ELEMENT' and node.nodeName == 'entry':
+ doc.expandNode(node)
+ if not _get_entry_schedule(node).startswith('delete'):
+ entries.append((node.getAttribute('path'),
+ node.getAttribute('kind')))
+
+ return entries[1:] # do not want the root directory
+
+
+def parse_externals_xml(decoded_str, prefix=''):
+ '''Parse a propget svn:externals xml'''
+ prefix = os.path.normpath(prefix)
+ prefix = os.path.normcase(prefix)
+
+ doc = xml.dom.pulldom.parseString(_get_xml_data(decoded_str))
+ externals = list()
+
+ for event, node in doc:
+ if event == 'START_ELEMENT' and node.nodeName == 'target':
+ doc.expandNode(node)
+ path = os.path.normpath(node.getAttribute('path'))
+
+ if os.path.normcase(path).startswith(prefix):
+ path = path[len(prefix)+1:]
+
+ data = _get_target_property(node)
+ #data should be decoded already
+ for external in parse_external_prop(data):
+ externals.append(joinpath(path, external))
+
+ return externals # do not want the root directory
+
+
+def parse_external_prop(lines):
+ """
+ Parse the value of a retrieved svn:externals entry.
+
+ possible token setups (with quotng and backscaping in laters versions)
+ URL[@#] EXT_FOLDERNAME
+ [-r#] URL EXT_FOLDERNAME
+ EXT_FOLDERNAME [-r#] URL
+ """
+ externals = []
+ for line in lines.splitlines():
+ line = line.lstrip() # there might be a "\ "
+ if not line:
+ continue
+
+ if PY2:
+ #shlex handles NULLs just fine and shlex in 2.7 tries to encode
+ #as ascii automatiically
+ line = line.encode('utf-8')
+ line = shlex.split(line)
+ if PY2:
+ line = [x.decode('utf-8') for x in line]
+
+ #EXT_FOLDERNAME is either the first or last depending on where
+ #the URL falls
+ if urlparse.urlsplit(line[-1])[0]:
+ external = line[0]
+ else:
+ external = line[-1]
+
+ external = decode_as_string(external, encoding="utf-8")
+ externals.append(os.path.normpath(external))
+
+ return externals
+
+
+def parse_prop_file(filename, key):
+ found = False
+ f = open(filename, 'rt')
+ data = ''
+ try:
+ for line in iter(f.readline, ''): # can't use direct iter!
+ parts = line.split()
+ if len(parts) == 2:
+ kind, length = parts
+ data = f.read(int(length))
+ if kind == 'K' and data == key:
+ found = True
+ elif kind == 'V' and found:
+ break
+ finally:
+ f.close()
+
+ return data
+
+
+class SvnInfo(object):
+ '''
+ Generic svn_info object. No has little knowledge of how to extract
+ information. Use cls.load to instatiate according svn version.
+
+ Paths are not filesystem encoded.
+ '''
+
+ @staticmethod
+ def get_svn_version():
+ # Temp config directory should be enough to check for repository
+ # This is needed because .svn always creates .subversion and
+ # some operating systems do not handle dot directory correctly.
+ # Real queries in real svn repos with be concerned with it creation
+ with TemporaryDirectory() as tempdir:
+ code, data = _run_command(['svn',
+ '--config-dir', tempdir,
+ '--version',
+ '--quiet'])
+
+ if code == 0 and data:
+ return data.strip()
+ else:
+ return ''
+
+ #svnversion return values (previous implementations return max revision)
+ # 4123:4168 mixed revision working copy
+ # 4168M modified working copy
+ # 4123S switched working copy
+ # 4123:4168MS mixed revision, modified, switched working copy
+ revision_re = re.compile(r'(?:([\-0-9]+):)?(\d+)([a-z]*)\s*$', re.I)
+
+ @classmethod
+ def load(cls, dirname=''):
+ normdir = os.path.normpath(dirname)
+
+ # Temp config directory should be enough to check for repository
+ # This is needed because .svn always creates .subversion and
+ # some operating systems do not handle dot directory correctly.
+ # Real queries in real svn repos with be concerned with it creation
+ with TemporaryDirectory() as tempdir:
+ code, data = _run_command(['svn',
+ '--config-dir', tempdir,
+ 'info', normdir])
+
+ # Must check for some contents, as some use empty directories
+ # in testcases, however only enteries is needed also the info
+ # command above MUST have worked
+ svn_dir = os.path.join(normdir, '.svn')
+ is_svn_wd = (not code or
+ os.path.isfile(os.path.join(svn_dir, 'entries')))
+
+ svn_version = tuple(cls.get_svn_version().split('.'))
+
+ try:
+ base_svn_version = tuple(int(x) for x in svn_version[:2])
+ except ValueError:
+ base_svn_version = tuple()
+
+ if not is_svn_wd:
+ #return an instance of this NO-OP class
+ return SvnInfo(dirname)
+
+ if code or not base_svn_version or base_svn_version < (1, 3):
+ warnings.warn(("No SVN 1.3+ command found: falling back "
+ "on pre 1.7 .svn parsing"), DeprecationWarning)
+ return SvnFileInfo(dirname)
+
+ if base_svn_version < (1, 5):
+ return Svn13Info(dirname)
+
+ return Svn15Info(dirname)
+
+ def __init__(self, path=''):
+ self.path = path
+ self._entries = None
+ self._externals = None
+
+ def get_revision(self):
+ 'Retrieve the directory revision information using svnversion'
+ code, data = _run_command(['svnversion', '-c', self.path])
+ if code:
+ log.warn("svnversion failed")
+ return 0
+
+ parsed = self.revision_re.match(data)
+ if parsed:
+ return int(parsed.group(2))
+ else:
+ return 0
+
+ @property
+ def entries(self):
+ if self._entries is None:
+ self._entries = self.get_entries()
+ return self._entries
+
+ @property
+ def externals(self):
+ if self._externals is None:
+ self._externals = self.get_externals()
+ return self._externals
+
+ def iter_externals(self):
+ '''
+ Iterate over the svn:external references in the repository path.
+ '''
+ for item in self.externals:
+ yield item
+
+ def iter_files(self):
+ '''
+ Iterate over the non-deleted file entries in the repository path
+ '''
+ for item, kind in self.entries:
+ if kind.lower() == 'file':
+ yield item
+
+ def iter_dirs(self, include_root=True):
+ '''
+ Iterate over the non-deleted file entries in the repository path
+ '''
+ if include_root:
+ yield self.path
+ for item, kind in self.entries:
+ if kind.lower() == 'dir':
+ yield item
+
+ def get_entries(self):
+ return []
+
+ def get_externals(self):
+ return []
+
+
+class Svn13Info(SvnInfo):
+ def get_entries(self):
+ code, data = _run_command(['svn', 'info', '-R', '--xml', self.path],
+ encoding="utf-8")
+
+ if code:
+ log.debug("svn info failed")
+ return []
+
+ return parse_dir_entries(data)
+
+ def get_externals(self):
+ #Previous to 1.5 --xml was not supported for svn propget and the -R
+ #output format breaks the shlex compatible semantics.
+ cmd = ['svn', 'propget', 'svn:externals']
+ result = []
+ for folder in self.iter_dirs():
+ code, lines = _run_command(cmd + [folder], encoding="utf-8")
+ if code != 0:
+ log.warn("svn propget failed")
+ return []
+ #lines should a str
+ for external in parse_external_prop(lines):
+ if folder:
+ external = os.path.join(folder, external)
+ result.append(os.path.normpath(external))
+
+ return result
+
+
+class Svn15Info(Svn13Info):
+ def get_externals(self):
+ cmd = ['svn', 'propget', 'svn:externals', self.path, '-R', '--xml']
+ code, lines = _run_command(cmd, encoding="utf-8")
+ if code:
+ log.debug("svn propget failed")
+ return []
+ return parse_externals_xml(lines, prefix=os.path.abspath(self.path))
+
+
+class SvnFileInfo(SvnInfo):
+
+ def __init__(self, path=''):
+ super(SvnFileInfo, self).__init__(path)
+ self._directories = None
+ self._revision = None
+
+ def _walk_svn(self, base):
+ entry_file = joinpath(base, '.svn', 'entries')
+ if os.path.isfile(entry_file):
+ entries = SVNEntriesFile.load(base)
+ yield (base, False, entries.parse_revision())
+ for path in entries.get_undeleted_records():
+ path = decode_as_string(path)
+ path = joinpath(base, path)
+ if os.path.isfile(path):
+ yield (path, True, None)
+ elif os.path.isdir(path):
+ for item in self._walk_svn(path):
+ yield item
+
+ def _build_entries(self):
+ entries = list()
+
+ rev = 0
+ for path, isfile, dir_rev in self._walk_svn(self.path):
+ if isfile:
+ entries.append((path, 'file'))
+ else:
+ entries.append((path, 'dir'))
+ rev = max(rev, dir_rev)
+
+ self._entries = entries
+ self._revision = rev
+
+ def get_entries(self):
+ if self._entries is None:
+ self._build_entries()
+ return self._entries
+
+ def get_revision(self):
+ if self._revision is None:
+ self._build_entries()
+ return self._revision
+
+ def get_externals(self):
+ prop_files = [['.svn', 'dir-prop-base'],
+ ['.svn', 'dir-props']]
+ externals = []
+
+ for dirname in self.iter_dirs():
+ prop_file = None
+ for rel_parts in prop_files:
+ filename = joinpath(dirname, *rel_parts)
+ if os.path.isfile(filename):
+ prop_file = filename
+
+ if prop_file is not None:
+ ext_prop = parse_prop_file(prop_file, 'svn:externals')
+ #ext_prop should be utf-8 coming from svn:externals
+ ext_prop = decode_as_string(ext_prop, encoding="utf-8")
+ externals.extend(parse_external_prop(ext_prop))
+
+ return externals
+
+
+def svn_finder(dirname=''):
+ #combined externals due to common interface
+ #combined externals and entries due to lack of dir_props in 1.7
+ info = SvnInfo.load(dirname)
+ for path in info.iter_files():
+ yield path
+
+ for path in info.iter_externals():
+ sub_info = SvnInfo.load(path)
+ for sub_path in sub_info.iter_files():
+ yield sub_path
+
+
+class SVNEntriesFile(object):
+ def __init__(self, data):
+ self.data = data
+
+ @classmethod
+ def load(class_, base):
+ filename = os.path.join(base, '.svn', 'entries')
+ f = open(filename)
+ try:
+ result = SVNEntriesFile.read(f)
+ finally:
+ f.close()
+ return result
+
+ @classmethod
+ def read(class_, fileobj):
+ data = fileobj.read()
+ is_xml = data.startswith('<?xml')
+ class_ = [SVNEntriesFileText, SVNEntriesFileXML][is_xml]
+ return class_(data)
+
+ def parse_revision(self):
+ all_revs = self.parse_revision_numbers() + [0]
+ return max(all_revs)
+
+
+class SVNEntriesFileText(SVNEntriesFile):
+ known_svn_versions = {
+ '1.4.x': 8,
+ '1.5.x': 9,
+ '1.6.x': 10,
+ }
+
+ def __get_cached_sections(self):
+ return self.sections
+
+ def get_sections(self):
+ SECTION_DIVIDER = '\f\n'
+ sections = self.data.split(SECTION_DIVIDER)
+ sections = [x for x in map(str.splitlines, sections)]
+ try:
+ # remove the SVN version number from the first line
+ svn_version = int(sections[0].pop(0))
+ if not svn_version in self.known_svn_versions.values():
+ log.warn("Unknown subversion verson %d", svn_version)
+ except ValueError:
+ return
+ self.sections = sections
+ self.get_sections = self.__get_cached_sections
+ return self.sections
+
+ def is_valid(self):
+ return bool(self.get_sections())
+
+ def get_url(self):
+ return self.get_sections()[0][4]
+
+ def parse_revision_numbers(self):
+ revision_line_number = 9
+ rev_numbers = [
+ int(section[revision_line_number])
+ for section in self.get_sections()
+ if (len(section) > revision_line_number
+ and section[revision_line_number])
+ ]
+ return rev_numbers
+
+ def get_undeleted_records(self):
+ undeleted = lambda s: s and s[0] and (len(s) < 6 or s[5] != 'delete')
+ result = [
+ section[0]
+ for section in self.get_sections()
+ if undeleted(section)
+ ]
+ return result
+
+
+class SVNEntriesFileXML(SVNEntriesFile):
+ def is_valid(self):
+ return True
+
+ def get_url(self):
+ "Get repository URL"
+ urlre = re.compile('url="([^"]+)"')
+ return urlre.search(self.data).group(1)
+
+ def parse_revision_numbers(self):
+ revre = re.compile(r'committed-rev="(\d+)"')
+ return [
+ int(m.group(1))
+ for m in revre.finditer(self.data)
+ ]
+
+ def get_undeleted_records(self):
+ entries_pattern = \
+ re.compile(r'name="([^"]+)"(?![^>]+deleted="true")', re.I)
+ results = [
+ unescape(match.group(1))
+ for match in entries_pattern.finditer(self.data)
+ ]
+ return results
+
+
+if __name__ == '__main__':
+ for name in svn_finder(sys.argv[1]):
+ print(name)
« no previous file with comments | « recipe_engine/third_party/setuptools/ssl_support.py ('k') | recipe_engine/third_party/setuptools/tests/__init__.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698