From 3e050db00d2ffa2c011efc4d9b47d8edeac5c43c Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 28 Nov 2009 07:41:13 -0500 Subject: Added Bug.merge() and Comment.merge(). Added *.explicit_attrs list creation to Bug and Comment.from_xml(). Added match_alt_id keyword argumennt to .comment_from_uuid(). Removed extra enline following '' tag in Bug and Comment.xml(). --- libbe/bug.py | 119 ++++++++++++++++++++++++++++++++++++++++++++++++++----- libbe/comment.py | 82 ++++++++++++++++++++++++++++++++++---- 2 files changed, 185 insertions(+), 16 deletions(-) diff --git a/libbe/bug.py b/libbe/bug.py index fecb9b7..23d5488 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 @@ -302,7 +303,7 @@ class Bug(settings_object.SavedSettingsObject): if v is not None: lines.append(' <%s>%s' % (k,xml.sax.saxutils.escape(v),k)) for estr in self.extra_strings: - lines.append(' %s\n' % estr) + lines.append(' %s' % estr) if show_comments == True: comout = self.comment_root.xml_thread(indent=indent+2, auto_name_map=True, @@ -332,6 +333,8 @@ class Bug(settings_object.SavedSettingsObject): >>> bugB.uuid = bugB.alt_id >>> bugB.xml(shortname="bug-1") == xml True + >>> bugB.explicit_attrs # doctest: +NORMALIZE_WHITESPACE + ['uuid', 'severity', 'status', 'creator', 'created', 'summary'] """ if type(xml_string) == types.UnicodeType: xml_string = xml_string.strip().encode('unicode_escape') @@ -340,8 +343,9 @@ class Bug(settings_object.SavedSettingsObject): raise utility.InvalidXML( \ 'bug', bug, 'root element must be ') tags=['uuid','short-name','severity','status','assigned','target', - 'reporter', 'creator', 'created', 'summary', 'extra-string', + 'reporter', 'creator','created','summary','extra-string', 'comment'] + self.explicit_attrs = [] uuid = None estrs = [] for child in bug.getchildren(): @@ -353,23 +357,120 @@ class Bug(settings_object.SavedSettingsObject): 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 + def merge(self, other, allow_changes=True, allow_new_comments=True): + """ + 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, allow_changes=False) + Traceback (most recent call last): + ... + ValueError: Merge would change creator "Frank"->"John" for bug 0123 + >>> bugA.merge(bugB, allow_new_comments=False) + Traceback (most recent call last): + ... + ValueError: Merge would add comment uuid-commB (alt: None) to bug 0123 + >>> bugA.merge(bugB) + >>> print bugA.xml(show_comments=True) # doctest: +ELLIPSIS + + 0123 + 0123 + minor + open + John + ... + More tests for Bug.merge() + TAG: favorite + TAG: useful + TAG: very helpful + + uuid-commA + 0123:1 + + ... + text/plain + comment A + + + uuid-commB + 0123:2 + + ... + text/plain + comment B + + + """ + for attr in other.explicit_attrs: + old = getattr(self, attr) + new = getattr(other, attr) + if old != new: + if allow_changes == True: + setattr(self, attr, new) + else: + raise ValueError, \ + 'Merge would change %s "%s"->"%s" for bug %s' \ + % (attr, old, new, self.uuid) + if allow_changes == False and len(other.extra_strings) > 0: + raise ValueError, \ + 'Merge would change extra_strings for bug %s' % self.uuid + for estr in other.extra_strings: + if not estr in self.extra_strings: + self.extra_strings.append(estr) + import sys + for o_comm in other.comments(): + s_comm = None + 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: + pass + if s_comm == None: + if allow_new_comments == False: + raise ValueError, \ + 'Merge would add comment %s (alt: %s) to bug %s' \ + % (o_comm.uuid, o_comm.alt_id, self.uuid) + o_comm_copy = copy.copy(o_comm) + o_comm_copy.bug = self + print >> sys.stderr, "add comment %s" % o_comm.uuid + self.comment_root.add_reply(o_comm_copy) + else: + print >> sys.stderr, "merge comment %s into %s" % (o_comm.uuid, s_comm.uuid) + s_comm.merge(o_comm, allow_changes=allow_changes) + def string(self, shortlist=False, show_comments=False): if self.bugdir == None: shortname = self.uuid @@ -493,8 +594,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/comment.py b/libbe/comment.py index 5cc43c4..1adb6f4 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -344,7 +344,7 @@ class Comment(Tree, settings_object.SavedSettingsObject): if v != None: lines.append(' <%s>%s' % (k,xml.sax.saxutils.escape(v),k)) for estr in self.extra_strings: - lines.append(' %s\n' % estr) + lines.append(' %s' % estr) lines.append('') istring = ' '*indent sep = '\n' + istring @@ -362,6 +362,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 @@ -377,6 +379,7 @@ class Comment(Tree, settings_object.SavedSettingsObject): 'comment', comment, 'root element must be ') tags=['uuid','alt-id','in-reply-to','author','date','content-type', 'body','extra-string'] + self.explicit_attrs = [] uuid = None body = None estrs = [] @@ -392,19 +395,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 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 +418,58 @@ class Comment(Tree, settings_object.SavedSettingsObject): self.body = base64.decodestring(body) self.extra_strings = estrs + def merge(self, other, allow_changes=True): + """ + 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, allow_changes=False) + Traceback (most recent call last): + ... + ValueError: Merge would change author "Frank"->"John" for comment 0123 + >>> commA.merge(commB) + >>> print commA.xml() + + 0123 + 0123 + John + Thu, 01 Jan 1970 00:00:00 +0000 + text/plain + More insightful remarks + TAG: favorite + TAG: useful + TAG: very helpful + + """ + for attr in other.explicit_attrs: + old = getattr(self, attr) + new = getattr(other, attr) + if old != new: + if allow_changes == True: + setattr(self, attr, new) + else: + raise ValueError, \ + 'Merge would change %s "%s"->"%s" for comment %s' \ + % (attr, old, new, self.uuid) + if allow_changes == False and len(other.extra_strings) > 0: + raise ValueError, \ + 'Merge would change extra_strings for comment %s' % self.uuid + for estr in other.extra_strings: + if not estr in self.extra_strings: + self.extra_strings.append(estr) + def string(self, indent=0, shortname=None): """ >>> comm = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n") @@ -674,7 +731,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 +741,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): -- cgit From c90ed61e7deb594edf3707850f2d3a87601a581b Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 28 Nov 2009 20:24:19 -0500 Subject: BugDir.list_uuids() -> BugDir.uuids() --- becommands/comment.py | 2 +- becommands/target.py | 2 +- libbe/bugdir.py | 10 +++++----- libbe/diff.py | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) 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/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/libbe/bugdir.py b/libbe/bugdir.py index 675b744..3d07754 100644 --- a/libbe/bugdir.py +++ b/libbe/bugdir.py @@ -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/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) -- cgit From 7e95956f5088346807a233c63f5bc25436550ef8 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 28 Nov 2009 20:27:53 -0500 Subject: test.py now uses unittest.TestSuite 'suite' in becommands if present. Such 'suite' instances have been required for libbe submodules. This will allow becommands to test themselves more thoroughly than they could with only doctests. --- test.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 -- cgit From 832843d26eed9023f4cf4fc431527c63ca1d533d Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 28 Nov 2009 22:07:16 -0500 Subject: Added comment import to Bug.from_xml(). This is a pretty critical feature, dunno how I missed it before. I also added a little check to both Bug and Comment.from_xml() so that xml_string can take an ElementTree Element as well as the usual raw string/unicode. This avoids repeated string <-> Element conversions. Added Bug.add_comment() which handles the addition of a Comment instance, matching .in_reply_to, checking .uuid uniqueness, etc. --- libbe/bug.py | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++------ libbe/comment.py | 5 ++- 2 files changed, 93 insertions(+), 10 deletions(-) diff --git a/libbe/bug.py b/libbe/bug.py index 23d5488..5f2cf54 100644 --- a/libbe/bug.py +++ b/libbe/bug.py @@ -325,32 +325,44 @@ 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 - ['uuid', 'severity', 'status', 'creator', 'created', 'summary'] + ['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 ') 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 = [] for child in bug.getchildren(): if child.tag == 'short-name': pass + elif child.tag == 'comment': + comm = comment.Comment(bug=self) + comm.from_xml(child) + self.add_comment(comm) + continue elif child.tag in tags: if child.text == None or len(child.text) == 0: text = settings_object.EMPTY @@ -374,6 +386,75 @@ class Bug(settings_object.SavedSettingsObject): self.alt_id = uuid self.extra_strings = estrs + def add_comment(self, new_comment): + """ + 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 + + 0123 + bug-1 + minor + open + Jack + ... + Need to test Bug.add_comment() + + commA + bug-1:1 + + ... + text/plain + comment A + + + commC + bug-1:2 + commA + + ... + text/plain + comment C + + + commB + bug-1:3 + + ... + text/plain + comment B + + + """ + uuid_map = {} + for c in self.comments(): + uuid_map[c.uuid] = c + if c.alt_id != None: + uuid_map[c.alt_id] = c + assert new_comment.uuid not in uuid_map + if new_comment.alt_id != None: + assert new_comment.alt_id not in uuid_map + if new_comment.in_reply_to == comment.INVALID_UUID: + new_comment.in_reply_to = None + if new_comment.in_reply_to == None: + parent = self.comment_root + else: + parent = uuid_map[new_comment.in_reply_to] + new_comment.bug = self + parent.append(new_comment) + def merge(self, other, allow_changes=True, allow_new_comments=True): """ Merge info from other into this bug. Overrides any attributes @@ -450,14 +531,13 @@ class Bug(settings_object.SavedSettingsObject): self.extra_strings.append(estr) import sys for o_comm in other.comments(): - s_comm = None 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: - pass + s_comm = None if s_comm == None: if allow_new_comments == False: raise ValueError, \ diff --git a/libbe/comment.py b/libbe/comment.py index 1adb6f4..e3dfea0 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -373,7 +373,10 @@ 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 ') -- cgit From 759c69d8c8a4bbd7ba9c42bb3a813cd0d06a52b7 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sun, 29 Nov 2009 03:19:30 -0500 Subject: Moved comment.list_to_root() to Bug.add_comments() with some cleanups. This makes Bug.add_comment simpler. Also makes Bug.from_xml() more robust, since it no longer depends on the order in which the XML file lists the comments. The previous Bug.from_xml() would have choked on B A A because when B was being added, the referenced A hadn't yet been noticed. --- libbe/bug.py | 59 ++++++++++++++++++++++++++++++++++++++++---------------- libbe/comment.py | 51 +++--------------------------------------------- 2 files changed, 45 insertions(+), 65 deletions(-) diff --git a/libbe/bug.py b/libbe/bug.py index 5f2cf54..d3dbe2d 100644 --- a/libbe/bug.py +++ b/libbe/bug.py @@ -228,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): @@ -355,13 +355,14 @@ class Bug(settings_object.SavedSettingsObject): 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) - self.add_comment(comm) + comments.append(comm) continue elif child.tag in tags: if child.text == None or len(child.text) == 0: @@ -385,8 +386,9 @@ class Bug(settings_object.SavedSettingsObject): 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, new_comment): + def add_comment(self, comment, *args, **kwargs): """ Add a comment too the current bug, under the parent specified by comment.in_reply_to. @@ -438,22 +440,47 @@ class Bug(settings_object.SavedSettingsObject): """ + 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 = {} - for c in self.comments(): + 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 - assert new_comment.uuid not in uuid_map - if new_comment.alt_id != None: - assert new_comment.alt_id not in uuid_map - if new_comment.in_reply_to == comment.INVALID_UUID: - new_comment.in_reply_to = None - if new_comment.in_reply_to == None: - parent = self.comment_root - else: - parent = uuid_map[new_comment.in_reply_to] - new_comment.bug = self - parent.append(new_comment) + 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, allow_changes=True, allow_new_comments=True): """ @@ -545,10 +572,8 @@ class Bug(settings_object.SavedSettingsObject): % (o_comm.uuid, o_comm.alt_id, self.uuid) o_comm_copy = copy.copy(o_comm) o_comm_copy.bug = self - print >> sys.stderr, "add comment %s" % o_comm.uuid self.comment_root.add_reply(o_comm_copy) else: - print >> sys.stderr, "merge comment %s into %s" % (o_comm.uuid, s_comm.uuid) s_comm.merge(o_comm, allow_changes=allow_changes) def string(self, shortlist=False, show_comments=False): diff --git a/libbe/comment.py b/libbe/comment.py index e3dfea0..45134e0 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: -- cgit From ff2475e98fe1726f8b1af1b3dc30746cc4e01071 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 30 Nov 2009 05:05:49 -0500 Subject: Minor BugDir docstring typo correction. --- libbe/bugdir.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libbe/bugdir.py b/libbe/bugdir.py index 3d07754..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 -- cgit From 19cea054def7997bb13ecc77269b7b612f658d16 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 30 Nov 2009 06:05:03 -0500 Subject: Changed Bug and Comment.merge() kwargs. The old allow_changes and allow_new_comments didn't have separate handling for extra_strings, like import_xml will need. It also didn't have a way to specify what to do if an illegal change occurs. Sometimes you'll want to raise an exception, but sometimes you'll want to ?silently? ignore the change. --- libbe/bug.py | 58 ++++++++++++++++++++++++++++++++++++++++---------------- libbe/comment.py | 36 ++++++++++++++++++++++++++--------- 2 files changed, 69 insertions(+), 25 deletions(-) diff --git a/libbe/bug.py b/libbe/bug.py index d3dbe2d..897d841 100644 --- a/libbe/bug.py +++ b/libbe/bug.py @@ -482,7 +482,9 @@ class Bug(settings_object.SavedSettingsObject): c.bug = self parent.append(c) - def merge(self, other, allow_changes=True, allow_new_comments=True): + 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. @@ -501,15 +503,35 @@ class Bug(settings_object.SavedSettingsObject): >>> bugB.extra_strings += ['TAG: useful'] >>> commB = bugB.comment_root.new_reply(body='comment B') >>> commB.uuid = 'uuid-commB' - >>> bugA.merge(bugB, allow_changes=False) + >>> 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 - >>> bugA.merge(bugB, allow_new_comments=False) + >>> 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 - >>> bugA.merge(bugB) + >>> 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 0123 @@ -544,19 +566,20 @@ class Bug(settings_object.SavedSettingsObject): old = getattr(self, attr) new = getattr(other, attr) if old != new: - if allow_changes == True: + if accept_changes == True: setattr(self, attr, new) - else: + elif change_exception == True: raise ValueError, \ 'Merge would change %s "%s"->"%s" for bug %s' \ % (attr, old, new, self.uuid) - if allow_changes == False and len(other.extra_strings) > 0: - raise ValueError, \ - 'Merge would change extra_strings for bug %s' % self.uuid for estr in other.extra_strings: if not estr in self.extra_strings: - self.extra_strings.append(estr) - import sys + 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) @@ -566,15 +589,18 @@ class Bug(settings_object.SavedSettingsObject): except KeyError, e: s_comm = None if s_comm == None: - if allow_new_comments == False: + 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) - o_comm_copy = copy.copy(o_comm) - o_comm_copy.bug = self - self.comment_root.add_reply(o_comm_copy) else: - s_comm.merge(o_comm, allow_changes=allow_changes) + 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: diff --git a/libbe/comment.py b/libbe/comment.py index 45134e0..9502adf 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -376,7 +376,8 @@ class Comment(Tree, settings_object.SavedSettingsObject): self.body = base64.decodestring(body) self.extra_strings = estrs - def merge(self, other, allow_changes=True): + 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. @@ -393,11 +394,26 @@ class Comment(Tree, settings_object.SavedSettingsObject): >>> commB.explicit_attrs = ['author', 'body'] >>> commB.extra_strings += ['TAG: very helpful'] >>> commB.extra_strings += ['TAG: useful'] - >>> commA.merge(commB, allow_changes=False) + >>> 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) + >>> 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() 0123 @@ -415,18 +431,20 @@ class Comment(Tree, settings_object.SavedSettingsObject): old = getattr(self, attr) new = getattr(other, attr) if old != new: - if allow_changes == True: + if accept_changes == True: setattr(self, attr, new) - else: + elif change_exception == True: raise ValueError, \ 'Merge would change %s "%s"->"%s" for comment %s' \ % (attr, old, new, self.uuid) - if allow_changes == False and len(other.extra_strings) > 0: - raise ValueError, \ - 'Merge would change extra_strings for comment %s' % self.uuid for estr in other.extra_strings: if not estr in self.extra_strings: - self.extra_strings.append(estr) + 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): """ -- cgit From c1fc4595171fa6eec802eb65a0fde0b53878a077 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 30 Nov 2009 06:26:49 -0500 Subject: Avoid redundant Comment.alt_ids --- libbe/comment.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libbe/comment.py b/libbe/comment.py index 9502adf..c5f1cc9 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -366,7 +366,7 @@ class Comment(Tree, settings_object.SavedSettingsObject): elif verbose == True: print >> sys.stderr, 'Ignoring unknown tag %s in %s' \ % (child.tag, comment.tag) - if self.alt_id == None: + if uuid != self.uuid and self.alt_id == None: self.explicit_attrs.append('alt_id') self.alt_id = uuid if body != None: @@ -437,6 +437,8 @@ class Comment(Tree, settings_object.SavedSettingsObject): 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: -- cgit From c903b668c13413f380f99720c01162b7fe90ec6d Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 30 Nov 2009 06:35:29 -0500 Subject: Fixed import_xml.py to live up to its longhelp description. Also added LonghelpTestCase to prove it. --- becommands/import_xml.py | 215 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 188 insertions(+), 27 deletions(-) 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 = """ + + + b + fixed + a test bug + don't forget your towel + watch out for flying dolphins + + c1 + So long + + + c3 + Jed + And thanks + + + + """ + 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()]) -- cgit From ef748431936015a9d914255db0dd20f2f8e1c426 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 30 Nov 2009 06:36:38 -0500 Subject: Added some thoughts on import_xml merge algorithms --- .../4068c833-0c06-475e-8b7e-6701bc416dee/body | 28 ++++++++++++++++++++++ .../4068c833-0c06-475e-8b7e-6701bc416dee/values | 11 +++++++++ 2 files changed, 39 insertions(+) create mode 100644 .be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/4068c833-0c06-475e-8b7e-6701bc416dee/body create mode 100644 .be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/4068c833-0c06-475e-8b7e-6701bc416dee/values 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 + + +Content-type: text/plain + + +Date: Sun, 29 Nov 2009 01:19:05 +0000 + + +In-reply-to: 0a995544-20dc-42a6-8d3f-348ebbc8921e + -- cgit From 8773e0e95758539addbfbcd11e5f37a2648c6e58 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 30 Nov 2009 06:26:09 -0500 Subject: Generalized doctest string in subproc.py --- libbe/subproc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 -- cgit From 2156616c3dab8207a933295bfbc9d125dac4bc34 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 30 Nov 2009 06:28:01 -0500 Subject: be-handle-mail uses more conservative --add-only for be-bugs:xml --- interfaces/email/interactive/be-handle-mail | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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): -- cgit From 13784e6067b652e4fe08e488fdc4baabc37f24ef Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 30 Nov 2009 06:29:04 -0500 Subject: Mark bug 565 as fixed: be email-bugs for bug submission from bzr-less users --- .be/bugs/56506b73-36cc-4e32-a578-258a219edba8/values | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 severity: minor -status: open +status: fixed summary: be email-bugs for bug submission from bzr-less users -- cgit