diff options
Diffstat (limited to 'libbe')
-rw-r--r-- | libbe/arch.py | 2 | ||||
-rw-r--r-- | libbe/bug.py | 112 | ||||
-rw-r--r-- | libbe/bugdir.py | 2 | ||||
-rw-r--r-- | libbe/bzr.py | 2 | ||||
-rw-r--r-- | libbe/cmdutil.py | 80 | ||||
-rw-r--r-- | libbe/comment.py | 103 | ||||
-rw-r--r-- | libbe/git.py | 2 | ||||
-rw-r--r-- | libbe/subproc.py | 220 | ||||
-rw-r--r-- | libbe/utility.py | 15 | ||||
-rw-r--r-- | libbe/vcs.py | 82 |
10 files changed, 491 insertions, 129 deletions
diff --git a/libbe/arch.py b/libbe/arch.py index 4687555..48129b5 100644 --- a/libbe/arch.py +++ b/libbe/arch.py @@ -45,7 +45,7 @@ def new(): return Arch() class Arch(vcs.VCS): - name = "Arch" + name = "arch" client = client versioned = True _archive_name = None diff --git a/libbe/bug.py b/libbe/bug.py index 48f8358..fecb9b7 100644 --- a/libbe/bug.py +++ b/libbe/bug.py @@ -25,6 +25,10 @@ import os.path import errno import time import types +try: # import core module, Python >= 2.5 + from xml.etree import ElementTree +except ImportError: # look for non-core module + from elementtree import ElementTree import xml.sax.saxutils import doctest @@ -271,40 +275,100 @@ class Bug(settings_object.SavedSettingsObject): return str(value) return value - def xml(self, show_comments=False): - if self.bugdir == None: - shortname = self.uuid - else: - shortname = self.bugdir.bug_shortname(self) + def xml(self, indent=0, shortname=None, show_comments=False): + if shortname == None: + if self.bugdir == None: + shortname = self.uuid + else: + shortname = self.bugdir.bug_shortname(self) if self.time == None: timestring = "" else: timestring = utility.time_to_str(self.time) - info = [("uuid", self.uuid), - ("short-name", shortname), - ("severity", self.severity), - ("status", self.status), - ("assigned", self.assigned), - ("target", self.target), - ("reporter", self.reporter), - ("creator", self.creator), - ("created", timestring), - ("summary", self.summary)] - ret = '<bug>\n' + info = [('uuid', self.uuid), + ('short-name', shortname), + ('severity', self.severity), + ('status', self.status), + ('assigned', self.assigned), + ('target', self.target), + ('reporter', self.reporter), + ('creator', self.creator), + ('created', timestring), + ('summary', self.summary)] + lines = ['<bug>'] for (k,v) in info: if v is not None: - ret += ' <%s>%s</%s>\n' % (k,xml.sax.saxutils.escape(v),k) + lines.append(' <%s>%s</%s>' % (k,xml.sax.saxutils.escape(v),k)) for estr in self.extra_strings: - ret += ' <extra-string>%s</extra-string>\n' % estr + lines.append(' <extra-string>%s</extra-string>\n' % estr) if show_comments == True: - comout = self.comment_root.xml_thread(auto_name_map=True, + comout = self.comment_root.xml_thread(indent=indent+2, + auto_name_map=True, bug_shortname=shortname) if len(comout) > 0: - ret += comout+'\n' - ret += '</bug>' - return ret + lines.append(comout) + lines.append('</bug>') + istring = ' '*indent + sep = '\n' + istring + return istring + sep.join(lines).rstrip('\n') + + def from_xml(self, xml_string, verbose=True): + """ + Note: If a bug uuid is given, set .alt_id to it's value. + >>> bugA = Bug(uuid="0123", summary="Need to test Bug.from_xml()") + >>> bugA.date = "Thu, 01 Jan 1970 00:00:00 +0000" + >>> bugA.creator = u'Fran\xe7ois' + >>> bugA.extra_strings += ['TAG: very helpful'] + >>> commA = bugA.comment_root.new_reply(body='comment A') + >>> commB = bugA.comment_root.new_reply(body='comment B') + >>> commC = commA.new_reply(body='comment C') + >>> xml = bugA.xml(shortname="bug-1") + >>> bugB = Bug() + >>> bugB.from_xml(xml, verbose=True) + >>> bugB.xml(shortname="bug-1") == xml + False + >>> bugB.uuid = bugB.alt_id + >>> bugB.xml(shortname="bug-1") == xml + True + """ + if type(xml_string) == types.UnicodeType: + xml_string = xml_string.strip().encode('unicode_escape') + bug = ElementTree.XML(xml_string) + if bug.tag != 'bug': + raise utility.InvalidXML( \ + 'bug', bug, 'root element must be <comment>') + tags=['uuid','short-name','severity','status','assigned','target', + 'reporter', 'creator', 'created', 'summary', 'extra-string', + 'comment'] + uuid = None + estrs = [] + for child in bug.getchildren(): + if child.tag == 'short-name': + pass + elif child.tag in tags: + if child.text == None or len(child.text) == 0: + text = settings_object.EMPTY + else: + text = xml.sax.saxutils.unescape(child.text) + text = text.decode('unicode_escape').strip() + if child.tag == "uuid": + uuid = text + continue # don't set the bug's uuid tag. + if child.tag == 'extra-string': + estrs.append(text) + continue # don't set the bug's extra_string yet. + else: + attr_name = child.tag.replace('-','_') + setattr(self, attr_name, text) + elif verbose == True: + print >> sys.stderr, "Ignoring unknown tag %s in %s" \ + % (child.tag, comment.tag) + if uuid not in [None, self.uuid]: + if not hasattr(self, 'alt_id') or self.alt_id == None: + self.alt_id = uuid + self.extra_strings = estrs def string(self, shortlist=False, show_comments=False): if self.bugdir == None: @@ -525,6 +589,7 @@ cmp_assigned = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "assigned") cmp_target = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "target") cmp_reporter = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "reporter") cmp_summary = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "summary") +cmp_extra_strings = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "extra_strings") # chronological rankings (newer < older) cmp_time = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "time", invert=True) @@ -547,7 +612,8 @@ def cmp_comments(bug_1, bug_2): DEFAULT_CMP_FULL_CMP_LIST = \ (cmp_status, cmp_severity, cmp_assigned, cmp_time, cmp_creator, - cmp_reporter, cmp_target, cmp_comments, cmp_summary, cmp_uuid) + cmp_reporter, cmp_target, cmp_comments, cmp_summary, cmp_uuid, + cmp_extra_strings) class BugCompoundComparator (object): def __init__(self, cmp_list=DEFAULT_CMP_FULL_CMP_LIST): diff --git a/libbe/bugdir.py b/libbe/bugdir.py index 5b942d3..675b744 100644 --- a/libbe/bugdir.py +++ b/libbe/bugdir.py @@ -213,7 +213,7 @@ that the Arch VCS backend *enforces* ids with this format.""", settings easy. Don't set this attribute. Set .vcs instead, and .vcs_name will be automatically adjusted.""", default="None", - allowed=["None", "Arch", "bzr", "darcs", "git", "hg"]) + allowed=["None"]+vcs.VCS_ORDER) def vcs_name(): return {} def _get_vcs(self, vcs_name=None): diff --git a/libbe/bzr.py b/libbe/bzr.py index 2cf1cba..281493d 100644 --- a/libbe/bzr.py +++ b/libbe/bzr.py @@ -90,7 +90,7 @@ class Bzr(vcs.VCS): if self._u_any_in_string(strings, error) == True: raise vcs.EmptyCommit() else: - raise vcs.CommandError(args, status, stdout="", stderr=error) + raise vcs.CommandError(args, status, stderr=error) revision = None revline = re.compile("Committed revision (.*)[.]") match = revline.search(error) diff --git a/libbe/cmdutil.py b/libbe/cmdutil.py index f1c8acd..e37750d 100644 --- a/libbe/cmdutil.py +++ b/libbe/cmdutil.py @@ -30,10 +30,10 @@ import sys import doctest import bugdir +import comment import plugin import encoding - class UserError(Exception): def __init__(self, msg): Exception.__init__(self, msg) @@ -76,11 +76,12 @@ def get_command(command_name): return cmd -def execute(cmd, args, manipulate_encodings=True): +def execute(cmd, args, manipulate_encodings=True, restrict_file_access=False): enc = encoding.get_encoding() cmd = get_command(cmd) ret = cmd.execute([a.decode(enc) for a in args], - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + restrict_file_access=restrict_file_access) if ret == None: ret = 0 return ret @@ -213,16 +214,85 @@ def underlined(instring): return "%s\n%s" % (instring, "="*len(instring)) -def bug_from_shortname(bdir, shortname): +def restrict_file_access(bugdir, path): + """ + Check that the file at path is inside bugdir.root. This is + important if you allow other users to execute becommands with your + username (e.g. if you're running be-handle-mail through your + ~/.procmailrc). If this check wasn't made, a user could e.g. + run + be commit -b ~/.ssh/id_rsa "Hack to expose ssh key" + which would expose your ssh key to anyone who could read the VCS + log. + """ + in_root = bugdir.vcs.path_in_root(path, bugdir.root) + if in_root == False: + raise UserError('file access restricted!\n %s not in %s' + % (path, bugdir.root)) + +def parse_id(id): + """ + Return (bug_id, comment_id) tuple. + Basically inverts Comment.comment_shortnames() + >>> parse_id('XYZ') + ('XYZ', None) + >>> parse_id('XYZ:123') + ('XYZ', ':123') + >>> parse_id('') + Traceback (most recent call last): + ... + UserError: invalid id ''. + >>> parse_id('::') + Traceback (most recent call last): + ... + UserError: invalid id '::'. + """ + if len(id) == 0: + raise UserError("invalid id '%s'." % id) + if id.count(':') > 1: + raise UserError("invalid id '%s'." % id) + elif id.count(':') == 1: + # Split shortname generated by Comment.comment_shortnames() + bug_id,comment_id = id.split(':') + comment_id = ':'+comment_id + else: + bug_id = id + comment_id = None + return (bug_id, comment_id) + +def bug_from_id(bdir, id): """ Exception translation for the command-line interface. + id can be either the bug shortname or the full uuid. """ try: - bug = bdir.bug_from_shortname(shortname) + bug = bdir.bug_from_shortname(id) except (bugdir.MultipleBugMatches, bugdir.NoBugMatches), e: raise UserError(e.message) return bug +def bug_comment_from_id(bdir, id): + """ + Return (bug,comment) tuple matching shortname. id can be either + the bug/comment shortname or the full uuid. If there is no + comment part to the id, the returned comment is the bug's + .comment_root. + """ + bug_id,comment_id = parse_id(id) + try: + bug = bdir.bug_from_shortname(bug_id) + except (bugdir.MultipleBugMatches, bugdir.NoBugMatches), e: + raise UserError(e.message) + if comment_id == None: + comm = bug.comment_root + else: + #bug.load_comments(load_full=False) + try: + comm = bug.comment_root.comment_from_shortname(comment_id) + except comment.InvalidShortname, e: + raise UserError(e.message) + return (bug, comm) + def _test(): import doctest import sys diff --git a/libbe/comment.py b/libbe/comment.py index 5f67878..5cc43c4 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -51,14 +51,6 @@ class InvalidShortname(KeyError): self.shortname = shortname self.shortnames = shortnames -class InvalidXML(ValueError): - def __init__(self, element, comment): - msg = "Invalid comment xml: %s\n %s\n" \ - % (comment, ElementTree.tostring(element)) - ValueError.__init__(self, msg) - self.element = element - self.comment = comment - class MissingReference(ValueError): def __init__(self, comment): msg = "Missing reference to %s" % (comment.in_reply_to) @@ -331,27 +323,29 @@ class Comment(Tree, settings_object.SavedSettingsObject): """ if shortname == None: shortname = self.uuid - if self.content_type.startswith("text/"): - body = (self.body or "").rstrip('\n') + if self.content_type.startswith('text/'): + body = (self.body or '').rstrip('\n') else: maintype,subtype = self.content_type.split('/',1) msg = email.mime.base.MIMEBase(maintype, subtype) - msg.set_payload(self.body or "") + msg.set_payload(self.body or '') email.encoders.encode_base64(msg) - body = base64.encodestring(self.body or "") - info = [("uuid", self.uuid), - ("alt-id", self.alt_id), - ("short-name", shortname), - ("in-reply-to", self.in_reply_to), - ("author", self._setting_attr_string("author")), - ("date", self.date), - ("content-type", self.content_type), - ("body", body)] - lines = ["<comment>"] + body = base64.encodestring(self.body or '') + info = [('uuid', self.uuid), + ('alt-id', self.alt_id), + ('short-name', shortname), + ('in-reply-to', self.in_reply_to), + ('author', self._setting_attr_string('author')), + ('date', self.date), + ('content-type', self.content_type), + ('body', body)] + lines = ['<comment>'] for (k,v) in info: if v != None: lines.append(' <%s>%s</%s>' % (k,xml.sax.saxutils.escape(v),k)) - lines.append("</comment>") + for estr in self.extra_strings: + lines.append(' <extra-string>%s</extra-string>\n' % estr) + lines.append('</comment>') istring = ' '*indent sep = '\n' + istring return istring + sep.join(lines).rstrip('\n') @@ -363,58 +357,61 @@ class Comment(Tree, settings_object.SavedSettingsObject): >>> commA = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n") >>> commA.uuid = "0123" >>> commA.date = "Thu, 01 Jan 1970 00:00:00 +0000" + >>> commA.author = u'Fran\xe7ois' + >>> commA.extra_strings += ['TAG: very helpful'] >>> xml = commA.xml(shortname="com-1") >>> commB = Comment() - >>> commB.from_xml(xml) - >>> attrs=['uuid','alt_id','in_reply_to','author','date','content_type','body'] - >>> for attr in attrs: # doctest: +ELLIPSIS - ... if getattr(commB, attr) != getattr(commA, attr): - ... estr = "Mismatch on %s: '%s' should be '%s'" - ... args = (attr, getattr(commB, attr), getattr(commA, attr)) - ... print estr % args - Mismatch on uuid: '...' should be '0123' - Mismatch on alt_id: '0123' should be 'None' - >>> print commB.alt_id - 0123 - >>> commA.author - >>> commB.author + >>> commB.from_xml(xml, verbose=True) + >>> commB.xml(shortname="com-1") == xml + False + >>> commB.uuid = commB.alt_id + >>> commB.alt_id = None + >>> commB.xml(shortname="com-1") == xml + True """ if type(xml_string) == types.UnicodeType: - xml_string = xml_string.strip().encode("unicode_escape") + xml_string = xml_string.strip().encode('unicode_escape') comment = ElementTree.XML(xml_string) - if comment.tag != "comment": - raise InvalidXML(comment, "root element must be <comment>") - tags=['uuid','alt-id','in-reply-to','author','date','content-type','body'] + if comment.tag != 'comment': + raise utility.InvalidXML( \ + 'comment', comment, 'root element must be <comment>') + tags=['uuid','alt-id','in-reply-to','author','date','content-type', + 'body','extra-string'] uuid = None body = None + estrs = [] for child in comment.getchildren(): - if child.tag == "short-name": + if child.tag == 'short-name': pass elif child.tag in tags: if child.text == None or len(child.text) == 0: text = settings_object.EMPTY else: text = xml.sax.saxutils.unescape(child.text) - text = unicode(text).decode("unicode_escape").strip() - if child.tag == "uuid": + text = text.decode('unicode_escape').strip() + if child.tag == 'uuid': uuid = text - continue # don't set the bug's uuid tag. - if child.tag == "body": + continue # don't set the comment's uuid tag. + if child.tag == 'body': body = text - continue # don't set the bug's body yet. + continue # don't set the comment's body yet. + if child.tag == 'extra-string': + estrs.append(text) + continue # don't set the comment's extra_string yet. else: attr_name = child.tag.replace('-','_') setattr(self, attr_name, text) elif verbose == True: - print >> sys.stderr, "Ignoring unknown tag %s in %s" \ + print >> sys.stderr, 'Ignoring unknown tag %s in %s' \ % (child.tag, comment.tag) if self.alt_id == None and uuid not in [None, self.uuid]: self.alt_id = uuid if body != None: - if self.content_type.startswith("text/"): - self.body = body+"\n" # restore trailing newline + if self.content_type.startswith('text/'): + self.body = body+'\n' # restore trailing newline else: self.body = base64.decodestring(body) + self.extra_strings = estrs def string(self, indent=0, shortname=None): """ @@ -644,6 +641,12 @@ class Comment(Tree, settings_object.SavedSettingsObject): bug-1:2 b bug-1:3 c bug-1:4 d + >>> for id,name in a.comment_shortnames(): + ... print id, name.uuid + :1 a + :2 b + :3 c + :4 d """ if bug_shortname == None: bug_shortname = "" @@ -726,12 +729,14 @@ cmp_author = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "autho cmp_in_reply_to = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "in_reply_to") cmp_content_type = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "content_type") cmp_body = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "body") +cmp_extra_strings = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "extra_strings") # chronological rankings (newer < older) cmp_time = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "time", invert=True) + DEFAULT_CMP_FULL_CMP_LIST = \ (cmp_time, cmp_author, cmp_content_type, cmp_body, cmp_in_reply_to, - cmp_uuid) + cmp_uuid, cmp_extra_strings) class CommentCompoundComparator (object): def __init__(self, cmp_list=DEFAULT_CMP_FULL_CMP_LIST): diff --git a/libbe/git.py b/libbe/git.py index cb4436a..55556de 100644 --- a/libbe/git.py +++ b/libbe/git.py @@ -134,7 +134,7 @@ class Git(vcs.VCS): if status == 128: if error.startswith("fatal: ambiguous argument 'HEAD': unknown "): return None - raise vcs.CommandError(args, status, stdout="", stderr=error) + raise vcs.CommandError(args, status, stderr=error) commits = output.splitlines() try: return commits[index] diff --git a/libbe/subproc.py b/libbe/subproc.py new file mode 100644 index 0000000..3e58271 --- /dev/null +++ b/libbe/subproc.py @@ -0,0 +1,220 @@ +# Copyright (C) 2009 W. Trevor King <wking@drexel.edu> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Functions for running external commands in subprocesses. +""" + +from subprocess import Popen, PIPE +import sys +import doctest + +from encoding import get_encoding + +_MSWINDOWS = sys.platform == 'win32' +_POSIX = not _MSWINDOWS + +if _POSIX == True: + import os + import select + +class CommandError(Exception): + def __init__(self, command, status, stdout=None, stderr=None): + strerror = ['Command failed (%d):\n %s\n' % (status, stderr), + 'while executing\n %s' % command] + Exception.__init__(self, '\n'.join(strerror)) + self.command = command + self.status = status + self.stdout = stdout + self.stderr = stderr + +def invoke(args, stdin=None, stdout=PIPE, stderr=PIPE, expect=(0,), + cwd=None, unicode_output=True, verbose=False, encoding=None): + """ + expect should be a tuple of allowed exit codes. cwd should be + the directory from which the command will be executed. When + unicode_output == True, convert stdout and stdin strings to + unicode before returing them. + """ + if cwd == None: + cwd = '.' + if verbose == True: + print >> sys.stderr, '%s$ %s' % (cwd, ' '.join(args)) + try : + if _POSIX: + q = Popen(args, stdin=PIPE, stdout=stdout, stderr=stderr, cwd=cwd) + else: + assert _MSWINDOWS==True, 'invalid platform' + # win32 don't have os.execvp() so have to run command in a shell + q = Popen(args, stdin=PIPE, stdout=stdout, stderr=stderr, + shell=True, cwd=cwd) + except OSError, e: + raise CommandError(args, status=e.args[0], stderr=e) + stdout,stderr = q.communicate(input=stdin) + status = q.wait() + if unicode_output == True: + if encoding == None: + encoding = get_encoding() + if stdout != None: + stdout = unicode(stdout, encoding) + if stderr != None: + stderr = unicode(stderr, encoding) + if verbose == True: + print >> sys.stderr, '%d\n%s%s' % (status, stdout, stderr) + if status not in expect: + raise CommandError(args, status, stdout, stderr) + return status, stdout, stderr + +class Pipe (object): + """ + Simple interface for executing POSIX-style pipes based on the + subprocess module. The only complication is the adaptation of + subprocess.Popen._comminucate to listen to the stderrs of all + processes involved in the pipe, as well as the terminal process' + stdout. There are two implementations of Pipe._communicate, one + for MS Windows, and one for POSIX systems. The MS Windows + implementation is currently untested. + + >>> p = Pipe([['find', '/etc/'], ['grep', '^/etc/ssh$']]) + >>> p.stdout + '/etc/ssh\\n' + >>> p.status + 1 + >>> p.statuses + [1, 0] + >>> p.stderrs # doctest: +ELLIPSIS + ["find: `...': Permission denied\\n...", ''] + """ + def __init__(self, cmds, stdin=None): + # spawn processes + self._procs = [] + for cmd in cmds: + if len(self._procs) != 0: + stdin = self._procs[-1].stdout + self._procs.append(Popen(cmd, stdin=stdin, stdout=PIPE, stderr=PIPE)) + + self.stdout,self.stderrs = self._communicate(input=None) + + # collect process statuses + self.statuses = [] + self.status = 0 + for proc in self._procs: + self.statuses.append(proc.wait()) + if self.statuses[-1] != 0: + self.status = self.statuses[-1] + + # Code excerpted from subprocess.Popen._communicate() + if _MSWINDOWS == True: + def _communicate(self, input=None): + assert input == None, 'stdin != None not yet supported' + # listen to each process' stderr + threads = [] + std_X_arrays = [] + for proc in self._procs: + stderr_array = [] + thread = Thread(target=proc._readerthread, + args=(proc.stderr, stderr_array)) + thread.setDaemon(True) + thread.start() + threads.append(thread) + std_X_arrays.append(stderr_array) + + # also listen to the last processes stdout + stdout_array = [] + thread = Thread(target=proc._readerthread, + args=(proc.stdout, stdout_array)) + thread.setDaemon(True) + thread.start() + threads.append(thread) + std_X_arrays.append(stdout_array) + + # join threads as they die + for thread in threads: + thread.join() + + # read output from reader threads + std_X_strings = [] + for std_X_array in std_X_arrays: + std_X_strings.append(std_X_array[0]) + + stdout = std_X_strings.pop(-1) + stderrs = std_X_strings + return (stdout, stderrs) + else: + assert _POSIX==True, 'invalid platform' + def _communicate(self, input=None): + read_set = [] + write_set = [] + read_arrays = [] + stdout = None # Return + stderr = None # Return + + if self._procs[0].stdin: + # Flush stdio buffer. This might block, if the user has + # been writing to .stdin in an uncontrolled fashion. + self._procs[0].stdin.flush() + if input: + write_set.append(self._procs[0].stdin) + else: + self._procs[0].stdin.close() + for proc in self._procs: + read_set.append(proc.stderr) + read_arrays.append([]) + read_set.append(self._procs[-1].stdout) + read_arrays.append([]) + + input_offset = 0 + while read_set or write_set: + try: + rlist, wlist, xlist = select.select(read_set, write_set, []) + except select.error, e: + if e.args[0] == errno.EINTR: + continue + raise + if self._procs[0].stdin in wlist: + # When select has indicated that the file is writable, + # we can write up to PIPE_BUF bytes without risk + # blocking. POSIX defines PIPE_BUF >= 512 + chunk = input[input_offset : input_offset + 512] + bytes_written = os.write(self.stdin.fileno(), chunk) + input_offset += bytes_written + if input_offset >= len(input): + self._procs[0].stdin.close() + write_set.remove(self._procs[0].stdin) + if self._procs[-1].stdout in rlist: + data = os.read(self._procs[-1].stdout.fileno(), 1024) + if data == '': + self._procs[-1].stdout.close() + read_set.remove(self._procs[-1].stdout) + read_arrays[-1].append(data) + for i,proc in enumerate(self._procs): + if proc.stderr in rlist: + data = os.read(proc.stderr.fileno(), 1024) + if data == '': + proc.stderr.close() + read_set.remove(proc.stderr) + read_arrays[i].append(data) + + # All data exchanged. Translate lists into strings. + read_strings = [] + for read_array in read_arrays: + read_strings.append(''.join(read_array)) + + stdout = read_strings.pop(-1) + stderrs = read_strings + return (stdout, stderrs) + +suite = doctest.DocTestSuite() diff --git a/libbe/utility.py b/libbe/utility.py index 4126913..7510b16 100644 --- a/libbe/utility.py +++ b/libbe/utility.py @@ -29,6 +29,21 @@ import time import types import doctest +class InvalidXML(ValueError): + """ + Invalid XML while parsing for a *.from_xml() method. + type - string identifying *, e.g. "bug", "comment", ... + element - ElementTree.Element instance which caused the error + error - string describing the error + """ + def __init__(self, type, element, error): + msg = 'Invalid %s xml: %s\n %s\n' \ + % (type, error, ElementTree.tostring(element)) + ValueError.__init__(self, msg) + self.type = type + self.element = element + self.error = error + def search_parent_directories(path, filename): """ Find the file (or directory) named filename in path or in any diff --git a/libbe/vcs.py b/libbe/vcs.py index 1ac5dd9..57a0245 100644 --- a/libbe/vcs.py +++ b/libbe/vcs.py @@ -25,7 +25,6 @@ subclassed by other Version Control System backends. The base class implements a "do not version" VCS. """ -from subprocess import Popen, PIPE import codecs import os import os.path @@ -38,16 +37,24 @@ import unittest import doctest from utility import Dir, search_parent_directories +from subproc import CommandError, invoke +from plugin import get_plugin +# List VCS modules in order of preference. +# Don't list this module, it is implicitly last. +VCS_ORDER = ['arch', 'bzr', 'darcs', 'git', 'hg'] + +def set_preferred_vcs(name): + global VCS_ORDER + assert name in VCS_ORDER, \ + 'unrecognized VCS %s not in\n %s' % (name, VCS_ORDER) + VCS_ORDER.remove(name) + VCS_ORDER.insert(0, name) def _get_matching_vcs(matchfn): """Return the first module for which matchfn(VCS_instance) is true""" - import arch - import bzr - import darcs - import git - import hg - for module in [arch, bzr, darcs, git, hg]: + for submodname in VCS_ORDER: + module = get_plugin('libbe', submodname) vcs = module.new() if matchfn(vcs) == True: return vcs @@ -67,15 +74,6 @@ def installed_vcs(): return _get_matching_vcs(lambda vcs: vcs.installed()) -class CommandError(Exception): - def __init__(self, command, status, stdout, stderr): - strerror = ["Command failed (%d):\n %s\n" % (status, stderr), - "while executing\n %s" % command] - Exception.__init__(self, "\n".join(strerror)) - self.command = command - self.status = status - self.stdout = stdout - self.stderr = stderr class SettingIDnotSupported(NotImplementedError): pass @@ -126,6 +124,10 @@ class VCS(object): self._duplicateDirname = None self.encoding = encoding self.version = self._get_version() + def __str__(self): + return "<%s %s>" % (self.__class__.__name__, id(self)) + def __repr__(self): + return str(self) def _vcs_version(self): """ Return the VCS version string. @@ -332,6 +334,10 @@ class VCS(object): """ Get the file as it was in a given revision. Revision==None specifies the current revision. + + allow_no_vcs==True allows direct access to files through + codecs.open() or open() if the vcs decides it can't handle the + given path. """ if not os.path.exists(path): raise NoSuchFile(path) @@ -339,7 +345,10 @@ class VCS(object): relpath = self._u_rel_path(path) contents = self._vcs_get_file_contents(relpath,revision,binary=binary) else: - f = codecs.open(path, "r", self.encoding) + if binary == True: + f = codecs.open(path, "r", self.encoding) + else: + f = open(path, "rb") contents = f.read() f.close() return contents @@ -457,37 +466,14 @@ class VCS(object): if list_string in string: return True return False - def _u_invoke(self, args, stdin=None, expect=(0,), cwd=None, - unicode_output=True): - """ - expect should be a tuple of allowed exit codes. cwd should be - the directory from which the command will be executed. When - unicode_output == True, convert stdout and stdin strings to - unicode before returing them. - """ - if cwd == None: - cwd = self.rootdir - if self.verboseInvoke == True: - print >> sys.stderr, "%s$ %s" % (cwd, " ".join(args)) - try : - if sys.platform != "win32": - q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, cwd=cwd) - else: - # win32 don't have os.execvp() so have to run command in a shell - q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, - shell=True, cwd=cwd) - except OSError, e : - raise CommandError(args, status=e.args[0], stdout="", stderr=e) - stdout,stderr = q.communicate(input=stdin) - status = q.wait() - if unicode_output == True: - stdout = unicode(stdout, self.encoding) - stderr = unicode(stderr, self.encoding) - if self.verboseInvoke == True: - print >> sys.stderr, "%d\n%s%s" % (status, stdout, stderr) - if status not in expect: - raise CommandError(args, status, stdout, stderr) - return status, stdout, stderr + def _u_invoke(self, *args, **kwargs): + if 'cwd' not in kwargs: + kwargs['cwd'] = self.rootdir + if 'verbose' not in kwargs: + kwargs['verbose'] = self.verboseInvoke + if 'encoding' not in kwargs: + kwargs['encoding'] = self.encoding + return invoke(*args, **kwargs) def _u_invoke_client(self, *args, **kwargs): cl_args = [self.client] cl_args.extend(args) |