aboutsummaryrefslogtreecommitdiffstats
path: root/libbe
diff options
context:
space:
mode:
Diffstat (limited to 'libbe')
-rw-r--r--libbe/arch.py2
-rw-r--r--libbe/bug.py112
-rw-r--r--libbe/bugdir.py2
-rw-r--r--libbe/bzr.py2
-rw-r--r--libbe/cmdutil.py80
-rw-r--r--libbe/comment.py103
-rw-r--r--libbe/git.py2
-rw-r--r--libbe/subproc.py220
-rw-r--r--libbe/utility.py15
-rw-r--r--libbe/vcs.py82
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)