1 # Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc.
2 # Gianluca Montecchi <gian@grys.it>
3 # W. Trevor King <wking@drexel.edu>
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License along
16 # with this program; if not, write to the Free Software Foundation, Inc.,
17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19 """Compare two bug trees."""
24 from libbe import bugdir, bug, settings_object, tree
25 from libbe.utility import time_to_str
26 if libbe.TESTING == True:
30 class SubscriptionType (tree.Tree):
32 Trees of subscription types to allow users to select exactly what
33 notifications they want to subscribe to.
35 def __init__(self, type_name, *args, **kwargs):
36 tree.Tree.__init__(self, *args, **kwargs)
41 return "<SubscriptionType: %s>" % str(self)
42 def string_tree(self, indent=0):
44 for depth,node in self.thread():
45 lines.append("%s%s" % (" "*(indent+2*depth), node))
46 return "\n".join(lines)
48 BUGDIR_TYPE_NEW = SubscriptionType("new")
49 BUGDIR_TYPE_ALL = SubscriptionType("all", [BUGDIR_TYPE_NEW])
51 # same name as BUGDIR_TYPE_ALL for consistency
52 BUG_TYPE_ALL = SubscriptionType(str(BUGDIR_TYPE_ALL))
54 INVALID_TYPE = SubscriptionType("INVALID")
56 class InvalidType (ValueError):
57 def __init__(self, type_name, type_root):
58 msg = "Invalid type %s for tree:\n%s" \
59 % (type_name, type_root.string_tree(4))
60 ValueError.__init__(self, msg)
61 self.type_name = type_name
62 self.type_root = type_root
64 def type_from_name(name, type_root, default=None, default_ok=False):
65 if name == str(type_root):
67 for t in type_root.traverse():
72 raise InvalidType(name, type_root)
76 class DiffTree (tree.Tree):
78 A tree holding difference data for easy report generation.
79 >>> bugdir = DiffTree("bugdir")
80 >>> bdsettings = DiffTree("settings", data="target: None -> 1.0")
81 >>> bugdir.append(bdsettings)
82 >>> bugs = DiffTree("bugs", "bug-count: 5 -> 6")
83 >>> bugdir.append(bugs)
84 >>> new = DiffTree("new", "new bugs: ABC, DEF")
86 >>> rem = DiffTree("rem", "removed bugs: RST, UVW")
88 >>> print bugdir.report_string()
92 removed bugs: RST, UVW
93 >>> print "\\n".join(bugdir.paths())
99 >>> bugdir.child_by_path("/") == bugdir
101 >>> bugdir.child_by_path("/bugs") == bugs
103 >>> bugdir.child_by_path("/bugs/rem") == rem
105 >>> bugdir.child_by_path("bugdir") == bugdir
107 >>> bugdir.child_by_path("bugdir/") == bugdir
109 >>> bugdir.child_by_path("bugdir/bugs") == bugs
111 >>> bugdir.child_by_path("/bugs").masked = True
112 >>> print bugdir.report_string()
115 def __init__(self, name, data=None, data_part_fn=str,
116 requires_children=False, masked=False):
117 tree.Tree.__init__(self)
120 self.data_part_fn = data_part_fn
121 self.requires_children = requires_children
123 def paths(self, parent_path=None):
125 if parent_path == None:
128 path = "%s/%s" % (parent_path, self.name)
131 paths.extend(child.paths(path))
133 def child_by_path(self, path):
134 if hasattr(path, "split"): # convert string path to a list of names
135 names = path.split("/")
137 names[0] = self.name # replace root with self
138 if len(names) > 1 and names[-1] == "":
139 names = names[:-1] # strip empty tail
140 else: # it was already an array
142 assert len(names) > 0, path
143 if names[0] == self.name:
147 if names[1] == child.name:
148 return child.child_by_path(names[1:])
150 raise KeyError, "%s doesn't match '%s'" % (names, self.name)
151 raise KeyError, "%s points to child not in %s" % (names, [c.name for c in self])
152 def report_string(self):
153 return "\n".join(self.report())
154 def report(self, root=None, parent=None, depth=0):
156 root = self.make_root()
157 if self.masked == True:
159 data_part = self.data_part(depth)
160 if self.requires_children == True and len(self) == 0:
163 self.join(root, parent, data_part)
164 if data_part != None:
167 child.report(root, self, depth)
171 def join(self, root, parent, data_part):
172 if data_part != None:
173 root.append(data_part)
174 def data_part(self, depth, indent=True):
175 if self.data == None:
177 if hasattr(self, "_cached_data_part"):
178 return self._cached_data_part
179 data_part = self.data_part_fn(self.data)
181 data_part_lines = data_part.splitlines()
183 line_sep = "\n"+indent
184 data_part = indent+line_sep.join(data_part_lines)
185 self._cached_data_part = data_part
190 Difference tree generator for BugDirs.
192 >>> bd = bugdir.SimpleBugDir(sync_with_disk=False)
193 >>> bd.user_id = "John Doe <j@doe.com>"
194 >>> bd_new = copy.deepcopy(bd)
195 >>> bd_new.target = "1.0"
196 >>> a = bd_new.bug_from_uuid("a")
197 >>> rep = a.comment_root.new_reply("I'm closing this bug")
198 >>> rep.uuid = "acom"
199 >>> rep.date = "Thu, 01 Jan 1970 00:00:00 +0000"
200 >>> a.status = "closed"
201 >>> b = bd_new.bug_from_uuid("b")
202 >>> bd_new.remove_bug(b)
203 >>> c = bd_new.new_bug("c", "Bug C")
204 >>> d = Diff(bd, bd_new)
205 >>> r = d.report_tree()
206 >>> print "\\n".join(r.paths())
216 bugdir/bugs/mod/a/settings
217 bugdir/bugs/mod/a/comments
218 bugdir/bugs/mod/a/comments/new
219 bugdir/bugs/mod/a/comments/new/acom
220 bugdir/bugs/mod/a/comments/rem
221 bugdir/bugs/mod/a/comments/mod
222 >>> print r.report_string()
223 Changed bug directory settings:
231 Changed bug settings:
232 status: open -> closed
234 from John Doe <j@doe.com> on Thu, 01 Jan 1970 00:00:00 +0000
235 I'm closing this bug...
238 def __init__(self, old_bugdir, new_bugdir):
239 self.old_bugdir = old_bugdir
240 self.new_bugdir = new_bugdir
242 # data assembly methods
244 def _changed_bugs(self):
246 Search for differences in all bugs between .old_bugdir and
248 (added_bugs, modified_bugs, removed_bugs)
249 where added_bugs and removed_bugs are lists of added and
250 removed bugs respectively. modified_bugs is a list of
251 (old_bug,new_bug) pairs.
253 if hasattr(self, "__changed_bugs"):
254 return self.__changed_bugs
258 for uuid in self.new_bugdir.uuids():
259 new_bug = self.new_bugdir.bug_from_uuid(uuid)
261 old_bug = self.old_bugdir.bug_from_uuid(uuid)
263 added.append(new_bug)
265 if old_bug.sync_with_disk == True:
266 old_bug.load_comments()
267 if new_bug.sync_with_disk == True:
268 new_bug.load_comments()
269 if old_bug != new_bug:
270 modified.append((old_bug, new_bug))
271 for uuid in self.old_bugdir.uuids():
272 if not self.new_bugdir.has_bug(uuid):
273 old_bug = self.old_bugdir.bug_from_uuid(uuid)
274 removed.append(old_bug)
277 modified.sort(self._bug_modified_cmp)
278 self.__changed_bugs = (added, modified, removed)
279 return self.__changed_bugs
280 def _bug_modified_cmp(self, left, right):
281 return cmp(left[1], right[1])
282 def _changed_comments(self, old, new):
284 Search for differences in all loaded comments between the bugs
286 (added_comments, modified_comments, removed_comments)
287 analogous to ._changed_bugs.
289 if hasattr(self, "__changed_comments"):
290 if new.uuid in self.__changed_comments:
291 return self.__changed_comments[new.uuid]
293 self.__changed_comments = {}
297 old.comment_root.sort(key=lambda comm : comm.time)
298 new.comment_root.sort(key=lambda comm : comm.time)
299 old_comment_ids = [c.uuid for c in old.comments()]
300 new_comment_ids = [c.uuid for c in new.comments()]
301 for uuid in new_comment_ids:
302 new_comment = new.comment_from_uuid(uuid)
304 old_comment = old.comment_from_uuid(uuid)
306 added.append(new_comment)
308 if old_comment != new_comment:
309 modified.append((old_comment, new_comment))
310 for uuid in old_comment_ids:
311 if uuid not in new_comment_ids:
312 old_comment = old.comment_from_uuid(uuid)
313 removed.append(old_comment)
314 self.__changed_comments[new.uuid] = (added, modified, removed)
315 return self.__changed_comments[new.uuid]
316 def _attribute_changes(self, old, new, attributes):
318 Take two objects old and new, and compare the value of *.attr
319 for attr in the list attribute names. Returns a list of
320 (attr_name, old_value, new_value)
324 for attr in attributes:
325 old_value = getattr(old, attr)
326 new_value = getattr(new, attr)
327 if old_value != new_value:
328 change_list.append((attr, old_value, new_value))
329 if len(change_list) >= 0:
332 def _settings_properties_attribute_changes(self, old, new,
333 hidden_properties=[]):
334 properties = sorted(new.settings_properties)
335 for p in hidden_properties:
337 attributes = [settings_object.setting_name_to_attr_name(None, p)
339 return self._attribute_changes(old, new, attributes)
340 def _bugdir_attribute_changes(self):
341 return self._settings_properties_attribute_changes( \
342 self.old_bugdir, self.new_bugdir,
343 ["vcs_name"]) # tweaked by bugdir.duplicate_bugdir
344 def _bug_attribute_changes(self, old, new):
345 return self._settings_properties_attribute_changes(old, new)
346 def _comment_attribute_changes(self, old, new):
347 return self._settings_properties_attribute_changes(old, new)
349 # report generation methods
351 def report_tree(self, diff_tree=DiffTree):
353 Pretty bare to make it easy to adjust to specific cases. You
354 can pass in a DiffTree subclass via diff_tree to override the
355 default report assembly process.
357 if hasattr(self, "__report_tree"):
358 return self.__report_tree
359 bugdir_settings = sorted(self.new_bugdir.settings_properties)
360 bugdir_settings.remove("vcs_name") # tweaked by bugdir.duplicate_bugdir
361 root = diff_tree("bugdir")
362 bugdir_attribute_changes = self._bugdir_attribute_changes()
363 if len(bugdir_attribute_changes) > 0:
364 bugdir = diff_tree("settings", bugdir_attribute_changes,
365 self.bugdir_attribute_change_string)
367 bug_root = diff_tree("bugs")
368 root.append(bug_root)
369 add,mod,rem = self._changed_bugs()
370 bnew = diff_tree("new", "New bugs:", requires_children=True)
371 bug_root.append(bnew)
373 b = diff_tree(bug.uuid, bug, self.bug_add_string)
375 brem = diff_tree("rem", "Removed bugs:", requires_children=True)
376 bug_root.append(brem)
378 b = diff_tree(bug.uuid, bug, self.bug_rem_string)
380 bmod = diff_tree("mod", "Modified bugs:", requires_children=True)
381 bug_root.append(bmod)
383 b = diff_tree(new.uuid, (old,new), self.bug_mod_string)
385 bug_attribute_changes = self._bug_attribute_changes(old, new)
386 if len(bug_attribute_changes) > 0:
387 bset = diff_tree("settings", bug_attribute_changes,
388 self.bug_attribute_change_string)
390 if old.summary != new.summary:
391 data = (old.summary, new.summary)
392 bsum = diff_tree("summary", data, self.bug_summary_change_string)
394 cr = diff_tree("comments")
396 a,m,d = self._changed_comments(old, new)
397 cnew = diff_tree("new", "New comments:", requires_children=True)
399 c = diff_tree(comment.uuid, comment, self.comment_add_string)
401 crem = diff_tree("rem", "Removed comments:",requires_children=True)
403 c = diff_tree(comment.uuid, comment, self.comment_rem_string)
405 cmod = diff_tree("mod","Modified comments:",requires_children=True)
407 c = diff_tree(n.uuid, (o,n), self.comment_mod_string)
409 comm_attribute_changes = self._comment_attribute_changes(o, n)
410 if len(comm_attribute_changes) > 0:
411 cset = diff_tree("settings", comm_attribute_changes,
412 self.comment_attribute_change_string)
414 data = (o.body, n.body)
415 cbody = diff_tree("cbody", data,
416 self.comment_body_change_string)
418 cr.extend([cnew, crem, cmod])
419 self.__report_tree = root
420 return self.__report_tree
422 # change data -> string methods.
423 # Feel free to play with these in subclasses.
425 def attribute_change_string(self, attribute_changes, indent=0):
426 indent_string = " "*indent
427 change_strings = [u"%s: %s -> %s" % f for f in attribute_changes]
428 for i,change_string in enumerate(change_strings):
429 change_strings[i] = indent_string+change_string
430 return u"\n".join(change_strings)
431 def bugdir_attribute_change_string(self, attribute_changes):
432 return "Changed bug directory settings:\n%s" % \
433 self.attribute_change_string(attribute_changes, indent=1)
434 def bug_attribute_change_string(self, attribute_changes):
435 return "Changed bug settings:\n%s" % \
436 self.attribute_change_string(attribute_changes, indent=1)
437 def comment_attribute_change_string(self, attribute_changes):
438 return "Changed comment settings:\n%s" % \
439 self.attribute_change_string(attribute_changes, indent=1)
440 def bug_add_string(self, bug):
441 return bug.string(shortlist=True)
442 def bug_rem_string(self, bug):
443 return bug.string(shortlist=True)
444 def bug_mod_string(self, bugs):
445 old_bug,new_bug = bugs
446 return new_bug.string(shortlist=True)
447 def bug_summary_change_string(self, summaries):
448 old_summary,new_summary = summaries
449 return "summary changed:\n %s\n %s" % (old_summary, new_summary)
450 def _comment_summary_string(self, comment):
451 return "from %s on %s" % (comment.author, time_to_str(comment.time))
452 def comment_add_string(self, comment):
453 summary = self._comment_summary_string(comment)
454 first_line = comment.body.splitlines()[0]
455 return "%s\n %s..." % (summary, first_line)
456 def comment_rem_string(self, comment):
457 summary = self._comment_summary_string(comment)
458 first_line = comment.body.splitlines()[0]
459 return "%s\n %s..." % (summary, first_line)
460 def comment_mod_string(self, comments):
461 old_comment,new_comment = comments
462 return self._comment_summary_string(new_comment)
463 def comment_body_change_string(self, bodies):
464 old_body,new_body = bodies
465 return difflib.unified_diff(old_body, new_body)
468 if libbe.TESTING == True:
469 suite = doctest.DocTestSuite()