diff options
-rw-r--r-- | .be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/4068c833-0c06-475e-8b7e-6701bc416dee/body | 28 | ||||
-rw-r--r-- | .be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/4068c833-0c06-475e-8b7e-6701bc416dee/values | 11 | ||||
-rw-r--r-- | .be/bugs/56506b73-36cc-4e32-a578-258a219edba8/values | 2 | ||||
-rw-r--r-- | becommands/comment.py | 2 | ||||
-rw-r--r-- | becommands/import_xml.py | 215 | ||||
-rw-r--r-- | becommands/target.py | 2 | ||||
-rwxr-xr-x | interfaces/email/interactive/be-handle-mail | 2 | ||||
-rw-r--r-- | libbe/bug.py | 262 | ||||
-rw-r--r-- | libbe/bugdir.py | 12 | ||||
-rw-r--r-- | libbe/comment.py | 158 | ||||
-rw-r--r-- | libbe/diff.py | 4 | ||||
-rw-r--r-- | libbe/subproc.py | 2 | ||||
-rw-r--r-- | test.py | 5 |
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 @@ -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 |