aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorW. Trevor King <wking@drexel.edu>2009-11-30 06:29:48 -0500
committerW. Trevor King <wking@drexel.edu>2009-11-30 06:29:48 -0500
commitc4a9b465fb512fdfa2d43ece22c786b021d8c2ce (patch)
treec92629e416c42c813f45ee6ca69f5197873e27cf
parentf3de7e1a6d07b5488fd3c9e01caba53216e612d2 (diff)
parent13784e6067b652e4fe08e488fdc4baabc37f24ef (diff)
downloadbugseverywhere-c4a9b465fb512fdfa2d43ece22c786b021d8c2ce.tar.gz
Merged completed be.email-bugs branch.
Highlights: * import-xml now works as advertized in its longhelp string * new methods Bug.merge() and Comment.merge() * comment.list_to_root() is now Bug.add_comments() * BugDir.list_uuids() is now BugDir.uuids() * Bug.from_xml() now imports comments :p * test.py uses unittest.TestSuite 'suite' in becommands, if present.
-rw-r--r--.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/4068c833-0c06-475e-8b7e-6701bc416dee/body28
-rw-r--r--.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/4068c833-0c06-475e-8b7e-6701bc416dee/values11
-rw-r--r--.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/values2
-rw-r--r--becommands/comment.py2
-rw-r--r--becommands/import_xml.py215
-rw-r--r--becommands/target.py2
-rwxr-xr-xinterfaces/email/interactive/be-handle-mail2
-rw-r--r--libbe/bug.py262
-rw-r--r--libbe/bugdir.py12
-rw-r--r--libbe/comment.py158
-rw-r--r--libbe/diff.py4
-rw-r--r--libbe/subproc.py2
-rw-r--r--test.py5
13 files changed, 593 insertions, 112 deletions
diff --git a/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/4068c833-0c06-475e-8b7e-6701bc416dee/body b/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/4068c833-0c06-475e-8b7e-6701bc416dee/body
new file mode 100644
index 0000000..d3d9d0c
--- /dev/null
+++ b/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/4068c833-0c06-475e-8b7e-6701bc416dee/body
@@ -0,0 +1,28 @@
+> With interfaces/email/interactive listening on the recieving end to
+> grab new-bug emails and import them into an incoming bug repository.
+
+The email-bugs -> be-handle-mail import is based on `be import-xml`.
+The current import-xml implementation allows good control over what
+gets overwritten during a merge by overriding only those fields
+defined in the incoming XML.
+
+For clients without the versioned bugdir (e.g. they installed via a
+release tarball or their distro's packaging system), `be email-bugs`
+will not know what fields have been changed/added/etc., so it sets
+_all_ the fields in the outgoing XML. Importing that XML file will
+override any changes that may have been made to the listed
+bugs/comments between the release and your current source version, so
+you may have to do some manual tweaking of the post-merge bugdir.
+
+One possible workaround would be to change the merge algorithm in
+import-xml to take advantage of version information given in the XML
+file. import-xml could checkout the shared root version of any
+modified bugs, and compute the changes made by the remote user and
+those made in the local tree. It could then merge these changes more
+intelligently, by prompting the user, keeping the local changes,
+keeping the remote changes, etc.
+
+While the more automated approach might be better, it's also more
+complicated, so for now we'll stick with the simple "override all
+fields defined in the XML" approach.
+
diff --git a/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/4068c833-0c06-475e-8b7e-6701bc416dee/values b/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/4068c833-0c06-475e-8b7e-6701bc416dee/values
new file mode 100644
index 0000000..e77ec55
--- /dev/null
+++ b/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/4068c833-0c06-475e-8b7e-6701bc416dee/values
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sun, 29 Nov 2009 01:19:05 +0000
+
+
+In-reply-to: 0a995544-20dc-42a6-8d3f-348ebbc8921e
+
diff --git a/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/values b/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/values
index 2e15ca9..2d546cb 100644
--- a/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/values
+++ b/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/values
@@ -7,7 +7,7 @@ reporter: W. Trevor King <wking@drexel.edu>
severity: minor
-status: open
+status: fixed
summary: be email-bugs for bug submission from bzr-less users
diff --git a/becommands/comment.py b/becommands/comment.py
index 8e899ce..fbc994f 100644
--- a/becommands/comment.py
+++ b/becommands/comment.py
@@ -145,7 +145,7 @@ def complete(options, args, parser):
bd = bugdir.BugDir(from_disk=True,
manipulate_encodings=False)
bugs = []
- for uuid in bd.list_uuids():
+ for uuid in bd.uuids():
if uuid.startswith(partial):
bug = bd.bug_from_uuid(uuid)
if bug.active == True:
diff --git a/becommands/import_xml.py b/becommands/import_xml.py
index a74d329..892a09e 100644
--- a/becommands/import_xml.py
+++ b/becommands/import_xml.py
@@ -16,12 +16,15 @@
"""Import comments and bugs from XML"""
from libbe import cmdutil, bugdir, bug, comment, utility
from becommands.comment import complete
+import copy
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
+import doctest
+import unittest
__desc__ = __doc__
def execute(args, manipulate_encodings=True, restrict_file_access=False):
@@ -63,6 +66,22 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False):
if options.comment_root != None:
croot_bug,croot_comment = \
cmdutil.bug_comment_from_id(bd, options.comment_root)
+ croot_bug.load_comments(load_full=True)
+ croot_bug.set_sync_with_disk(False)
+ if croot_comment.uuid == comment.INVALID_UUID:
+ croot_comment = croot_bug.comment_root
+ else:
+ croot_comment = croot_bug.comment_from_uuid(croot_comment.uuid)
+ new_croot_bug = bug.Bug(bugdir=bd, uuid=croot_bug.uuid)
+ new_croot_bug.explicit_attrs = []
+ new_croot_bug.comment_root = copy.deepcopy(croot_bug.comment_root)
+ if croot_comment.uuid == comment.INVALID_UUID:
+ new_croot_comment = new_croot_bug.comment_root
+ else:
+ new_croot_comment = \
+ new_croot_bug.comment_from_uuid(croot_comment.uuid)
+ for new in new_croot_bug.comments():
+ new.explicit_attrs = []
else:
croot_bug,croot_comment = (None, None)
@@ -87,7 +106,7 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False):
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'))
+ new.from_xml(unicode(ElementTree.tostring(child)).decode("unicode_escape"))
root_bugs.append(new)
elif child.tag == 'comment':
new = comment.Comment(croot_bug)
@@ -107,35 +126,47 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False):
% (child.tag, comment_list.tag)
# merge the new root_comments
+ if options.add_only == True:
+ accept_changes = False
+ accept_extra_strings = False
+ else:
+ accept_changes = True
+ accept_extra_strings = True
+ accept_comments = True
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)
+ new_croot_bug.add_comments(root_comments,
+ default_parent=new_croot_comment,
+ ignore_missing_references= \
+ options.ignore_missing_references)
except comment.MissingReference, e:
raise cmdutil.UserError(e)
+ croot_bug.merge(new_croot_bug, accept_changes=accept_changes,
+ accept_extra_strings=accept_extra_strings,
+ accept_comments=accept_comments)
+
# merge the new croot_bugs
+ merged_bugs = []
+ old_bugs = []
for new in root_bugs:
- bd.append(new)
+ try:
+ old = bd.bug_from_uuid(new.alt_id)
+ except KeyError:
+ old = None
+ if old == None:
+ bd.append(new)
+ else:
+ old.load_comments(load_full=True)
+ old.merge(new, accept_changes=accept_changes,
+ accept_extra_strings=accept_extra_strings,
+ accept_comments=accept_comments)
+ merged_bugs.append(new)
+ old_bugs.append(old)
# protect against programmer error causing data loss:
if croot_bug != None:
@@ -144,14 +175,18 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False):
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)
+ if not new in merged_bugs:
+ assert bd.has_bug(new.uuid), \
+ "bug %s wasn't added" % (new.uuid)
# save new information
- for new in root_comments:
- new.save()
+ if croot_bug != None:
+ croot_bug.save()
for new in root_bugs:
- new.save()
+ if not new in merged_bugs:
+ new.save()
+ for old in old_bugs:
+ old.save()
def get_parser():
parser = cmdutil.CmdOptionParser("be import-xml XMLFILE")
@@ -213,7 +248,7 @@ repeats.
Here's an example of import activity:
Repository
- bug (uuid=B, author=John, status=open)
+ bug (uuid=B, creator=John, status=open)
estr (don't forget your towel)
estr (helps with space travel)
com (uuid=C1, author=Jane, body=Hello)
@@ -225,7 +260,7 @@ Here's an example of import activity:
com (uuid=C1, body=So long)
com (uuid=C3, author=Jed, body=And thanks)
Result
- bug (uuid=B, author=John, status=fixed)
+ bug (uuid=B, creator=John, status=fixed)
estr (don't forget your towel)
estr (helps with space travel)
estr (watch out for flying dolphins)
@@ -233,7 +268,7 @@ Here's an example of import activity:
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)
+ bug (uuid=B, creator=John, status=open)
estr (don't forget your towel)
estr (helps with space travel)
com (uuid=C1, author=Jane, body=Hello)
@@ -265,3 +300,129 @@ Devs recieve email, and save it's contents as demux-bug.xml
def help():
return get_parser().help_str() + longhelp
+
+
+class LonghelpTestCase (unittest.TestCase):
+ """
+ Test import scenarios given in longhelp.
+ """
+ def setUp(self):
+ self.bugdir = bugdir.SimpleBugDir()
+ self.original_working_dir = os.getcwd()
+ os.chdir(self.bugdir.root)
+ bugA = self.bugdir.bug_from_uuid('a')
+ self.bugdir.remove_bug(bugA)
+ self.bugdir.set_sync_with_disk(False)
+ bugB = self.bugdir.bug_from_uuid('b')
+ bugB.creator = 'John'
+ bugB.status = 'open'
+ bugB.extra_strings += ["don't forget your towel"]
+ bugB.extra_strings += ['helps with space travel']
+ comm1 = bugB.comment_root.new_reply(body='Hello\n')
+ comm1.uuid = 'c1'
+ comm1.author = 'Jane'
+ comm2 = bugB.comment_root.new_reply(body='World\n')
+ comm2.uuid = 'c2'
+ comm2.author = 'Jess'
+ bugB.save()
+ self.bugdir.set_sync_with_disk(True)
+ self.xml = """
+ <be-xml>
+ <bug>
+ <uuid>b</uuid>
+ <status>fixed</status>
+ <summary>a test bug</summary>
+ <extra-string>don't forget your towel</extra-string>
+ <extra-string>watch out for flying dolphins</extra-string>
+ <comment>
+ <uuid>c1</uuid>
+ <body>So long</body>
+ </comment>
+ <comment>
+ <uuid>c3</uuid>
+ <author>Jed</author>
+ <body>And thanks</body>
+ </comment>
+ </bug>
+ </be-xml>
+ """
+ def tearDown(self):
+ os.chdir(self.original_working_dir)
+ self.bugdir.cleanup()
+ def _execute(self, *args):
+ import StringIO
+ orig_stdin = sys.stdin
+ sys.stdin = StringIO.StringIO(self.xml)
+ execute(list(args)+["-"], manipulate_encodings=False,
+ restrict_file_access=True)
+ sys.stdin = orig_stdin
+ self.bugdir._clear_bugs()
+ def testCleanBugdir(self):
+ uuids = list(self.bugdir.uuids())
+ self.failUnless(uuids == ['b'], uuids)
+ def testNotAddOnly(self):
+ self._execute()
+ uuids = list(self.bugdir.uuids())
+ self.failUnless(uuids == ['b'], uuids)
+ bugB = self.bugdir.bug_from_uuid('b')
+ self.failUnless(bugB.uuid == 'b', bugB.uuid)
+ self.failUnless(bugB.creator == 'John', bugB.creator)
+ self.failUnless(bugB.status == 'fixed', bugB.status)
+ estrs = ["don't forget your towel",
+ 'helps with space travel',
+ 'watch out for flying dolphins']
+ self.failUnless(bugB.extra_strings == estrs, bugB.extra_strings)
+ comments = list(bugB.comments())
+ self.failUnless(len(comments) == 3,
+ ['%s (%s, %s)' % (c.uuid, c.alt_id, c.body) for c in comments])
+ c1 = bugB.comment_from_uuid('c1')
+ comments.remove(c1)
+ self.failUnless(c1.uuid == 'c1', c1.uuid)
+ self.failUnless(c1.alt_id == None, c1.alt_id)
+ self.failUnless(c1.author == 'Jane', c1.author)
+ self.failUnless(c1.body == 'So long\n', c1.body)
+ c2 = bugB.comment_from_uuid('c2')
+ comments.remove(c2)
+ self.failUnless(c2.uuid == 'c2', c2.uuid)
+ self.failUnless(c2.alt_id == None, c2.alt_id)
+ self.failUnless(c2.author == 'Jess', c2.author)
+ self.failUnless(c2.body == 'World\n', c2.body)
+ c4 = comments[0]
+ self.failUnless(len(c4.uuid) == 36, c4.uuid)
+ self.failUnless(c4.alt_id == 'c3', c4.alt_id)
+ self.failUnless(c4.author == 'Jed', c4.author)
+ self.failUnless(c4.body == 'And thanks\n', c4.body)
+ def testAddOnly(self):
+ self._execute('--add-only')
+ uuids = list(self.bugdir.uuids())
+ self.failUnless(uuids == ['b'], uuids)
+ bugB = self.bugdir.bug_from_uuid('b')
+ self.failUnless(bugB.uuid == 'b', bugB.uuid)
+ self.failUnless(bugB.creator == 'John', bugB.creator)
+ self.failUnless(bugB.status == 'open', bugB.status)
+ estrs = ["don't forget your towel",
+ 'helps with space travel']
+ self.failUnless(bugB.extra_strings == estrs, bugB.extra_strings)
+ comments = list(bugB.comments())
+ self.failUnless(len(comments) == 3,
+ ['%s (%s)' % (c.uuid, c.alt_id) for c in comments])
+ c1 = bugB.comment_from_uuid('c1')
+ comments.remove(c1)
+ self.failUnless(c1.uuid == 'c1', c1.uuid)
+ self.failUnless(c1.alt_id == None, c1.alt_id)
+ self.failUnless(c1.author == 'Jane', c1.author)
+ self.failUnless(c1.body == 'Hello\n', c1.body)
+ c2 = bugB.comment_from_uuid('c2')
+ comments.remove(c2)
+ self.failUnless(c2.uuid == 'c2', c2.uuid)
+ self.failUnless(c2.alt_id == None, c2.alt_id)
+ self.failUnless(c2.author == 'Jess', c2.author)
+ self.failUnless(c2.body == 'World\n', c2.body)
+ c4 = comments[0]
+ self.failUnless(len(c4.uuid) == 36, c4.uuid)
+ self.failUnless(c4.alt_id == 'c3', c4.alt_id)
+ self.failUnless(c4.author == 'Jed', c4.author)
+ self.failUnless(c4.body == 'And thanks\n', c4.body)
+
+unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
+suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()])
diff --git a/becommands/target.py b/becommands/target.py
index efb2479..9a202b1 100644
--- a/becommands/target.py
+++ b/becommands/target.py
@@ -50,7 +50,7 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False):
bd = bugdir.BugDir(from_disk=True,
manipulate_encodings=manipulate_encodings)
if options.list:
- ts = set([bd.bug_from_uuid(bug).target for bug in bd.list_uuids()])
+ ts = set([bd.bug_from_uuid(bug).target for bug in bd.uuids()])
for target in sorted(ts):
if target and isinstance(target,str):
print target
diff --git a/interfaces/email/interactive/be-handle-mail b/interfaces/email/interactive/be-handle-mail
index e0e3490..3b321cf 100755
--- a/interfaces/email/interactive/be-handle-mail
+++ b/interfaces/email/interactive/be-handle-mail
@@ -598,7 +598,7 @@ class Message (object):
raise InvalidEmail(self,
u"Emails to %s must have MIME type 'text/xml', not '%s'."
% (SUBJECT_TAG_XML, mime_type))
- args = [u"-"]
+ args = [u"--add-only", u"-"]
commands = [Command(self, command, args, stdin=body)]
return commands
def run(self):
diff --git a/libbe/bug.py b/libbe/bug.py
index fecb9b7..897d841 100644
--- a/libbe/bug.py
+++ b/libbe/bug.py
@@ -20,6 +20,7 @@
Define the Bug class for representing bugs.
"""
+import copy
import os
import os.path
import errno
@@ -227,7 +228,7 @@ class Bug(settings_object.SavedSettingsObject):
@Property
@cached_property(generator=_get_comment_root)
@local_property("comment_root")
- @doc_property(doc="The trunk of the comment tree")
+ @doc_property(doc="The trunk of the comment tree. We use a dummy root comment by default, because there can be several comment threads rooted on the same parent bug. To simplify comment interaction, we condense these threads into a single thread with a Comment dummy root.")
def comment_root(): return {}
def _get_vcs(self):
@@ -302,7 +303,7 @@ class Bug(settings_object.SavedSettingsObject):
if v is not None:
lines.append(' <%s>%s</%s>' % (k,xml.sax.saxutils.escape(v),k))
for estr in self.extra_strings:
- lines.append(' <extra-string>%s</extra-string>\n' % estr)
+ lines.append(' <extra-string>%s</extra-string>' % estr)
if show_comments == True:
comout = self.comment_root.xml_thread(indent=indent+2,
auto_name_map=True,
@@ -324,51 +325,282 @@ class Bug(settings_object.SavedSettingsObject):
>>> 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")
+ >>> xml = bugA.xml(shortname="bug-1", show_comments=True)
>>> bugB = Bug()
>>> bugB.from_xml(xml, verbose=True)
- >>> bugB.xml(shortname="bug-1") == xml
+ >>> bugB.xml(shortname="bug-1", show_comments=True) == xml
False
>>> bugB.uuid = bugB.alt_id
- >>> bugB.xml(shortname="bug-1") == xml
+ >>> for comm in bugB.comments():
+ ... comm.uuid = comm.alt_id
+ ... comm.alt_id = None
+ >>> bugB.xml(shortname="bug-1", show_comments=True) == xml
True
+ >>> bugB.explicit_attrs # doctest: +NORMALIZE_WHITESPACE
+ ['severity', 'status', 'creator', 'created', 'summary']
+ >>> len(list(bugB.comments()))
+ 3
"""
if type(xml_string) == types.UnicodeType:
xml_string = xml_string.strip().encode('unicode_escape')
- bug = ElementTree.XML(xml_string)
+ if hasattr(xml_string, 'getchildren'): # already an ElementTree Element
+ bug = xml_string
+ else:
+ 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']
+ 'reporter', 'creator','created','summary','extra-string']
+ self.explicit_attrs = []
uuid = None
estrs = []
+ comments = []
for child in bug.getchildren():
if child.tag == 'short-name':
pass
+ elif child.tag == 'comment':
+ comm = comment.Comment(bug=self)
+ comm.from_xml(child)
+ comments.append(comm)
+ continue
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":
+ if child.tag == 'uuid':
uuid = text
continue # don't set the bug's uuid tag.
- if child.tag == 'extra-string':
+ elif child.tag == 'extra-string':
estrs.append(text)
continue # don't set the bug's extra_string yet.
- else:
- attr_name = child.tag.replace('-','_')
+ attr_name = child.tag.replace('-','_')
+ self.explicit_attrs.append(attr_name)
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 uuid != self.uuid:
if not hasattr(self, 'alt_id') or self.alt_id == None:
self.alt_id = uuid
self.extra_strings = estrs
+ self.add_comments(comments)
+
+ def add_comment(self, comment, *args, **kwargs):
+ """
+ Add a comment too the current bug, under the parent specified
+ by comment.in_reply_to.
+ Note: If a bug uuid is given, set .alt_id to it's value.
+ >>> bugA = Bug(uuid='0123', summary='Need to test Bug.add_comment()')
+ >>> bugA.creator = 'Jack'
+ >>> commA = bugA.comment_root.new_reply(body='comment A')
+ >>> commA.uuid = 'commA'
+ >>> commB = comment.Comment(body='comment B')
+ >>> commB.uuid = 'commB'
+ >>> bugA.add_comment(commB)
+ >>> commC = comment.Comment(body='comment C')
+ >>> commC.uuid = 'commC'
+ >>> commC.in_reply_to = commA.uuid
+ >>> bugA.add_comment(commC)
+ >>> print bugA.xml(shortname="bug-1", show_comments=True) # doctest: +ELLIPSIS
+ <bug>
+ <uuid>0123</uuid>
+ <short-name>bug-1</short-name>
+ <severity>minor</severity>
+ <status>open</status>
+ <creator>Jack</creator>
+ <created>...</created>
+ <summary>Need to test Bug.add_comment()</summary>
+ <comment>
+ <uuid>commA</uuid>
+ <short-name>bug-1:1</short-name>
+ <author></author>
+ <date>...</date>
+ <content-type>text/plain</content-type>
+ <body>comment A</body>
+ </comment>
+ <comment>
+ <uuid>commC</uuid>
+ <short-name>bug-1:2</short-name>
+ <in-reply-to>commA</in-reply-to>
+ <author></author>
+ <date>...</date>
+ <content-type>text/plain</content-type>
+ <body>comment C</body>
+ </comment>
+ <comment>
+ <uuid>commB</uuid>
+ <short-name>bug-1:3</short-name>
+ <author></author>
+ <date>...</date>
+ <content-type>text/plain</content-type>
+ <body>comment B</body>
+ </comment>
+ </bug>
+ """
+ self.add_comments([comment], **kwargs)
+
+ def add_comments(self, comments, default_parent=None,
+ ignore_missing_references=False):
+ """
+ Convert a raw list of comments to single root comment. If a
+ comment does not specify a parent with .in_reply_to, the
+ parent defaults to .comment_root, but you can specify another
+ default parent via default_parent.
+ """
+ uuid_map = {}
+ if default_parent == None:
+ default_parent = self.comment_root
+ for c in list(self.comments()) + comments:
+ assert c.uuid != None
+ assert c.uuid not in uuid_map
+ uuid_map[c.uuid] = c
+ if c.alt_id != None:
+ uuid_map[c.alt_id] = c
+ uuid_map[None] = self.comment_root
+ if default_parent != self.comment_root:
+ assert default_parent.uuid in uuid_map, default_parent
+ for c in comments:
+ if c.in_reply_to == None \
+ and default_parent.uuid != comment.INVALID_UUID:
+ c.in_reply_to = default_parent.uuid
+ elif c.in_reply_to == comment.INVALID_UUID:
+ c.in_reply_to = None
+ try:
+ parent = uuid_map[c.in_reply_to]
+ except KeyError:
+ if ignore_missing_references == True:
+ print >> sys.stderr, \
+ "Ignoring missing reference to %s" % c.in_reply_to
+ parent = default_parent
+ if parent.uuid != comment.INVALID_UUID:
+ c.in_reply_to = parent.uuid
+ else:
+ raise comment.MissingReference(c)
+ c.bug = self
+ parent.append(c)
+
+ def merge(self, other, accept_changes=True,
+ accept_extra_strings=True, accept_comments=True,
+ change_exception=False):
+ """
+ Merge info from other into this bug. Overrides any attributes
+ in self that are listed in other.explicit_attrs.
+ >>> bugA = Bug(uuid='0123', summary='Need to test Bug.merge()')
+ >>> bugA.date = 'Thu, 01 Jan 1970 00:00:00 +0000'
+ >>> bugA.creator = 'Frank'
+ >>> bugA.extra_strings += ['TAG: very helpful']
+ >>> bugA.extra_strings += ['TAG: favorite']
+ >>> commA = bugA.comment_root.new_reply(body='comment A')
+ >>> commA.uuid = 'uuid-commA'
+ >>> bugB = Bug(uuid='3210', summary='More tests for Bug.merge()')
+ >>> bugB.date = 'Fri, 02 Jan 1970 00:00:00 +0000'
+ >>> bugB.creator = 'John'
+ >>> bugB.explicit_attrs = ['creator', 'summary']
+ >>> bugB.extra_strings += ['TAG: very helpful']
+ >>> bugB.extra_strings += ['TAG: useful']
+ >>> commB = bugB.comment_root.new_reply(body='comment B')
+ >>> commB.uuid = 'uuid-commB'
+ >>> bugA.merge(bugB, accept_changes=False, accept_extra_strings=False,
+ ... accept_comments=False, change_exception=False)
+ >>> print bugA.creator
+ Frank
+ >>> bugA.merge(bugB, accept_changes=False, accept_extra_strings=False,
+ ... accept_comments=False, change_exception=True)
+ Traceback (most recent call last):
+ ...
+ ValueError: Merge would change creator "Frank"->"John" for bug 0123
+ >>> print bugA.creator
+ Frank
+ >>> bugA.merge(bugB, accept_changes=True, accept_extra_strings=False,
+ ... accept_comments=False, change_exception=True)
+ Traceback (most recent call last):
+ ...
+ ValueError: Merge would add extra string "TAG: useful" for bug 0123
+ >>> print bugA.creator
+ John
+ >>> print bugA.extra_strings
+ ['TAG: favorite', 'TAG: very helpful']
+ >>> bugA.merge(bugB, accept_changes=True, accept_extra_strings=True,
+ ... accept_comments=False, change_exception=True)
+ Traceback (most recent call last):
+ ...
+ ValueError: Merge would add comment uuid-commB (alt: None) to bug 0123
+ >>> print bugA.extra_strings
+ ['TAG: favorite', 'TAG: useful', 'TAG: very helpful']
+ >>> bugA.merge(bugB, accept_changes=True, accept_extra_strings=True,
+ ... accept_comments=True, change_exception=True)
+ >>> print bugA.xml(show_comments=True) # doctest: +ELLIPSIS
+ <bug>
+ <uuid>0123</uuid>
+ <short-name>0123</short-name>
+ <severity>minor</severity>
+ <status>open</status>
+ <creator>John</creator>
+ <created>...</created>
+ <summary>More tests for Bug.merge()</summary>
+ <extra-string>TAG: favorite</extra-string>
+ <extra-string>TAG: useful</extra-string>
+ <extra-string>TAG: very helpful</extra-string>
+ <comment>
+ <uuid>uuid-commA</uuid>
+ <short-name>0123:1</short-name>
+ <author></author>
+ <date>...</date>
+ <content-type>text/plain</content-type>
+ <body>comment A</body>
+ </comment>
+ <comment>
+ <uuid>uuid-commB</uuid>
+ <short-name>0123:2</short-name>
+ <author></author>
+ <date>...</date>
+ <content-type>text/plain</content-type>
+ <body>comment B</body>
+ </comment>
+ </bug>
+ """
+ for attr in other.explicit_attrs:
+ old = getattr(self, attr)
+ new = getattr(other, attr)
+ if old != new:
+ if accept_changes == True:
+ setattr(self, attr, new)
+ elif change_exception == True:
+ raise ValueError, \
+ 'Merge would change %s "%s"->"%s" for bug %s' \
+ % (attr, old, new, self.uuid)
+ for estr in other.extra_strings:
+ if not estr in self.extra_strings:
+ if accept_extra_strings == True:
+ self.extra_strings.append(estr)
+ elif change_exception == True:
+ raise ValueError, \
+ 'Merge would add extra string "%s" for bug %s' \
+ % (estr, self.uuid)
+ for o_comm in other.comments():
+ try:
+ s_comm = self.comment_root.comment_from_uuid(o_comm.uuid)
+ except KeyError, e:
+ try:
+ s_comm = self.comment_root.comment_from_uuid(o_comm.alt_id)
+ except KeyError, e:
+ s_comm = None
+ if s_comm == None:
+ if accept_comments == True:
+ o_comm_copy = copy.copy(o_comm)
+ o_comm_copy.bug = self
+ self.comment_root.add_reply(o_comm_copy)
+ elif change_exception == True:
+ raise ValueError, \
+ 'Merge would add comment %s (alt: %s) to bug %s' \
+ % (o_comm.uuid, o_comm.alt_id, self.uuid)
+ else:
+ s_comm.merge(o_comm, accept_changes=accept_changes,
+ accept_extra_strings=accept_extra_strings,
+ change_exception=change_exception)
def string(self, shortlist=False, show_comments=False):
if self.bugdir == None:
@@ -493,8 +725,8 @@ class Bug(settings_object.SavedSettingsObject):
return self.comment_root.comment_from_shortname(shortname,
*args, **kwargs)
- def comment_from_uuid(self, uuid):
- return self.comment_root.comment_from_uuid(uuid)
+ def comment_from_uuid(self, uuid, *args, **kwargs):
+ return self.comment_root.comment_from_uuid(uuid, *args, **kwargs)
def comment_shortnames(self, shortname=None):
"""
diff --git a/libbe/bugdir.py b/libbe/bugdir.py
index 675b744..301ceb6 100644
--- a/libbe/bugdir.py
+++ b/libbe/bugdir.py
@@ -115,7 +115,7 @@ class BugDir (list, settings_object.SavedSettingsObject):
all bugs/comments/etc. that have been loaded into memory. If
you've been living in memory and want to move to
.sync_with_disk==True, but you're not sure if anything has been
- changed in memory, a call to save() immediately before the
+ changed in memory, a call to .save() immediately before the
.set_sync_with_disk(True) call is a safe move.
Regardless of .sync_with_disk, a call to .save() will write out
@@ -239,7 +239,7 @@ settings easy. Don't set this attribute. Set .vcs instead, and
map = {}
for bug in self:
map[bug.uuid] = bug
- for uuid in self.list_uuids():
+ for uuid in self.uuids():
if uuid not in map:
map[uuid] = None
self._bug_map_value = map # ._bug_map_value used by @local_property
@@ -483,7 +483,7 @@ settings easy. Don't set this attribute. Set .vcs instead, and
if self.sync_with_disk == False:
raise DiskAccessRequired("load all bugs")
self._clear_bugs()
- for uuid in self.list_uuids():
+ for uuid in self.uuids():
self._load_bug(uuid)
def save(self):
@@ -550,7 +550,7 @@ settings easy. Don't set this attribute. Set .vcs instead, and
# methods for managing bugs
- def list_uuids(self):
+ def uuids(self):
uuids = []
if self.sync_with_disk == True and os.path.exists(self.get_path()):
# list the uuids on disk
@@ -651,7 +651,7 @@ class SimpleBugDir (BugDir):
"""
For testing. Set sync_with_disk==False for a memory-only bugdir.
>>> bugdir = SimpleBugDir()
- >>> uuids = list(bugdir.list_uuids())
+ >>> uuids = list(bugdir.uuids())
>>> uuids.sort()
>>> print uuids
['a', 'b']
@@ -741,7 +741,7 @@ class BugDirTestCase(unittest.TestCase):
self.bugdir.new_bug(uuid="c", summary="Praying mantis")
length = len(self.bugdir)
self.failUnless(length == 3, "%d != 3 bugs" % length)
- uuids = list(self.bugdir.list_uuids())
+ uuids = list(self.bugdir.uuids())
self.failUnless(len(uuids) == 3, "%d != 3 uuids" % len(uuids))
self.failUnless(uuids == ["a","b","c"], str(uuids))
bugA = self.bugdir.bug_from_uuid("a")
diff --git a/libbe/comment.py b/libbe/comment.py
index 5cc43c4..c5f1cc9 100644
--- a/libbe/comment.py
+++ b/libbe/comment.py
@@ -65,53 +65,6 @@ class DiskAccessRequired (Exception):
INVALID_UUID = "!!~~\n INVALID-UUID \n~~!!"
-def list_to_root(comments, bug, root=None,
- ignore_missing_references=False):
- """
- Convert a raw list of comments to single root comment. We use a
- dummy root comment by default, because there can be several
- comment threads rooted on the same parent bug. To simplify
- comment interaction, we condense these threads into a single
- thread with a Comment dummy root. Can also be used to append
- a list of subcomments to a non-dummy root comment, so long as
- all the new comments are descendants of the root comment.
-
- No Comment method should use the dummy comment.
- """
- root_comments = []
- uuid_map = {}
- for comment in comments:
- assert comment.uuid != None
- uuid_map[comment.uuid] = comment
- for comment in comments:
- if comment.alt_id != None and comment.alt_id not in uuid_map:
- uuid_map[comment.alt_id] = comment
- if root == None:
- root = Comment(bug, uuid=INVALID_UUID)
- else:
- uuid_map[root.uuid] = root
- for comm in comments:
- if comm.in_reply_to == INVALID_UUID:
- comm.in_reply_to = None
- rep = comm.in_reply_to
- if rep == None or rep == bug.uuid:
- root_comments.append(comm)
- else:
- parentUUID = comm.in_reply_to
- try:
- parent = uuid_map[parentUUID]
- parent.add_reply(comm)
- except KeyError, e:
- if ignore_missing_references == True:
- print >> sys.stderr, \
- "Ignoring missing reference to %s" % parentUUID
- comm.in_reply_to = None
- root_comments.append(comm)
- else:
- raise MissingReference(comm)
- root.extend(root_comments)
- return root
-
def loadComments(bug, load_full=False):
"""
Set load_full=True when you want to load the comment completely
@@ -132,7 +85,9 @@ def loadComments(bug, load_full=False):
comm.load_settings()
dummy = comm.body # force the body to load
comments.append(comm)
- return list_to_root(comments, bug)
+ bug.comment_root = Comment(bug, uuid=INVALID_UUID)
+ bug.add_comments(comments)
+ return bug.comment_root
def saveComments(bug):
if bug.sync_with_disk == False:
@@ -344,7 +299,7 @@ class Comment(Tree, settings_object.SavedSettingsObject):
if v != None:
lines.append(' <%s>%s</%s>' % (k,xml.sax.saxutils.escape(v),k))
for estr in self.extra_strings:
- lines.append(' <extra-string>%s</extra-string>\n' % estr)
+ lines.append(' <extra-string>%s</extra-string>' % estr)
lines.append('</comment>')
istring = ' '*indent
sep = '\n' + istring
@@ -362,6 +317,8 @@ class Comment(Tree, settings_object.SavedSettingsObject):
>>> xml = commA.xml(shortname="com-1")
>>> commB = Comment()
>>> commB.from_xml(xml, verbose=True)
+ >>> commB.explicit_attrs
+ ['author', 'date', 'content_type', 'body', 'alt_id']
>>> commB.xml(shortname="com-1") == xml
False
>>> commB.uuid = commB.alt_id
@@ -371,12 +328,16 @@ class Comment(Tree, settings_object.SavedSettingsObject):
"""
if type(xml_string) == types.UnicodeType:
xml_string = xml_string.strip().encode('unicode_escape')
- comment = ElementTree.XML(xml_string)
+ if hasattr(xml_string, 'getchildren'): # already an ElementTree Element
+ comment = xml_string
+ else:
+ comment = ElementTree.XML(xml_string)
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']
+ self.explicit_attrs = []
uuid = None
body = None
estrs = []
@@ -392,19 +353,21 @@ class Comment(Tree, settings_object.SavedSettingsObject):
if child.tag == 'uuid':
uuid = text
continue # don't set the comment's uuid tag.
- if child.tag == 'body':
+ elif child.tag == 'body':
body = text
+ self.explicit_attrs.append(child.tag)
continue # don't set the comment's body yet.
- if child.tag == 'extra-string':
+ elif child.tag == 'extra-string':
estrs.append(text)
continue # don't set the comment's extra_string yet.
- else:
- attr_name = child.tag.replace('-','_')
+ attr_name = child.tag.replace('-','_')
+ self.explicit_attrs.append(attr_name)
setattr(self, attr_name, text)
elif verbose == True:
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]:
+ if uuid != self.uuid and self.alt_id == None:
+ self.explicit_attrs.append('alt_id')
self.alt_id = uuid
if body != None:
if self.content_type.startswith('text/'):
@@ -413,6 +376,78 @@ class Comment(Tree, settings_object.SavedSettingsObject):
self.body = base64.decodestring(body)
self.extra_strings = estrs
+ def merge(self, other, accept_changes=True,
+ accept_extra_strings=True, change_exception=False):
+ """
+ Merge info from other into this comment. Overrides any
+ attributes in self that are listed in other.explicit_attrs.
+ >>> commA = Comment(bug=None, body='Some insightful remarks')
+ >>> commA.uuid = '0123'
+ >>> commA.date = 'Thu, 01 Jan 1970 00:00:00 +0000'
+ >>> commA.author = 'Frank'
+ >>> commA.extra_strings += ['TAG: very helpful']
+ >>> commA.extra_strings += ['TAG: favorite']
+ >>> commB = Comment(bug=None, body='More insightful remarks')
+ >>> commB.uuid = '3210'
+ >>> commB.date = 'Fri, 02 Jan 1970 00:00:00 +0000'
+ >>> commB.author = 'John'
+ >>> commB.explicit_attrs = ['author', 'body']
+ >>> commB.extra_strings += ['TAG: very helpful']
+ >>> commB.extra_strings += ['TAG: useful']
+ >>> commA.merge(commB, accept_changes=False,
+ ... accept_extra_strings=False, change_exception=False)
+ >>> commA.merge(commB, accept_changes=False,
+ ... accept_extra_strings=False, change_exception=True)
+ Traceback (most recent call last):
+ ...
+ ValueError: Merge would change author "Frank"->"John" for comment 0123
+ >>> commA.merge(commB, accept_changes=True,
+ ... accept_extra_strings=False, change_exception=True)
+ Traceback (most recent call last):
+ ...
+ ValueError: Merge would add extra string "TAG: useful" to comment 0123
+ >>> print commA.author
+ John
+ >>> print commA.extra_strings
+ ['TAG: favorite', 'TAG: very helpful']
+ >>> commA.merge(commB, accept_changes=True,
+ ... accept_extra_strings=True, change_exception=True)
+ >>> print commA.extra_strings
+ ['TAG: favorite', 'TAG: useful', 'TAG: very helpful']
+ >>> print commA.xml()
+ <comment>
+ <uuid>0123</uuid>
+ <short-name>0123</short-name>
+ <author>John</author>
+ <date>Thu, 01 Jan 1970 00:00:00 +0000</date>
+ <content-type>text/plain</content-type>
+ <body>More insightful remarks</body>
+ <extra-string>TAG: favorite</extra-string>
+ <extra-string>TAG: useful</extra-string>
+ <extra-string>TAG: very helpful</extra-string>
+ </comment>
+ """
+ for attr in other.explicit_attrs:
+ old = getattr(self, attr)
+ new = getattr(other, attr)
+ if old != new:
+ if accept_changes == True:
+ setattr(self, attr, new)
+ elif change_exception == True:
+ raise ValueError, \
+ 'Merge would change %s "%s"->"%s" for comment %s' \
+ % (attr, old, new, self.uuid)
+ if self.alt_id == self.uuid:
+ self.alt_id = None
+ for estr in other.extra_strings:
+ if not estr in self.extra_strings:
+ if accept_extra_strings == True:
+ self.extra_strings.append(estr)
+ elif change_exception == True:
+ raise ValueError, \
+ 'Merge would add extra string "%s" to comment %s' \
+ % (estr, self.uuid)
+
def string(self, indent=0, shortname=None):
"""
>>> comm = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n")
@@ -674,7 +709,7 @@ class Comment(Tree, settings_object.SavedSettingsObject):
raise InvalidShortname(comment_shortname,
list(self.comment_shortnames(*args, **kwargs)))
- def comment_from_uuid(self, uuid):
+ def comment_from_uuid(self, uuid, match_alt_id=True):
"""
Use a comment shortname to look up a comment.
>>> a = Comment(bug=None, uuid="a")
@@ -684,13 +719,24 @@ class Comment(Tree, settings_object.SavedSettingsObject):
>>> c.uuid = "c"
>>> d = a.new_reply()
>>> d.uuid = "d"
+ >>> d.alt_id = "d-alt"
>>> comm = a.comment_from_uuid("d")
>>> id(comm) == id(d)
True
+ >>> comm = a.comment_from_uuid("d-alt")
+ >>> id(comm) == id(d)
+ True
+ >>> comm = a.comment_from_uuid(None, match_alt_id=False)
+ Traceback (most recent call last):
+ ...
+ KeyError: None
"""
for comment in self.traverse():
if comment.uuid == uuid:
return comment
+ if match_alt_id == True and uuid != None \
+ and comment.alt_id == uuid:
+ return comment
raise KeyError(uuid)
def cmp_attr(comment_1, comment_2, attr, invert=False):
diff --git a/libbe/diff.py b/libbe/diff.py
index cce3b0f..6e830c6 100644
--- a/libbe/diff.py
+++ b/libbe/diff.py
@@ -207,7 +207,7 @@ class Diff (object):
added = []
removed = []
modified = []
- for uuid in self.new_bugdir.list_uuids():
+ for uuid in self.new_bugdir.uuids():
new_bug = self.new_bugdir.bug_from_uuid(uuid)
try:
old_bug = self.old_bugdir.bug_from_uuid(uuid)
@@ -220,7 +220,7 @@ class Diff (object):
new_bug.load_comments()
if old_bug != new_bug:
modified.append((old_bug, new_bug))
- for uuid in self.old_bugdir.list_uuids():
+ for uuid in self.old_bugdir.uuids():
if not self.new_bugdir.has_bug(uuid):
old_bug = self.old_bugdir.bug_from_uuid(uuid)
removed.append(old_bug)
diff --git a/libbe/subproc.py b/libbe/subproc.py
index 3e58271..fe88206 100644
--- a/libbe/subproc.py
+++ b/libbe/subproc.py
@@ -96,7 +96,7 @@ class Pipe (object):
>>> p.statuses
[1, 0]
>>> p.stderrs # doctest: +ELLIPSIS
- ["find: `...': Permission denied\\n...", '']
+ [...find: ...: Permission denied..., '']
"""
def __init__(self, cmds, stdin=None):
# spawn processes
diff --git a/test.py b/test.py
index 57091c7..81674cf 100644
--- a/test.py
+++ b/test.py
@@ -27,7 +27,10 @@ if len(sys.argv) > 1:
print "Module \"%s\" has no test suite" % submodname
mod = plugin.get_plugin("becommands", submodname)
if mod is not None:
- suite.addTest(doctest.DocTestSuite(mod))
+ if hasattr(mod, "suite"):
+ suite.addTest(mod.suite)
+ else:
+ suite.addTest(doctest.DocTestSuite(mod))
match = True
if not match:
print "No modules match \"%s\"" % submodname