aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorW. Trevor King <wking@drexel.edu>2009-11-21 15:06:10 -0500
committerW. Trevor King <wking@drexel.edu>2009-11-21 15:06:10 -0500
commitf3de7e1a6d07b5488fd3c9e01caba53216e612d2 (patch)
tree9ad63d3c5e960271eac24b13e94df741dca74f43
parent64cb5e5ec672cd357bc66a8480465e531db25f52 (diff)
parent614d4e40e148520ac511cbe0606bcbdcf24c8a08 (diff)
downloadbugseverywhere-f3de7e1a6d07b5488fd3c9e01caba53216e612d2.tar.gz
Merged mostly completed `be email-bugs'.
Highlights: * new be commands 'email-bugs' and 'import-xml' * standardized <be-xml> format for XML files. * new be-handle-mail interface '[be-bug:xml]' * restrict_file_access security patch * new subprocess handling submodule libbe.subproc * test.py adjusted to use an installed VCS for most tests. * assorted bugfixes Altered interfaces to the following be commands: * comment --xml tag gone, use import-xml. * show --xml xml format updated to <be-xml> format. Also adjusted be-mbox-to-xml and be-xml-to-mbox to handle new <be-xml> format and provide better handling of *.extra_strings.
-rw-r--r--.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/0a995544-20dc-42a6-8d3f-348ebbc8921e/body18
-rw-r--r--.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/0a995544-20dc-42a6-8d3f-348ebbc8921e/values8
-rw-r--r--.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/values17
-rw-r--r--README.dev10
-rw-r--r--becommands/assign.py4
-rw-r--r--becommands/close.py4
-rw-r--r--becommands/comment.py82
-rw-r--r--becommands/commit.py6
-rw-r--r--becommands/depend.py6
-rw-r--r--becommands/diff.py2
-rw-r--r--becommands/email_bugs.py237
-rw-r--r--becommands/help.py2
-rw-r--r--becommands/html.py2
-rw-r--r--becommands/import_xml.py267
-rw-r--r--becommands/init.py16
-rw-r--r--becommands/list.py2
-rw-r--r--becommands/merge.py6
-rw-r--r--becommands/new.py2
-rw-r--r--becommands/open.py4
-rw-r--r--becommands/remove.py4
-rw-r--r--becommands/set.py2
-rw-r--r--becommands/severity.py4
-rw-r--r--becommands/show.py158
-rw-r--r--becommands/status.py4
-rw-r--r--becommands/subscribe.py2
-rw-r--r--becommands/tag.py4
-rw-r--r--becommands/target.py4
-rw-r--r--interfaces/email/interactive/README15
-rwxr-xr-xinterfaces/email/interactive/be-handle-mail38
-rw-r--r--interfaces/email/interactive/examples/email_bugs37
-rwxr-xr-xinterfaces/xml/be-mbox-to-xml8
-rwxr-xr-xinterfaces/xml/be-xml-to-mbox65
-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
-rwxr-xr-xrelease.py8
-rw-r--r--test.py9
-rwxr-xr-xupdate_copyright.py141
45 files changed, 1340 insertions, 478 deletions
diff --git a/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/0a995544-20dc-42a6-8d3f-348ebbc8921e/body b/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/0a995544-20dc-42a6-8d3f-348ebbc8921e/body
new file mode 100644
index 0000000..8596c92
--- /dev/null
+++ b/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/0a995544-20dc-42a6-8d3f-348ebbc8921e/body
@@ -0,0 +1,18 @@
+Since we'll be distributing a non-bzr-repo version, it would be nice
+to adapt our 'submit bug' procedure
+ $ be new "The demuxulizer is broken"
+ Created bug with ID 48f
+ $ be comment 48f
+ <Describe bug>
+ $ bzr commit --message "Reported bug in demuxulizer"
+ $ bzr send --mail-to "be-devel@bugseverywhere.org"
+to one that works with this setup. Without guaranteed versioning,
+that would probably be something along the lines of
+ $ be new "The demuxulizer is broken"
+ Created bug with ID 48f
+ $ be comment 48f
+ <Describe bug>
+ $ be email-bugs [--to be-devel@bugseverywhere.org] 48f
+With interfaces/email/interactive listening on the recieving end to
+grab new-bug emails and import them into an incoming bug repository.
+
diff --git a/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/0a995544-20dc-42a6-8d3f-348ebbc8921e/values b/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/0a995544-20dc-42a6-8d3f-348ebbc8921e/values
new file mode 100644
index 0000000..4bd8f81
--- /dev/null
+++ b/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/0a995544-20dc-42a6-8d3f-348ebbc8921e/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Fri, 20 Nov 2009 13:31:25 +0000
+
diff --git a/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/values b/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/values
new file mode 100644
index 0000000..2e15ca9
--- /dev/null
+++ b/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/values
@@ -0,0 +1,17 @@
+creator: W. Trevor King <wking@drexel.edu>
+
+
+reporter: W. Trevor King <wking@drexel.edu>
+
+
+severity: minor
+
+
+status: open
+
+
+summary: be email-bugs for bug submission from bzr-less users
+
+
+time: Fri, 20 Nov 2009 13:26:59 +0000
+
diff --git a/README.dev b/README.dev
index ddc3a88..fb4f471 100644
--- a/README.dev
+++ b/README.dev
@@ -10,11 +10,19 @@ To fit into the current framework, your extension module should
provide the following elements:
__desc__
A short string describing the purpose of your plugin
- execute(args)
+ execute(args, manipulate_encodings=True, restrict_file_access=False)
The entry function for your plugin. args is everything from
sys.argv after the name of your plugin (e.g. for the command
`be open abc', args=['abc']).
+ manipulate_encodings should be passed through to any calls to
+ bugdir.BugDir(). See the BugDir documentation for details.
+
+ If restrict_file_access==True, you should call
+ cmdutil.restrict_file_access(bugdir, path)
+ before attempting to read or write a file. See the
+ restrict_file_access documentation for details.
+
Note: be supports command-completion. To avoid raising errors you
need to deal with possible '--complete' options and arguments.
See the 'Command completion' section below for more information.
diff --git a/becommands/assign.py b/becommands/assign.py
index fbef281..2c78f69 100644
--- a/becommands/assign.py
+++ b/becommands/assign.py
@@ -21,7 +21,7 @@
from libbe import cmdutil, bugdir
__desc__ = __doc__
-def execute(args, manipulate_encodings=True):
+def execute(args, manipulate_encodings=True, restrict_file_access=False):
"""
>>> import os
>>> bd = bugdir.SimpleBugDir()
@@ -57,7 +57,7 @@ def execute(args, manipulate_encodings=True):
raise cmdutil.UsageError("Too many arguments.")
bd = bugdir.BugDir(from_disk=True,
manipulate_encodings=manipulate_encodings)
- bug = cmdutil.bug_from_shortname(bd, args[0])
+ bug = cmdutil.bug_from_id(bd, args[0])
bug = bd.bug_from_shortname(args[0])
if len(args) == 1:
bug.assigned = bd.user_id
diff --git a/becommands/close.py b/becommands/close.py
index 2cdcb59..a14cea8 100644
--- a/becommands/close.py
+++ b/becommands/close.py
@@ -21,7 +21,7 @@
from libbe import cmdutil, bugdir
__desc__ = __doc__
-def execute(args, manipulate_encodings=True):
+def execute(args, manipulate_encodings=True, restrict_file_access=False):
"""
>>> from libbe import bugdir
>>> import os
@@ -45,7 +45,7 @@ def execute(args, manipulate_encodings=True):
raise cmdutil.UsageError("Too many arguments.")
bd = bugdir.BugDir(from_disk=True,
manipulate_encodings=manipulate_encodings)
- bug = cmdutil.bug_from_shortname(bd, args[0])
+ bug = cmdutil.bug_from_id(bd, args[0])
bug.status = "closed"
bd.save()
diff --git a/becommands/comment.py b/becommands/comment.py
index dad32dd..8e899ce 100644
--- a/becommands/comment.py
+++ b/becommands/comment.py
@@ -19,20 +19,16 @@
from libbe import cmdutil, bugdir, comment, editor
import os
import sys
-try: # import core module, Python >= 2.5
- from xml.etree import ElementTree
-except ImportError: # look for non-core module
- from elementtree import ElementTree
__desc__ = __doc__
-def execute(args, manipulate_encodings=True):
+def execute(args, manipulate_encodings=True, restrict_file_access=False):
"""
>>> import time
>>> bd = bugdir.SimpleBugDir()
>>> os.chdir(bd.root)
>>> execute(["a", "This is a comment about a"], manipulate_encodings=False)
>>> bd._clear_bugs()
- >>> bug = cmdutil.bug_from_shortname(bd, "a")
+ >>> bug = cmdutil.bug_from_id(bd, "a")
>>> bug.load_comments(load_full=False)
>>> comment = bug.comment_root[0]
>>> print comment.body
@@ -54,7 +50,7 @@ def execute(args, manipulate_encodings=True):
>>> os.environ["EDITOR"] = "echo 'I like cheese' > "
>>> execute(["b"], manipulate_encodings=False)
>>> bd._clear_bugs()
- >>> bug = cmdutil.bug_from_shortname(bd, "b")
+ >>> bug = cmdutil.bug_from_id(bd, "b")
>>> bug.load_comments(load_full=False)
>>> comment = bug.comment_root[0]
>>> print comment.body
@@ -71,25 +67,10 @@ def execute(args, manipulate_encodings=True):
raise cmdutil.UsageError("Too many arguments.")
shortname = args[0]
- if shortname.count(':') > 1:
- raise cmdutil.UserError("Invalid id '%s'." % shortname)
- elif shortname.count(':') == 1:
- # Split shortname generated by Comment.comment_shortnames()
- bugname = shortname.split(':')[0]
- is_reply = True
- else:
- bugname = shortname
- is_reply = False
bd = bugdir.BugDir(from_disk=True,
manipulate_encodings=manipulate_encodings)
- bug = cmdutil.bug_from_shortname(bd, bugname)
- bug.load_comments(load_full=False)
- if is_reply:
- parent = bug.comment_root.comment_from_shortname(shortname,
- bug_shortname=bugname)
- else:
- parent = bug.comment_root
+ bug, parent = cmdutil.bug_comment_from_id(bd, shortname)
if len(args) == 1: # try to launch an editor for comment-body entry
try:
@@ -118,51 +99,11 @@ def execute(args, manipulate_encodings=True):
if not body.endswith('\n'):
body+='\n'
- if options.XML == False:
- new = parent.new_reply(body=body, content_type=options.content_type)
- if options.author != None:
- new.author = options.author
- if options.alt_id != None:
- new.alt_id = options.alt_id
- else: # import XML comment [list]
- # read in the comments
- str_body = body.encode("unicode_escape").replace(r'\n', '\n')
- comment_list = ElementTree.XML(str_body)
- if comment_list.tag not in ["bug", "comment-list"]:
- raise comment.InvalidXML(
- comment_list, "root element must be <bug> or <comment-list>")
- new_comments = []
- ids = []
- for c in bug.comment_root.traverse():
- ids.append(c.uuid)
- if c.alt_id != None:
- ids.append(c.alt_id)
- for child in comment_list.getchildren():
- if child.tag == "comment":
- new = comment.Comment(bug)
- new.from_xml(unicode(ElementTree.tostring(child)).decode("unicode_escape"))
- if new.alt_id in ids:
- raise cmdutil.UserError(
- "Clashing comment alt_id: %s" % new.alt_id)
- ids.append(new.uuid)
- if new.alt_id != None:
- ids.append(new.alt_id)
- if new.in_reply_to == None:
- new.in_reply_to = parent.uuid
- new_comments.append(new)
- else:
- print >> sys.stderr, "Ignoring unknown tag %s in %s" \
- % (child.tag, comment_list.tag)
- try:
- comment.list_to_root(new_comments,bug,root=parent, # link new comments
- ignore_missing_references=options.ignore_missing_references)
- except comment.MissingReference, e:
- raise cmdutil.UserError(e)
- # Protect against programmer error causing data loss:
- kids = [c.uuid for c in parent.traverse()]
- for nc in new_comments:
- assert nc.uuid in kids, "%s wasn't added to %s" % (nc.uuid, parent.uuid)
- nc.save()
+ new = parent.new_reply(body=body, content_type=options.content_type)
+ if options.author != None:
+ new.author = options.author
+ if options.alt_id != None:
+ new.alt_id = options.alt_id
def get_parser():
parser = cmdutil.CmdOptionParser("be comment ID [COMMENT]")
@@ -172,11 +113,6 @@ def get_parser():
help="Set an alternate comment ID", default=None)
parser.add_option("-c", "--content-type", metavar="MIME", dest="content_type",
help="Set comment content-type (e.g. text/plain)", default=None)
- parser.add_option("-x", "--xml", action="store_true", default=False,
- dest='XML', help="Use COMMENT to specify an XML comment description rather than the comment body. The root XML element should be either <bug> or <comment-list> with one or more <comment> children. The syntax for the <comment> elements should match that generated by 'be show --xml COMMENT-ID'. Unrecognized tags are ignored. Missing tags are left at the default value. The comment UUIDs are always auto-generated, so if you set a <uuid> field, but no <alt-id> field, your <uuid> will be used as the comment's <alt-id>. An exception is raised if <alt-id> conflicts with an existing comment.")
- parser.add_option("-i", "--ignore-missing-references", action="store_true",
- dest="ignore_missing_references",
- help="For XML import, if any comment's <in-reply-to> refers to a non-existent comment, ignore it (instead of raising an exception).")
return parser
longhelp="""
diff --git a/becommands/commit.py b/becommands/commit.py
index dc70e7e..39d1e2e 100644
--- a/becommands/commit.py
+++ b/becommands/commit.py
@@ -18,9 +18,9 @@ from libbe import cmdutil, bugdir, editor, vcs
import sys
__desc__ = __doc__
-def execute(args, manipulate_encodings=True):
+def execute(args, manipulate_encodings=True, restrict_file_access=False):
"""
- >>> import os, time
+ >>> import os
>>> from libbe import bug
>>> bd = bugdir.SimpleBugDir()
>>> os.chdir(bd.root)
@@ -49,6 +49,8 @@ def execute(args, manipulate_encodings=True):
elif options.body == "EDITOR":
body = editor.editor_string("Please enter your commit message above")
else:
+ if restrict_file_access == True:
+ cmdutil.restrict_file_access(bd, options.body)
body = bd.vcs.get_file_contents(options.body, allow_no_vcs=True)
try:
revision = bd.vcs.commit(summary, body=body,
diff --git a/becommands/depend.py b/becommands/depend.py
index f52527e..f50d693 100644
--- a/becommands/depend.py
+++ b/becommands/depend.py
@@ -35,7 +35,7 @@ class BrokenLink (Exception):
self.blocking_bug = blocking_bug
-def execute(args, manipulate_encodings=True):
+def execute(args, manipulate_encodings=True, restrict_file_access=False):
"""
>>> from libbe import utility
>>> bd = bugdir.SimpleBugDir()
@@ -95,7 +95,7 @@ def execute(args, manipulate_encodings=True):
for blockee,blocker in fixed])
return 0
- bugA = cmdutil.bug_from_shortname(bd, args[0])
+ bugA = cmdutil.bug_from_id(bd, args[0])
if options.tree_depth != None:
dtree = DependencyTree(bd, bugA, options.tree_depth)
@@ -112,7 +112,7 @@ def execute(args, manipulate_encodings=True):
return 0
if len(args) == 2:
- bugB = cmdutil.bug_from_shortname(bd, args[1])
+ bugB = cmdutil.bug_from_id(bd, args[1])
if options.remove == True:
remove_block(bugA, bugB)
else: # add the dependency
diff --git a/becommands/diff.py b/becommands/diff.py
index e71da9b..6477934 100644
--- a/becommands/diff.py
+++ b/becommands/diff.py
@@ -21,7 +21,7 @@ from libbe import cmdutil, bugdir, diff
import os
__desc__ = __doc__
-def execute(args, manipulate_encodings=True):
+def execute(args, manipulate_encodings=True, restrict_file_access=False):
"""
>>> import os
>>> bd = bugdir.SimpleBugDir()
diff --git a/becommands/email_bugs.py b/becommands/email_bugs.py
new file mode 100644
index 0000000..d0366df
--- /dev/null
+++ b/becommands/email_bugs.py
@@ -0,0 +1,237 @@
+# 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.
+"""Email specified bugs in a be-handle-mail compatible format."""
+
+import copy
+from cStringIO import StringIO
+from email import Message
+from email.mime.text import MIMEText
+from email.generator import Generator
+import sys
+import time
+
+from libbe import cmdutil, bugdir
+from libbe.subproc import invoke
+from libbe.utility import time_to_str
+from libbe.vcs import detect_vcs, installed_vcs
+import show
+
+__desc__ = __doc__
+
+sendmail='/usr/sbin/sendmail -t'
+
+def execute(args, manipulate_encodings=True, restrict_file_access=False):
+ """
+ >>> import os
+ >>> from libbe import bug
+ >>> bd = bugdir.SimpleBugDir()
+ >>> bd.encoding = 'utf-8'
+ >>> os.chdir(bd.root)
+ >>> import email.charset as c
+ >>> c.add_charset('utf-8', c.SHORTEST, c.QP, 'utf-8')
+ >>> execute(["-o", "--to", "a@b.com", "--from", "b@c.edu", "a", "b"],
+ ... manipulate_encodings=False) # doctest: +ELLIPSIS
+ Content-Type: text/xml; charset="utf-8"
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: quoted-printable
+ From: b@c.edu
+ To: a@b.com
+ Date: ...
+ Subject: [be-bug:xml] Updates to a, b
+ <BLANKLINE>
+ <?xml version=3D"1.0" encoding=3D"utf-8" ?>
+ <be-xml>
+ <version>
+ <tag>...</tag>
+ <branch-nick>...</branch-nick>
+ <revno>...</revno>
+ <revision-id>...
+ </version>
+ <bug>
+ <uuid>a</uuid>
+ <short-name>a</short-name>
+ <severity>minor</severity>
+ <status>open</status>
+ <creator>John Doe &lt;jdoe@example.com&gt;</creator>
+ <created>Thu, 01 Jan 1970 00:00:00 +0000</created>
+ <summary>Bug A</summary>
+ </bug>
+ <bug>
+ <uuid>b</uuid>
+ <short-name>b</short-name>
+ <severity>minor</severity>
+ <status>closed</status>
+ <creator>Jane Doe &lt;jdoe@example.com&gt;</creator>
+ <created>Thu, 01 Jan 1970 00:00:00 +0000</created>
+ <summary>Bug B</summary>
+ </bug>
+ </be-xml>
+ >>> bd.cleanup()
+
+ Note that the '=3D' bits in
+ <?xml version=3D"1.0" encoding=3D"utf-8" ?>
+ are the way quoted-printable escapes '='.
+
+ The unclosed <revision-id>... is because revision ids can be long
+ enough to cause line wraps, and we want to ensure we match even if
+ the closing </revision-id> is split by the wrapping.
+ """
+ parser = get_parser()
+ options, args = parser.parse_args(args)
+ cmdutil.default_complete(options, args, parser,
+ bugid_args={-1: lambda bug : bug.active==True})
+ if len(args) == 0:
+ raise cmdutil.UsageError
+ bd = bugdir.BugDir(from_disk=True,
+ manipulate_encodings=manipulate_encodings)
+ xml = show.output(args, bd, as_xml=True, with_comments=True)
+ subject = options.subject
+ if subject == None:
+ subject = '[be-bug:xml] Updates to %s' % ', '.join(args)
+ submit_email = TextEmail(to_address=options.to_address,
+ from_address=options.from_address,
+ subject=subject,
+ body=xml,
+ encoding=bd.encoding,
+ subtype='xml')
+ if options.output == True:
+ print submit_email
+ else:
+ submit_email.send()
+
+def get_parser():
+ parser = cmdutil.CmdOptionParser("be email-bugs [options] ID [ID ...]")
+ parser.add_option("-t", "--to", metavar="EMAIL", dest="to_address",
+ help="Submission email address (%default)",
+ default="be-devel@bugseverywhere.org")
+ parser.add_option("-f", "--from", metavar="EMAIL", dest="from_address",
+ help="Senders email address, overriding auto-generated default",
+ default=None)
+ parser.add_option("-s", "--subject", metavar="STRING", dest="subject",
+ help="Subject line, overriding auto-generated default. If you use this option, remember that be-handle-mail probably want something like '[be-bug:xml] ...'",
+ default=None)
+ parser.add_option('-o', '--output', dest='output', action='store_true',
+ help="Don't mail the generated message, print it to stdout instead. Useful for testing functionality.")
+ return parser
+
+longhelp="""
+Email specified bugs in a be-handle-mail compatible format. This is
+the prefered method for reporting bugs if you did not install bzr by
+branching a bzr repository.
+
+If you _did_ install bzr by branching a bzr repository, we suggest you
+commit any new bug information with
+ bzr commit --message "Reported bug in demuxulizer"
+and then email a bzr merge directive with
+ bzr send --mail-to "be-devel@bugseverywhere.org"
+rather than using this command.
+"""
+
+def help():
+ return get_parser().help_str() + longhelp
+
+class TextEmail (object):
+ """
+ Make it very easy to compose and send single-part text emails.
+ >>> msg = TextEmail(to_address='Monty <monty@a.com>',
+ ... from_address='Python <python@b.edu>',
+ ... subject='Parrots',
+ ... header={'x-special-header':'your info here'},
+ ... body="Remarkable bird, id'nit, squire?\\nLovely plumage!")
+ >>> print msg # doctest: +ELLIPSIS
+ Content-Type: text/plain; charset="utf-8"
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: base64
+ From: Python <python@b.edu>
+ To: Monty <monty@a.com>
+ Date: ...
+ Subject: Parrots
+ x-special-header: your info here
+ <BLANKLINE>
+ UmVtYXJrYWJsZSBiaXJkLCBpZCduaXQsIHNxdWlyZT8KTG92ZWx5IHBsdW1hZ2Uh
+ <BLANKLINE>
+ >>> import email.charset as c
+ >>> c.add_charset('utf-8', c.SHORTEST, c.QP, 'utf-8')
+ >>> print msg # doctest: +ELLIPSIS
+ Content-Type: text/plain; charset="utf-8"
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: quoted-printable
+ From: Python <python@b.edu>
+ To: Monty <monty@a.com>
+ Date: ...
+ Subject: Parrots
+ x-special-header: your info here
+ <BLANKLINE>
+ Remarkable bird, id'nit, squire?
+ Lovely plumage!
+ """
+ def __init__(self, to_address, from_address=None, subject=None,
+ header=None, body=None, encoding='utf-8', subtype='plain'):
+ self.to_address = to_address
+ self.from_address = from_address
+ if self.from_address == None:
+ self.from_address = self._guess_from_address()
+ self.subject = subject
+ self.header = header
+ if self.header == None:
+ self.header = {}
+ self.body = body
+ self.encoding = encoding
+ self.subtype = subtype
+ def _guess_from_address(self):
+ vcs = detect_vcs('.')
+ if vcs.name == "None":
+ vcs = installed_vcs()
+ return vcs.get_user_id()
+ def encoded_MIME_body(self):
+ return MIMEText(self.body.encode(self.encoding),
+ self.subtype,
+ self.encoding)
+ def message(self):
+ response = self.encoded_MIME_body()
+ response['From'] = self.from_address
+ response['To'] = self.to_address
+ response['Date'] = time_to_str(time.time())
+ response['Subject'] = self.subject
+ for k,v in self.header.items():
+ response[k] = v
+ return response
+ def flatten(self, to_unicode=False):
+ """
+ This is a simplified version of send_pgp_mime.flatten().
+ """
+ fp = StringIO()
+ g = Generator(fp, mangle_from_=False)
+ g.flatten(self.message())
+ text = fp.getvalue()
+ if to_unicode == True:
+ encoding = msg.get_content_charset() or "utf-8"
+ text = unicode(text, encoding=encoding)
+ return text
+ def __str__(self):
+ return self.flatten()
+ def __unicode__(self):
+ return self.flatten(to_unicode=True)
+ def send(self, sendmail=None):
+ """
+ This is a simplified version of send_pgp_mime.mail().
+
+ Send an email Message instance on its merry way by shelling
+ out to the user specified sendmail.
+ """
+ if sendmail == None:
+ sendmail = SENDMAIL
+ invoke(sendmail, stdin=self.flatten())
diff --git a/becommands/help.py b/becommands/help.py
index c12c56a..99ab8c4 100644
--- a/becommands/help.py
+++ b/becommands/help.py
@@ -20,7 +20,7 @@
from libbe import cmdutil, utility
__desc__ = __doc__
-def execute(args, manipulate_encodings=False):
+def execute(args, manipulate_encodings=True, restrict_file_access=False):
"""
Print help of specified command (the manipulate_encodings argument
is ignored).
diff --git a/becommands/html.py b/becommands/html.py
index b0640da..622a531 100644
--- a/becommands/html.py
+++ b/becommands/html.py
@@ -21,7 +21,7 @@ import xml.sax.saxutils, htmlentitydefs
__desc__ = __doc__
-def execute(args, manipulate_encodings=True):
+def execute(args, manipulate_encodings=True, restrict_file_access=False):
"""
>>> import os
>>> bd = bugdir.SimpleBugDir()
diff --git a/becommands/import_xml.py b/becommands/import_xml.py
new file mode 100644
index 0000000..a74d329
--- /dev/null
+++ b/becommands/import_xml.py
@@ -0,0 +1,267 @@
+# 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.
+"""Import comments and bugs from XML"""
+from libbe import cmdutil, bugdir, bug, comment, utility
+from becommands.comment import complete
+import os
+import sys
+try: # import core module, Python >= 2.5
+ from xml.etree import ElementTree
+except ImportError: # look for non-core module
+ from elementtree import ElementTree
+__desc__ = __doc__
+
+def execute(args, manipulate_encodings=True, restrict_file_access=False):
+ """
+ >>> import time
+ >>> import StringIO
+ >>> bd = bugdir.SimpleBugDir()
+ >>> os.chdir(bd.root)
+ >>> orig_stdin = sys.stdin
+ >>> sys.stdin = StringIO.StringIO("<be-xml><comment><uuid>c</uuid><body>This is a comment about a</body></comment></be-xml>")
+ >>> execute(["-c", "a", "-"], manipulate_encodings=False)
+ >>> sys.stdin = orig_stdin
+ >>> bd._clear_bugs()
+ >>> bug = cmdutil.bug_from_id(bd, "a")
+ >>> bug.load_comments(load_full=False)
+ >>> comment = bug.comment_root[0]
+ >>> print comment.body
+ This is a comment about a
+ <BLANKLINE>
+ >>> comment.author == bd.user_id
+ True
+ >>> comment.time <= int(time.time())
+ True
+ >>> comment.in_reply_to is None
+ True
+ >>> bd.cleanup()
+ """
+ parser = get_parser()
+ options, args = parser.parse_args(args)
+ complete(options, args, parser)
+ if len(args) < 1:
+ raise cmdutil.UsageError("Please specify an XML file.")
+ if len(args) > 1:
+ raise cmdutil.UsageError("Too many arguments.")
+ filename = args[0]
+
+ bd = bugdir.BugDir(from_disk=True,
+ manipulate_encodings=manipulate_encodings)
+ if options.comment_root != None:
+ croot_bug,croot_comment = \
+ cmdutil.bug_comment_from_id(bd, options.comment_root)
+ else:
+ croot_bug,croot_comment = (None, None)
+
+ if filename == '-':
+ xml = sys.stdin.read()
+ else:
+ if restrict_file_access == True:
+ cmdutil.restrict_file_access(bd, options.body)
+ xml = bd.vcs.get_file_contents(filename, allow_no_vcs=True)
+ str_xml = xml.encode('unicode_escape').replace(r'\n', '\n')
+ # unicode read + encode to string so we know the encoding,
+ # which might not be given (?) in a binary string read?
+
+ # parse the xml
+ root_bugs = []
+ root_comments = []
+ version = {}
+ be_xml = ElementTree.XML(str_xml)
+ if be_xml.tag != 'be-xml':
+ raise utility.InvalidXML(
+ 'import-xml', be_xml, 'root element must be <be-xml>')
+ for child in be_xml.getchildren():
+ if child.tag == 'bug':
+ new = bug.Bug(bugdir=bd)
+ new.from_xml(unicode(ElementTree.tostring(child)).decode('unicode_escape'))
+ root_bugs.append(new)
+ elif child.tag == 'comment':
+ new = comment.Comment(croot_bug)
+ new.from_xml(unicode(ElementTree.tostring(child)).decode("unicode_escape"))
+ root_comments.append(new)
+ elif child.tag == 'version':
+ for gchild in child.getchildren():
+ if child.tag in ['tag', 'nick', 'revision', 'revision-id']:
+ text = xml.sax.saxutils.unescape(child.text)
+ text = text.decode('unicode_escape').strip()
+ version[child.tag] = text
+ else:
+ print >> sys.stderr, 'ignoring unknown tag %s in %s' \
+ % (gchild.tag, child.tag)
+ else:
+ print >> sys.stderr, 'ignoring unknown tag %s in %s' \
+ % (child.tag, comment_list.tag)
+
+ # merge the new root_comments
+ if len(root_comments) > 0:
+ if croot_bug == None:
+ raise UserError(
+ '--comment-root option is required for your root comments:\n%s'
+ % '\n\n'.join([c.string() for c in root_comments]))
+ croot_cids = []
+ for c in croot_bug.comment_root.traverse():
+ croot_cids.append(c.uuid)
+ if c.alt_id != None:
+ croot_cids.append(c.alt_id)
+ for new in root_comments:
+ if new.alt_id in croot_cids:
+ raise cmdutil.UserError(
+ 'clashing comment alt_id: %s' % new.alt_id)
+ croot_cids.append(new.uuid)
+ if new.alt_id != None:
+ croot_cids.append(new.alt_id)
+ if new.in_reply_to == None:
+ new.in_reply_to = croot_comment.uuid
+ try:
+ # link new comments
+ comment.list_to_root(root_comments,croot_bug,root=croot_comment,
+ ignore_missing_references= \
+ options.ignore_missing_references)
+ except comment.MissingReference, e:
+ raise cmdutil.UserError(e)
+ # merge the new croot_bugs
+ for new in root_bugs:
+ bd.append(new)
+
+ # protect against programmer error causing data loss:
+ if croot_bug != None:
+ comms = [c.uuid for c in croot_comment.traverse()]
+ for new in root_comments:
+ assert new.uuid in comms, \
+ "comment %s wasn't added to %s" % (new.uuid, croot_comment.uuid)
+ for new in root_bugs:
+ assert bd.has_bug(new.uuid), \
+ "bug %s wasn't added" % (new.uuid)
+
+ # save new information
+ for new in root_comments:
+ new.save()
+ for new in root_bugs:
+ new.save()
+
+def get_parser():
+ parser = cmdutil.CmdOptionParser("be import-xml XMLFILE")
+ parser.add_option("-i", "--ignore-missing-references", action="store_true",
+ dest="ignore_missing_references", default=False,
+ help="If any comment's <in-reply-to> refers to a non-existent comment, ignore it (instead of raising an exception).")
+ parser.add_option("-a", "--add-only", action='store_true',
+ dest="add_only", default=False,
+ help="If any bug or comment listed in the XML file already exists in the bug repository, do not alter the repository version.")
+ parser.add_option("-c", "--comment-root", dest="comment_root",
+ help="Supply a bug or comment ID as the root of any <comment> elements that are direct children of the <be-xml> element. If any such <comment> elements exist, you are required to set this option.")
+ return parser
+
+longhelp="""
+Import comments and bugs from XMLFILE. If XMLFILE is '-', the file is
+read from stdin.
+
+This command provides a fallback mechanism for passing bugs between
+repositories, in case the repositories VCSs are incompatible. If the
+VCSs are compatible, it's better to use their builtin merge/push/pull
+to share this information, as that will preserve a more detailed
+history.
+
+The XML file should be formatted similarly to
+ <be-xml>
+ <version>
+ <tag>1.0.0</tag>
+ <branch-nick>be</branch-nick>
+ <revno>446</revno>
+ <revision-id>a@b.com-20091119214553-iqyw2cpqluww3zna</revision-id>
+ <version>
+ <bug>
+ ...
+ <comment>...</comment>
+ <comment>...</comment>
+ </bug>
+ <bug>...</bug>
+ <comment>...</comment>
+ <comment>...</comment>
+ </be-xml>
+where the ellipses mark output commpatible with Bug.xml() and
+Comment.xml(). Take a look at the output of `be show --xml` for some
+explicit examples. Unrecognized tags are ignored. Missing tags are
+left at the default value. The version tag is not required, but is
+strongly recommended.
+
+The bug and comment UUIDs are always auto-generated, so if you set a
+<uuid> field, but no <alt-id> field, your <uuid> will be used as the
+comment's <alt-id>. An exception is raised if <alt-id> conflicts with
+an existing comment. Bugs do not have a permantent alt-id, so they
+the <uuid>s you specify are not saved. The <uuid>s _are_ used to
+match agains prexisting bug and comment uuids, and comment alt-ids,
+and fields explicitly given in the XML file will replace old versions
+unless the --add-only flag.
+
+*.extra_strings recieves special treatment, and if --add-only is not
+set, the resulting list concatenates both source lists and removes
+repeats.
+
+Here's an example of import activity:
+ Repository
+ bug (uuid=B, author=John, status=open)
+ estr (don't forget your towel)
+ estr (helps with space travel)
+ com (uuid=C1, author=Jane, body=Hello)
+ com (uuid=C2, author=Jess, body=World)
+ XML
+ bug (uuid=B, status=fixed)
+ estr (don't forget your towel)
+ estr (watch out for flying dolphins)
+ com (uuid=C1, body=So long)
+ com (uuid=C3, author=Jed, body=And thanks)
+ Result
+ bug (uuid=B, author=John, status=fixed)
+ estr (don't forget your towel)
+ estr (helps with space travel)
+ estr (watch out for flying dolphins)
+ com (uuid=C1, author=Jane, body=So long)
+ com (uuid=C2, author=Jess, body=World)
+ com (uuid=C4, alt-id=C3, author=Jed, body=And thanks)
+ Result, with --add-only
+ bug (uuid=B, author=John, status=open)
+ estr (don't forget your towel)
+ estr (helps with space travel)
+ com (uuid=C1, author=Jane, body=Hello)
+ com (uuid=C2, author=Jess, body=World)
+ com (uuid=C4, alt-id=C3, author=Jed, body=And thanks)
+
+Examples:
+
+Import comments (e.g. emails from an mbox) and append to bug XYZ
+ $ be-mbox-to-xml mail.mbox | be import-xml --c XYZ -
+Or you can append those emails underneath the prexisting comment XYZ-3
+ $ be-mbox-to-xml mail.mbox | be import-xml --c XYZ-3 -
+
+User creates a new bug
+ user$ be new "The demuxulizer is broken"
+ Created bug with ID 48f
+ user$ be comment 48f
+ <Describe bug>
+ ...
+User exports bug as xml and emails it to the developers
+ user$ be show --xml 48f > 48f.xml
+ user$ cat 48f.xml | mail -s "Demuxulizer bug xml" devs@b.com
+or equivalently (with a slightly fancier be-handle-mail compatible
+email):
+ user$ be email-bugs 48f
+Devs recieve email, and save it's contents as demux-bug.xml
+ dev$ cat demux-bug.xml | be import-xml -
+"""
+
+def help():
+ return get_parser().help_str() + longhelp
diff --git a/becommands/init.py b/becommands/init.py
index 275dd77..7d6d475 100644
--- a/becommands/init.py
+++ b/becommands/init.py
@@ -20,7 +20,7 @@ import os.path
from libbe import cmdutil, bugdir
__desc__ = __doc__
-def execute(args, manipulate_encodings=True):
+def execute(args, manipulate_encodings=True, restrict_file_access=False):
"""
>>> from libbe import utility, vcs
>>> import os
@@ -37,14 +37,14 @@ def execute(args, manipulate_encodings=True):
>>> dir = utility.Dir()
>>> os.chdir(dir.path)
- >>> vcs = vcs.installed_vcs()
- >>> vcs.init('.')
- >>> print vcs.name
- Arch
- >>> execute([], manipulate_encodings=False)
- Using Arch for revision control.
+ >>> _vcs = vcs.installed_vcs()
+ >>> _vcs.init('.')
+ >>> _vcs.name in vcs.VCS_ORDER
+ True
+ >>> execute([], manipulate_encodings=False) # doctest: +ELLIPSIS
+ Using ... for revision control.
Directory initialized.
- >>> vcs.cleanup()
+ >>> _vcs.cleanup()
>>> try:
... execute(['--root', '.'], manipulate_encodings=False)
diff --git a/becommands/list.py b/becommands/list.py
index 14e387b..4711789 100644
--- a/becommands/list.py
+++ b/becommands/list.py
@@ -26,7 +26,7 @@ __desc__ = __doc__
AVAILABLE_CMPS = [fn[4:] for fn in dir(bug) if fn[:4] == 'cmp_']
AVAILABLE_CMPS.remove("attr") # a cmp_* template.
-def execute(args, manipulate_encodings=True):
+def execute(args, manipulate_encodings=True, restrict_file_access=False):
"""
>>> import os
>>> bd = bugdir.SimpleBugDir()
diff --git a/becommands/merge.py b/becommands/merge.py
index bc18479..31c781f 100644
--- a/becommands/merge.py
+++ b/becommands/merge.py
@@ -19,7 +19,7 @@ from libbe import cmdutil, bugdir
import os, copy
__desc__ = __doc__
-def execute(args, manipulate_encodings=True):
+def execute(args, manipulate_encodings=True, restrict_file_access=False):
"""
>>> from libbe import utility
>>> bd = bugdir.SimpleBugDir()
@@ -137,9 +137,9 @@ def execute(args, manipulate_encodings=True):
bd = bugdir.BugDir(from_disk=True,
manipulate_encodings=manipulate_encodings)
- bugA = cmdutil.bug_from_shortname(bd, args[0])
+ bugA = cmdutil.bug_from_id(bd, args[0])
bugA.load_comments()
- bugB = cmdutil.bug_from_shortname(bd, args[1])
+ bugB = cmdutil.bug_from_id(bd, args[1])
bugB.load_comments()
mergeA = bugA.new_comment("Merged from bug %s" % bugB.uuid)
newCommTree = copy.deepcopy(bugB.comment_root)
diff --git a/becommands/new.py b/becommands/new.py
index 30a7d28..92d61e4 100644
--- a/becommands/new.py
+++ b/becommands/new.py
@@ -20,7 +20,7 @@ from libbe import cmdutil, bugdir
import sys
__desc__ = __doc__
-def execute(args, manipulate_encodings=True):
+def execute(args, manipulate_encodings=True, restrict_file_access=False):
"""
>>> import os, time
>>> from libbe import bug
diff --git a/becommands/open.py b/becommands/open.py
index c9e55a2..c2c15e2 100644
--- a/becommands/open.py
+++ b/becommands/open.py
@@ -21,7 +21,7 @@
from libbe import cmdutil, bugdir
__desc__ = __doc__
-def execute(args, manipulate_encodings=True):
+def execute(args, manipulate_encodings=True, restrict_file_access=False):
"""
>>> import os
>>> bd = bugdir.SimpleBugDir()
@@ -44,7 +44,7 @@ def execute(args, manipulate_encodings=True):
raise cmdutil.UsageError, "Too many arguments."
bd = bugdir.BugDir(from_disk=True,
manipulate_encodings=manipulate_encodings)
- bug = cmdutil.bug_from_shortname(bd, args[0])
+ bug = cmdutil.bug_from_id(bd, args[0])
bug.status = "open"
def get_parser():
diff --git a/becommands/remove.py b/becommands/remove.py
index 0e61362..e4f0065 100644
--- a/becommands/remove.py
+++ b/becommands/remove.py
@@ -18,7 +18,7 @@
from libbe import cmdutil, bugdir
__desc__ = __doc__
-def execute(args, manipulate_encodings=True):
+def execute(args, manipulate_encodings=True, restrict_file_access=False):
"""
>>> from libbe import mapfile
>>> import os
@@ -44,7 +44,7 @@ def execute(args, manipulate_encodings=True):
raise cmdutil.UsageError, "Please specify a bug id."
bd = bugdir.BugDir(from_disk=True,
manipulate_encodings=manipulate_encodings)
- bug = cmdutil.bug_from_shortname(bd, args[0])
+ bug = cmdutil.bug_from_id(bd, args[0])
bd.remove_bug(bug)
print "Removed bug %s" % bug.uuid
diff --git a/becommands/set.py b/becommands/set.py
index e5cab8d..c8c5a2c 100644
--- a/becommands/set.py
+++ b/becommands/set.py
@@ -32,7 +32,7 @@ def _value_string(bd, setting):
val = None
return str(val)
-def execute(args, manipulate_encodings=True):
+def execute(args, manipulate_encodings=True, restrict_file_access=False):
"""
>>> import os
>>> bd = bugdir.SimpleBugDir()
diff --git a/becommands/severity.py b/becommands/severity.py
index e987760..524976b 100644
--- a/becommands/severity.py
+++ b/becommands/severity.py
@@ -21,7 +21,7 @@
from libbe import cmdutil, bugdir, bug
__desc__ = __doc__
-def execute(args, manipulate_encodings=True):
+def execute(args, manipulate_encodings=True, restrict_file_access=False):
"""
>>> import os
>>> bd = bugdir.SimpleBugDir()
@@ -43,7 +43,7 @@ def execute(args, manipulate_encodings=True):
raise cmdutil.UsageError
bd = bugdir.BugDir(from_disk=True,
manipulate_encodings=manipulate_encodings)
- bug = cmdutil.bug_from_shortname(bd, args[0])
+ bug = cmdutil.bug_from_id(bd, args[0])
if len(args) == 1:
print bug.severity
elif len(args) == 2:
diff --git a/becommands/show.py b/becommands/show.py
index 94e16bf..557c63a 100644
--- a/becommands/show.py
+++ b/becommands/show.py
@@ -17,12 +17,12 @@
# 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.
-"""Show a particular bug"""
+"""Show a particular bug, comment, or combination of both."""
import sys
-from libbe import cmdutil, bugdir
+from libbe import cmdutil, bugdir, comment, version, _version
__desc__ = __doc__
-def execute(args, manipulate_encodings=True):
+def execute(args, manipulate_encodings=True, restrict_file_access=False):
"""
>>> import os
>>> bd = bugdir.SimpleBugDir()
@@ -41,15 +41,23 @@ def execute(args, manipulate_encodings=True):
<BLANKLINE>
>>> execute (["--xml", "a"], manipulate_encodings=False) # doctest: +ELLIPSIS
<?xml version="1.0" encoding="..." ?>
- <bug>
- <uuid>a</uuid>
- <short-name>a</short-name>
- <severity>minor</severity>
- <status>open</status>
- <creator>John Doe &lt;jdoe@example.com&gt;</creator>
- <created>...</created>
- <summary>Bug A</summary>
- </bug>
+ <be-xml>
+ <version>
+ <tag>...</tag>
+ <branch-nick>...</branch-nick>
+ <revno>...</revno>
+ <revision-id>...</revision-id>
+ </version>
+ <bug>
+ <uuid>a</uuid>
+ <short-name>a</short-name>
+ <severity>minor</severity>
+ <status>open</status>
+ <creator>John Doe &lt;jdoe@example.com&gt;</creator>
+ <created>Thu, 01 Jan 1970 00:00:00 +0000</created>
+ <summary>Bug A</summary>
+ </bug>
+ </be-xml>
>>> bd.cleanup()
"""
parser = get_parser()
@@ -60,38 +68,18 @@ def execute(args, manipulate_encodings=True):
raise cmdutil.UsageError
bd = bugdir.BugDir(from_disk=True,
manipulate_encodings=manipulate_encodings)
- if options.XML:
- print '<?xml version="1.0" encoding="%s" ?>' % bd.encoding
- for shortname in args:
- if shortname.count(':') > 1:
- raise cmdutil.UserError("Invalid id '%s'." % shortname)
- elif shortname.count(':') == 1:
- # Split shortname generated by Comment.comment_shortnames()
- bugname = shortname.split(':')[0]
- is_comment = True
- else:
- bugname = shortname
- is_comment = False
- if is_comment == True and options.comments == False:
- continue
- bug = cmdutil.bug_from_shortname(bd, bugname)
- if is_comment == False:
- if options.XML:
- print bug.xml(show_comments=options.comments)
- else:
- print bug.string(show_comments=options.comments)
- else:
- comment = bug.comment_root.comment_from_shortname(
- shortname, bug_shortname=bugname)
- if options.XML:
- print comment.xml(shortname=shortname)
- else:
- if len(args) == 1 and options.only_raw_body == True:
- sys.__stdout__.write(comment.body)
- else:
- print comment.string(shortname=shortname)
- if shortname != args[-1] and options.XML == False:
- print "" # add a blank line between bugs/comments
+
+ if options.only_raw_body == True:
+ if len(args) != 1:
+ raise cmdutil.UsageError(
+ 'only one ID accepted with --only-raw-body')
+ bug,comment = cmdutil.bug_comment_from_id(bd, args[0])
+ if comment == bug.comment_root:
+ raise cmdutil.UsageError(
+ "--only-raw-body requires a comment ID, not '%s'" % args[0])
+ sys.__stdout__.write(comment.body)
+ sys.exit(0)
+ print output(args, bd, as_xml=options.XML, with_comments=options.comments)
def get_parser():
parser = cmdutil.CmdOptionParser("be show [options] ID [ID ...]")
@@ -108,9 +96,87 @@ def get_parser():
longhelp="""
Show all information about the bugs or comments whose IDs are given.
-It's probably not a good idea to mix bug and comment IDs in a single
-call, but you're free to do so if you like.
+Without the --xml flag set, it's probably not a good idea to mix bug
+and comment IDs in a single call, but you're free to do so if you
+like. With the --xml flag set, there will never be any root comments,
+so mix and match away (the bug listings for directly requested
+comments will be restricted to the bug uuid and the requested
+comment(s)).
+
+Directly requested comments will be grouped by their parent bug and
+placed at the end of the output, so the ordering may not match the
+order of the listed IDs.
"""
def help():
return get_parser().help_str() + longhelp
+
+def _sort_ids(ids, with_comments=True):
+ bugs = []
+ root_comments = {}
+ for id in ids:
+ bugname,commname = cmdutil.parse_id(id)
+ if commname == None:
+ bugs.append(bugname)
+ elif with_comments == True:
+ if bugname not in root_comments:
+ root_comments[bugname] = [commname]
+ else:
+ root_comments[bugname].append(commname)
+ for bugname in root_comments.keys():
+ assert bugname not in bugs, \
+ "specifically requested both '%s%s' and '%s'" \
+ % (bugname, root_comments[bugname][0], bugname)
+ return (bugs, root_comments)
+
+def _xml_header(encoding):
+ lines = ['<?xml version="1.0" encoding="%s" ?>' % encoding,
+ '<be-xml>',
+ ' <version>',
+ ' <tag>%s</tag>' % version.version()]
+ for tag in ['branch-nick', 'revno', 'revision-id']:
+ value = _version.version_info[tag.replace('-', '_')]
+ lines.append(' <%s>%s</%s>' % (tag, value, tag))
+ lines.append(' </version>')
+ return lines
+
+def _xml_footer():
+ return ['</be-xml>']
+
+def output(ids, bd, as_xml=True, with_comments=True):
+ bugs,root_comments = _sort_ids(ids, with_comments)
+ lines = []
+ if as_xml:
+ lines.extend(_xml_header(bd.encoding))
+ else:
+ spaces_left = len(ids) - 1
+ for bugname in bugs:
+ bug = cmdutil.bug_from_id(bd, bugname)
+ if as_xml:
+ lines.append(bug.xml(indent=2, show_comments=with_comments))
+ else:
+ lines.append(bug.string(show_comments=with_comments))
+ if spaces_left > 0:
+ spaces_left -= 1
+ lines.append('') # add a blank line between bugs/comments
+ for bugname,comments in root_comments.items():
+ bug = cmdutil.bug_from_id(bd, bugname)
+ if as_xml:
+ lines.extend([' <bug>', ' <uuid>%s</uuid>' % bug.uuid])
+ for commname in comments:
+ try:
+ comment = bug.comment_root.comment_from_shortname(commname)
+ except comment.InvalidShortname, e:
+ raise UserError(e.message)
+ if as_xml:
+ lines.append(comment.xml(indent=4, shortname=bugname))
+ else:
+ lines.append(comment.string(shortname=shortname))
+ if spaces_left > 0:
+ spaces_left -= 1
+ lines.append('') # add a blank line between bugs/comments
+ if as_xml:
+ lines.append('</bug>')
+ if as_xml:
+ lines.extend(_xml_footer())
+ return '\n'.join(lines)
diff --git a/becommands/status.py b/becommands/status.py
index 827c7ce..fd31c97 100644
--- a/becommands/status.py
+++ b/becommands/status.py
@@ -18,7 +18,7 @@
from libbe import cmdutil, bugdir, bug
__desc__ = __doc__
-def execute(args, manipulate_encodings=True):
+def execute(args, manipulate_encodings=True, restrict_file_access=False):
"""
>>> import os
>>> bd = bugdir.SimpleBugDir()
@@ -40,7 +40,7 @@ def execute(args, manipulate_encodings=True):
raise cmdutil.UsageError
bd = bugdir.BugDir(from_disk=True,
manipulate_encodings=manipulate_encodings)
- bug = cmdutil.bug_from_shortname(bd, args[0])
+ bug = cmdutil.bug_from_id(bd, args[0])
if len(args) == 1:
print bug.status
else:
diff --git a/becommands/subscribe.py b/becommands/subscribe.py
index 0a23057..051341b 100644
--- a/becommands/subscribe.py
+++ b/becommands/subscribe.py
@@ -55,7 +55,7 @@ class InvalidType (ValueError):
self.type_root = type_root
-def execute(args, manipulate_encodings=True):
+def execute(args, manipulate_encodings=True, restrict_file_access=False):
"""
>>> bd = bugdir.SimpleBugDir()
>>> bd.set_sync_with_disk(True)
diff --git a/becommands/tag.py b/becommands/tag.py
index 31b43ba..e22cb70 100644
--- a/becommands/tag.py
+++ b/becommands/tag.py
@@ -19,7 +19,7 @@ from libbe import cmdutil, bugdir
import os, copy
__desc__ = __doc__
-def execute(args, manipulate_encodings=True):
+def execute(args, manipulate_encodings=True, restrict_file_access=False):
"""
>>> from libbe import utility
>>> bd = bugdir.SimpleBugDir()
@@ -95,7 +95,7 @@ def execute(args, manipulate_encodings=True):
if len(tags) > 0:
print '\n'.join(tags)
return
- bug = cmdutil.bug_from_shortname(bd, args[0])
+ bug = cmdutil.bug_from_id(bd, args[0])
if len(args) == 2:
given_tag = args[1]
estrs = bug.extra_strings
diff --git a/becommands/target.py b/becommands/target.py
index 7e41451..efb2479 100644
--- a/becommands/target.py
+++ b/becommands/target.py
@@ -22,7 +22,7 @@
from libbe import cmdutil, bugdir
__desc__ = __doc__
-def execute(args, manipulate_encodings=True):
+def execute(args, manipulate_encodings=True, restrict_file_access=False):
"""
>>> import os
>>> bd = bugdir.SimpleBugDir()
@@ -55,7 +55,7 @@ def execute(args, manipulate_encodings=True):
if target and isinstance(target,str):
print target
return
- bug = cmdutil.bug_from_shortname(bd, args[0])
+ bug = cmdutil.bug_from_id(bd, args[0])
if len(args) == 1:
if bug.target is None:
print "No target assigned."
diff --git a/interfaces/email/interactive/README b/interfaces/email/interactive/README
index 79ef9a9..2070973 100644
--- a/interfaces/email/interactive/README
+++ b/interfaces/email/interactive/README
@@ -23,13 +23,15 @@ execution.
Once be-handle-mail receives the email, the parsing method is selected
according to the subject tag that procmail used grab the email in the
-first place. There are three parsing styles:
+first place. There are four parsing styles:
Style Subject
creating bugs [be-bug:submit] new bug summary
commenting on bugs [be-bug:<bug-id>] commit message
control [be-bug] commit message
+ xml [be-bug:xml] commit message
These are analogous to submit@bugs.debian.org, nnn@bugs.debian.org,
-and control@bugs.debian.org respectively.
+and control@bugs.debian.org respectively. The xml style has no Debian
+analog.
Creating bugs
=============
@@ -106,6 +108,15 @@ shlex.split().
--
Goofy tagline ignored.
+XML
+===
+
+This interface allows users without access to the versioned source of
+the program to conveniently submit bugs and comments using be. You
+should not attempt to compose emails for this interface by hand. See
+the documentation for the `email-bugs' and `import-xml' be commands
+for details.
+
Example emails
==============
diff --git a/interfaces/email/interactive/be-handle-mail b/interfaces/email/interactive/be-handle-mail
index fa80698..e0e3490 100755
--- a/interfaces/email/interactive/be-handle-mail
+++ b/interfaces/email/interactive/be-handle-mail
@@ -81,6 +81,7 @@ SUBJECT_TAG_START = None
SUBJECT_TAG_NEW = None
SUBJECT_TAG_COMMENT = None
SUBJECT_TAG_CONTROL = None
+SUBJECT_TAG_XML = None
BREAK = u"--"
NEW_REQUIRED_PSEUDOHEADERS = [u"Version"]
@@ -90,7 +91,7 @@ NEW_OPTIONAL_PSEUDOHEADERS = [u"Reporter", u"Assign", u"Depend", u"Severity",
CONTROL_COMMENT = u"#"
ALLOWED_COMMANDS = [u"assign", u"comment", u"commit", u"depend", u"help",
u"list", u"merge", u"new", u"open", u"severity", u"show",
- u"status", u"subscribe", u"tag", u"target"]
+ u"status", u"subscribe", u"tag", u"target", u"import-xml"]
AUTOCOMMIT = True
@@ -241,7 +242,8 @@ class Command (object):
os.chdir(BE_DIR)
try:
self.ret = libbe.cmdutil.execute(self.command, self.args,
- manipulate_encodings=False)
+ manipulate_encodings=False,
+ restrict_file_access=True)
except libbe.cmdutil.GetHelp:
print libbe.cmdutil.help(command)
except libbe.cmdutil.GetCompletions:
@@ -402,8 +404,8 @@ class Message (object):
def _subject_tag_type(self):
"""
Parse subject tag, return (type, value), where type is one of
- None, "new", "comment", or "control"; and value is None except
- in the case of "comment", in which case it's the bug
+ None, "new", "comment", "control", or "xml"; and value is None
+ except in the case of "comment", in which case it's the bug
ID/shortname.
"""
tag,subject = self._split_subject()
@@ -413,6 +415,8 @@ class Message (object):
type = u"new"
elif tag == SUBJECT_TAG_CONTROL:
type = u"control"
+ elif tag == SUBJECT_TAG_XML:
+ type = u"xml"
else:
match = SUBJECT_TAG_COMMENT.match(tag)
if len(match.groups()) == 1:
@@ -506,6 +510,8 @@ class Message (object):
commands = self.parse_comment(value)
elif tag_type == u"control":
commands = self.parse_control()
+ elif tag_type == u"xml":
+ commands = self.parse_xml()
else:
raise Exception, u"Unrecognized tag type '%s'" % tag_type
return commands
@@ -564,8 +570,10 @@ class Message (object):
if mime_type == "text/plain":
body = self._strip_footer(body)
content_type = mime_type
- args = [u"--author", author, u"--alt-id", alt_id,
- u"--content-type", content_type, bug_id, u"-"]
+ args = [u"--author", author]
+ if alt_id != None:
+ args.extend([u"--alt-id", alt_id])
+ args.extend([u"--content-type", content_type, bug_id, u"-"])
commands = [Command(self, command, args, stdin=body)]
return commands
def parse_control(self):
@@ -583,6 +591,16 @@ class Message (object):
if len(commands) == 0:
raise InvalidEmail(self, u"No commands in control email.")
return commands
+ def parse_xml(self):
+ command = u"import-xml"
+ body,mime_type = list(self._get_bodies_and_mime_types())[0]
+ if mime_type != "text/xml":
+ raise InvalidEmail(self,
+ u"Emails to %s must have MIME type 'text/xml', not '%s'."
+ % (SUBJECT_TAG_XML, mime_type))
+ args = [u"-"]
+ commands = [Command(self, command, args, stdin=body)]
+ return commands
def run(self):
self._begin_response()
commands = self.parse()
@@ -736,13 +754,15 @@ def generate_global_tags(tag_base=u"be-bug"):
Generate a series of tags from a base tag string.
"""
global SUBJECT_TAG_BASE, SUBJECT_TAG_START, SUBJECT_TAG_RESPONSE, \
- SUBJECT_TAG_NEW, SUBJECT_TAG_COMMENT, SUBJECT_TAG_CONTROL
+ SUBJECT_TAG_NEW, SUBJECT_TAG_COMMENT, SUBJECT_TAG_CONTROL, \
+ SUBJECT_TAG_XML
SUBJECT_TAG_BASE = tag_base
SUBJECT_TAG_START = u"[%s" % tag_base
SUBJECT_TAG_RESPONSE = u"[%s]" % tag_base
SUBJECT_TAG_NEW = u"[%s:submit]" % tag_base
SUBJECT_TAG_COMMENT = re.compile(u"\[%s:([\-0-9a-z]*)]" % tag_base)
SUBJECT_TAG_CONTROL = SUBJECT_TAG_RESPONSE
+ SUBJECT_TAG_XML = u"[%s:xml]" % tag_base
def open_logfile(logpath=None):
"""
@@ -945,6 +965,10 @@ class GenerateGlobalTagsTestCase (unittest.TestCase):
m = SUBJECT_TAG_COMMENT.match("[projectX-bug:xyz-123]")
self.failUnlessEqual(len(m.groups()), 1)
self.failUnlessEqual(m.group(1), u"xyz-123")
+ def test_subject_tag_xml(self):
+ "Should set SUBJECT_TAG_XML global correctly"
+ generate_global_tags(u"projectX-bug")
+ self.failUnlessEqual(SUBJECT_TAG_XML, u"[projectX-bug:xml]")
if __name__ == "__main__":
main(sys.argv)
diff --git a/interfaces/email/interactive/examples/email_bugs b/interfaces/email/interactive/examples/email_bugs
new file mode 100644
index 0000000..949e1c1
--- /dev/null
+++ b/interfaces/email/interactive/examples/email_bugs
@@ -0,0 +1,37 @@
+From jdoe@example.com Fri Apr 18 12:00:00 2008
+Content-Type: text/xml; charset="utf-8"
+MIME-Version: 1.0
+Content-Transfer-Encoding: quoted-printable
+From: jdoe@example.com
+To: a@b.com
+Date: Fri, 18 Apr 2008 12:00:00 +0000
+Subject: [be-bug:xml] Updates to a, b
+
+<?xml version="1.0" encoding="utf-8" ?>
+<be-xml>
+ <version>
+ <tag>1.0.0</tag>
+ <branch-nick>be</branch-nick>
+ <revno>446</revno>
+ <revision-id>wking@drexel.edu-20091119214553-iqyw2cpqluww3zna</revision-id>
+ </version>
+ <bug>
+ <uuid>a</uuid>
+ <short-name>a</short-name>
+ <severity>minor</severity>
+ <status>open</status>
+ <creator>John Doe &lt;jdoe@example.com&gt;</creator>
+ <created>Thu, 01 Jan 1970 00:00:00 +0000</created>
+ <summary>Bug A</summary>
+ </bug>
+ <bug>
+ <uuid>b</uuid>
+ <short-name>b</short-name>
+ <severity>minor</severity>
+ <status>closed</status>
+ <creator>Jane Doe &lt;jdoe@example.com&gt;</creator>
+ <created>Thu, 01 Jan 1970 00:00:00 +0000</created>
+ <summary>Bug B</summary>
+ </bug>
+</be-xml>
+
diff --git a/interfaces/xml/be-mbox-to-xml b/interfaces/xml/be-mbox-to-xml
index a740117..3af2978 100755
--- a/interfaces/xml/be-mbox-to-xml
+++ b/interfaces/xml/be-mbox-to-xml
@@ -15,8 +15,8 @@
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""
-Convert an mbox into xml suitable for imput into be.
- $ cat mbox | be-mbox-to-xml | be comment --xml <ID> -
+Convert an mbox into xml suitable for input into be.
+ $ be-mbox-to-xml file.mbox | be import-xml -c <ID> -
mbox is a flat-file format, consisting of a series of messages.
Messages begin with a a From_ line, followed by RFC 822 email,
followed by a blank line.
@@ -140,10 +140,10 @@ def comment_message_to_xml(message, fields=None):
def main(mbox_filename):
mb = mbox(mbox_filename)
print u'<?xml version="1.0" encoding="%s" ?>' % DEFAULT_ENCODING
- print u"<comment-list>"
+ print u"<be-xml>"
for message in mb:
print comment_message_to_xml(message)
- print u"</comment-list>"
+ print u"</be-xml>"
if __name__ == "__main__":
diff --git a/interfaces/xml/be-xml-to-mbox b/interfaces/xml/be-xml-to-mbox
index 7960d56..dc4524e 100755
--- a/interfaces/xml/be-xml-to-mbox
+++ b/interfaces/xml/be-xml-to-mbox
@@ -86,21 +86,23 @@ class Bug (LimitedAttrDict):
u"comments",
u"extra-strings"]
def print_to_mbox(self):
- name,addr = email.utils.parseaddr(self["creator"])
- print "From %s %s" % (addr, rfc2822_to_asctime(self["created"]))
- print "Message-ID: <%s@%s>" % (self["uuid"], DEFAULT_DOMAIN)
- print "Date: %s" % self["created"]
- print "From: %s" % self["creator"]
- print "Content-Type: %s; charset=%s" % ("text/plain", DEFAULT_ENCODING)
- print "Content-Transfer-Encoding: 8bit"
- print "Subject: %s: %s" % (self["short-name"], self["summary"])
- print ""
- print self["summary"]
- print ""
- if "extra-strings" in self:
- print "extra strings:\n ",
- print '\n '.join(self["extra_strings"])
- print ""
+ if "creator" in self:
+ # otherwise, probably a `be show` uuid-only bug to avoid
+ # root comments.
+ name,addr = email.utils.parseaddr(self["creator"])
+ print "From %s %s" % (addr, rfc2822_to_asctime(self["created"]))
+ print "Message-id: <%s@%s>" % (self["uuid"], DEFAULT_DOMAIN)
+ print "Date: %s" % self["created"]
+ print "From: %s" % self["creator"]
+ print "Content-Type: %s; charset=%s" % ("text/plain", DEFAULT_ENCODING)
+ print "Content-Transfer-Encoding: 8bit"
+ print "Subject: %s: %s" % (self["short-name"], self["summary"])
+ if "extra-strings" in self:
+ for estr in self["extra_strings"]:
+ print "X-Extra-String: %s" % estr
+ print ""
+ print self["summary"]
+ print ""
if "comments" in self:
for comment in self["comments"]:
comment.print_to_mbox(self)
@@ -131,7 +133,8 @@ class Comment (LimitedAttrDict):
u"author",
u"date",
u"content-type",
- u"body"]
+ u"body",
+ u"extra-strings"]
def print_to_mbox(self, bug=None):
if bug == None:
bug = Bug()
@@ -142,7 +145,7 @@ class Comment (LimitedAttrDict):
elif "alt-id" in self: id = self["alt-id"]
else: id = None
if id != None:
- print "Message-ID: <%s@%s>" % (id, DEFAULT_DOMAIN)
+ print "Message-id: <%s@%s>" % (id, DEFAULT_DOMAIN)
print "Date: %s" % self["date"]
print "From: %s" % self["author"]
subject = ""
@@ -156,6 +159,9 @@ class Comment (LimitedAttrDict):
if "in-reply-to" not in self.keys():
self["in-reply-to"] = bug["uuid"]
print "In-Reply-To: <%s@%s>" % (self["in-reply-to"], DEFAULT_DOMAIN)
+ if "extra-strings" in self:
+ for estr in self["extra_strings"]:
+ print "X-Extra-String: %s" % estr
if self["content-type"].startswith("text/"):
print "Content-Transfer-Encoding: 8bit"
print "Content-Type: %s; charset=%s" % (self["content-type"], DEFAULT_ENCODING)
@@ -168,9 +174,15 @@ class Comment (LimitedAttrDict):
assert element.tag == "comment", element.tag
for field in element.getchildren():
text = unescape(unicode(field.text).decode("unicode_escape").strip())
- if field.tag == "body":
- text+="\n"
- self[field.tag] = text
+ if field.tag == "extra-string":
+ if "extra-strings" in self:
+ self["extra-strings"].append(text)
+ else:
+ self["extra-strings"] = [text]
+ else:
+ if field.tag == "body":
+ text+="\n"
+ self[field.tag] = text
def print_to_mbox(element):
if element.tag == "bug":
@@ -181,16 +193,9 @@ def print_to_mbox(element):
c = Comment()
c.init_from_etree(element)
c.print_to_mbox()
- elif element.tag in ["bugs", "bug-list"]:
- for b_elt in element.getchildren():
- b = Bug()
- b.init_from_etree(b_elt)
- b.print_to_mbox()
- elif element.tag in ["comments", "comment-list"]:
- for c_elt in element.getchildren():
- c = Comment()
- c.init_from_etree(c_elt)
- c.print_to_mbox()
+ elif element.tag in ["be-xml"]:
+ for elt in element.getchildren():
+ print_to_mbox(elt)
if __name__ == "__main__":
import sys
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)
diff --git a/release.py b/release.py
index 996e363..2e75687 100755
--- a/release.py
+++ b/release.py
@@ -20,10 +20,10 @@ import os
import os.path
import shutil
import string
-from subprocess import Popen
import sys
-from update_copyright import Pipe, update_authors, update_files
+from libbe.subproc import Pipe, invoke
+from update_copyright import update_authors, update_files
def validate_tag(tag):
"""
@@ -93,8 +93,8 @@ def make_version():
def make_changelog(filename, tag):
print 'generate ChangeLog file', filename, 'up to tag', tag
- p = Popen(['bzr', 'log', '--gnu-changelog', '-n1', '-r',
- '..tag:%s' % tag], stdout=file(filename, 'w'))
+ p = invoke(['bzr', 'log', '--gnu-changelog', '-n1', '-r',
+ '..tag:%s' % tag], stdout=file(filename, 'w'))
status = p.wait()
assert status == 0, status
diff --git a/test.py b/test.py
index 1f1ffcf..57091c7 100644
--- a/test.py
+++ b/test.py
@@ -8,7 +8,7 @@ When called with module name arguments, only run the doctests from
those modules.
"""
-from libbe import plugin
+from libbe import plugin, vcs
import unittest
import doctest
import sys
@@ -41,9 +41,10 @@ else:
for modname,module in plugin.iter_plugins("becommands"):
suite.addTest(doctest.DocTestSuite(module))
-#for s in suite._tests:
-# print s
-#exit(0)
+_vcs = vcs.installed_vcs()
+vcs.set_preferred_vcs(_vcs.name)
+print 'Using %s as the testing VCS' % _vcs.name
+
result = unittest.TextTestRunner(verbosity=2).run(suite)
numErrors = len(result.errors)
diff --git a/update_copyright.py b/update_copyright.py
index 6cdaa2f..5ca5455 100755
--- a/update_copyright.py
+++ b/update_copyright.py
@@ -24,9 +24,10 @@ import time
import os
import sys
import select
-from subprocess import Popen, PIPE, mswindows
from threading import Thread
+from libbe.subproc import Pipe
+
COPYRIGHT_TEXT="""#
# 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
@@ -71,144 +72,6 @@ EXCLUDES = [
IGNORED_PATHS = ['./.be/', './.bzr/', './build/']
IGNORED_FILES = ['COPYING', 'update_copyright.py', 'catmutt']
-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: # POSIX
- 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)
-
def _strip_email(*args):
"""
>>> _strip_email('J Doe <jdoe@a.com>')