1 # Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc.
2 # W. Trevor King <wking@drexel.edu>
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License along
15 # with this program; if not, write to the Free Software Foundation, Inc.,
16 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17 """Compare two bug trees"""
18 from libbe import bugdir, bug, settings_object, tree
19 from libbe.utility import time_to_str
23 class DiffTree (tree.Tree):
25 A tree holding difference data for easy report generation.
26 >>> all = DiffTree("all")
27 >>> bugdir = DiffTree("bugdir", data="target: None -> 1.0")
28 >>> all.append(bugdir)
29 >>> bugs = DiffTree("bugs", "bug-count: 5 -> 6")
31 >>> new = DiffTree("new", "new bugs: ABC, DEF")
33 >>> rem = DiffTree("rem", "removed bugs: RST, UVW")
35 >>> print all.report_string()
39 removed bugs: RST, UVW
40 >>> print "\\n".join(all.paths())
46 >>> all.child_by_path("/") == all
48 >>> all.child_by_path("/bugs") == bugs
50 >>> all.child_by_path("/bugs/rem") == rem
52 >>> all.child_by_path("all") == all
54 >>> all.child_by_path("all/") == all
56 >>> all.child_by_path("all/bugs") == bugs
58 >>> all.child_by_path("/bugs").masked = True
59 >>> print all.report_string()
62 def __init__(self, name, data=None, data_string_fn=str,
63 requires_children=False, masked=False):
64 tree.Tree.__init__(self)
67 self.data_string_fn = data_string_fn
68 self.requires_children = requires_children
70 def paths(self, parent_path=None):
72 if parent_path == None:
75 path = "%s/%s" % (parent_path, self.name)
78 paths.extend(child.paths(path))
80 def child_by_path(self, path):
81 if hasattr(path, "split"): # convert string path to a list of names
82 names = path.split("/")
84 names[0] = self.name # replace root with self
85 if len(names) > 1 and names[-1] == "":
86 names = names[:-1] # strip empty tail
87 else: # it was already an array
89 assert len(names) > 0, path
90 if names[0] == self.name:
94 if names[1] == child.name:
95 return child.child_by_path(names[1:])
97 raise KeyError, "%s doesn't match '%s'" % (names, self.name)
98 raise KeyError, "%s points to child not in %s" % (names, [c.name for c in self])
99 def report_string(self):
100 return "\n".join(self.report())
101 def report(self, root=None, depth=0):
103 root = self.make_root()
104 if self.masked == True:
106 data_string = self.data_string(depth)
107 if self.data == None:
109 elif self.requires_children == True and len(self) == 0:
112 self.join(root, data_string)
115 child.report(root, depth)
119 def join(self, root, part):
122 def data_string(self, depth, indent=True):
123 if hasattr(self, "_cached_data_string"):
124 return self._cached_data_string
125 data_string = self.data_string_fn(self.data)
127 data_string_lines = data_string.splitlines()
129 line_sep = "\n"+indent
130 data_string = indent+line_sep.join(data_string_lines)
131 self._cached_data_string = data_string
136 Difference tree generator for BugDirs.
138 >>> bd = bugdir.simple_bug_dir(sync_with_disk=False)
139 >>> bd.user_id = "John Doe <j@doe.com>"
140 >>> bd_new = copy.deepcopy(bd)
141 >>> bd_new.target = "1.0"
142 >>> a = bd_new.bug_from_uuid("a")
143 >>> rep = a.comment_root.new_reply("I'm closing this bug")
144 >>> rep.uuid = "acom"
145 >>> rep.date = "Thu, 01 Jan 1970 00:00:00 +0000"
146 >>> a.status = "closed"
147 >>> b = bd_new.bug_from_uuid("b")
148 >>> bd_new.remove_bug(b)
149 >>> c = bd_new.new_bug("c", "Bug C")
150 >>> d = Diff(bd, bd_new)
151 >>> r = d.report_tree()
152 >>> print "\\n".join(r.paths())
162 bugdir/bugs/mod/a/settings
163 bugdir/bugs/mod/a/comments
164 bugdir/bugs/mod/a/comments/new
165 bugdir/bugs/mod/a/comments/new/acom
166 bugdir/bugs/mod/a/comments/rem
167 bugdir/bugs/mod/a/comments/mod
168 >>> print r.report_string()
169 Changed bug directory settings:
177 Changed bug settings:
178 status: open -> closed
180 from John Doe <j@doe.com> on Thu, 01 Jan 1970 00:00:00 +0000
182 def __init__(self, old_bugdir, new_bugdir):
183 self.old_bugdir = old_bugdir
184 self.new_bugdir = new_bugdir
186 # data assembly methods
188 def _changed_bugs(self):
190 Search for differences in all bugs between .old_bugdir and
192 (added_bugs, modified_bugs, removed_bugs)
193 where added_bugs and removed_bugs are lists of added and
194 removed bugs respectively. modified_bugs is a list of
195 (old_bug,new_bug) pairs.
197 if hasattr(self, "__changed_bugs"):
198 return self.__changed_bugs
202 for uuid in self.new_bugdir.list_uuids():
203 new_bug = self.new_bugdir.bug_from_uuid(uuid)
205 old_bug = self.old_bugdir.bug_from_uuid(uuid)
207 added.append(new_bug)
209 if old_bug.sync_with_disk == True:
210 old_bug.load_comments()
211 if new_bug.sync_with_disk == True:
212 new_bug.load_comments()
213 if old_bug != new_bug:
214 modified.append((old_bug, new_bug))
215 for uuid in self.old_bugdir.list_uuids():
216 if not self.new_bugdir.has_bug(uuid):
217 old_bug = self.old_bugdir.bug_from_uuid(uuid)
218 removed.append(old_bug)
221 modified.sort(self._bug_modified_cmp)
222 self.__changed_bugs = (added, modified, removed)
223 return self.__changed_bugs
224 def _bug_modified_cmp(self, left, right):
225 return cmp(left[1], right[1])
226 def _changed_comments(self, old, new):
228 Search for differences in all loaded comments between the bugs
230 (added_comments, modified_comments, removed_comments)
231 analogous to ._changed_bugs.
233 if hasattr(self, "__changed_comments"):
234 if new.uuid in self.__changed_comments:
235 return self.__changed_comments[new.uuid]
237 self.__changed_comments = {}
241 old.comment_root.sort(key=lambda comm : comm.time)
242 new.comment_root.sort(key=lambda comm : comm.time)
243 old_comment_ids = [c.uuid for c in old.comments()]
244 new_comment_ids = [c.uuid for c in new.comments()]
245 for uuid in new_comment_ids:
246 new_comment = new.comment_from_uuid(uuid)
248 old_comment = old.comment_from_uuid(uuid)
250 added.append(new_comment)
252 if old_comment != new_comment:
253 modified.append((old_comment, new_comment))
254 for uuid in old_comment_ids:
255 if uuid not in new_comment_ids:
256 new_comment = new.comment_from_uuid(uuid)
257 removed.append(new_comment)
258 self.__changed_comments[new.uuid] = (added, modified, removed)
259 return self.__changed_comments[new.uuid]
260 def _attribute_changes(self, old, new, attributes):
262 Take two objects old and new, and compare the value of *.attr
263 for attr in the list attribute names. Returns a list of
264 (attr_name, old_value, new_value)
268 for attr in attributes:
269 old_value = getattr(old, attr)
270 new_value = getattr(new, attr)
271 if old_value != new_value:
272 change_list.append((attr, old_value, new_value))
273 if len(change_list) >= 0:
276 def _settings_properties_attribute_changes(self, old, new,
277 hidden_properties=[]):
278 properties = sorted(new.settings_properties)
279 for p in hidden_properties:
281 attributes = [settings_object.setting_name_to_attr_name(None, p)
283 return self._attribute_changes(old, new, attributes)
284 def _bugdir_attribute_changes(self):
285 return self._settings_properties_attribute_changes( \
286 self.old_bugdir, self.new_bugdir,
287 ["rcs_name"]) # tweaked by bugdir.duplicate_bugdir
288 def _bug_attribute_changes(self, old, new):
289 return self._settings_properties_attribute_changes(old, new)
290 def _comment_attribute_changes(self, old, new):
291 return self._settings_properties_attribute_changes(old, new)
293 # report generation methods
295 def report_tree(self, diff_tree=DiffTree):
297 Pretty bare to make it easy to adjust to specific cases. You
298 can pass in a DiffTree subclass via diff_tree to override the
299 default report assembly process.
301 if hasattr(self, "__report_tree"):
302 return self.__report_tree
303 bugdir_settings = sorted(self.new_bugdir.settings_properties)
304 bugdir_settings.remove("rcs_name") # tweaked by bugdir.duplicate_bugdir
305 root = diff_tree("bugdir")
306 bugdir_attribute_changes = self._bugdir_attribute_changes()
307 if len(bugdir_attribute_changes) > 0:
308 bugdir = diff_tree("settings", bugdir_attribute_changes,
309 self.bugdir_attribute_change_string)
311 bug_root = diff_tree("bugs")
312 root.append(bug_root)
313 add,mod,rem = self._changed_bugs()
314 bnew = diff_tree("new", "New bugs:", requires_children=True)
315 bug_root.append(bnew)
317 b = diff_tree(bug.uuid, bug, self.bug_add_string)
319 brem = diff_tree("rem", "Removed bugs:", requires_children=True)
320 bug_root.append(brem)
322 b = diff_tree(bug.uuid, bug, self.bug_rem_string)
324 bmod = diff_tree("mod", "Modified bugs:", requires_children=True)
325 bug_root.append(bmod)
327 b = diff_tree(new.uuid, (old,new), self.bug_mod_string)
329 bug_attribute_changes = self._bug_attribute_changes(old, new)
330 if len(bug_attribute_changes) > 0:
331 bset = diff_tree("settings", bug_attribute_changes,
332 self.bug_attribute_change_string)
334 if old.summary != new.summary:
335 data = (old.summary, new.summary)
336 bsum = diff_tree("summary", data, self.bug_summary_change_string)
338 cr = diff_tree("comments")
340 a,m,d = self._changed_comments(old, new)
341 cnew = diff_tree("new", "New comments:", requires_children=True)
343 c = diff_tree(comment.uuid, comment, self.comment_add_string)
345 crem = diff_tree("rem", "Removed comments:",requires_children=True)
347 c = diff_tree(comment.uuid, comment, self.comment_rem_string)
349 cmod = diff_tree("mod","Modified comments:",requires_children=True)
351 c = diff_tree(n.uuid, (o,n), self.comment_mod_string)
353 comm_attribute_changes = self._comment_attribute_changes(o, n)
354 if len(comm_attribute_changes) > 0:
355 cset = diff_tree("settings", comm_attribute_changes,
356 self.comment_attribute_change_string)
358 data = (o.body, n.body)
359 cbody = diff_tree("cbody", data,
360 self.comment_body_change_string)
362 cr.extend([cnew, crem, cmod])
363 self.__report_tree = root
364 return self.__report_tree
366 # change data -> string methods.
367 # Feel free to play with these in subclasses.
369 def attribute_change_string(self, attribute_changes, indent=0):
370 indent_string = " "*indent
371 change_strings = [u"%s: %s -> %s" % f for f in attribute_changes]
372 for i,change_string in enumerate(change_strings):
373 change_strings[i] = indent_string+change_string
374 return u"\n".join(change_strings)
375 def bugdir_attribute_change_string(self, attribute_changes):
376 return "Changed bug directory settings:\n%s" % \
377 self.attribute_change_string(attribute_changes, indent=1)
378 def bug_attribute_change_string(self, attribute_changes):
379 return "Changed bug settings:\n%s" % \
380 self.attribute_change_string(attribute_changes, indent=1)
381 def comment_attribute_change_string(self, attribute_changes):
382 return "Changed comment settings:\n%s" % \
383 self.attribute_change_string(attribute_changes, indent=1)
384 def bug_add_string(self, bug):
385 return bug.string(shortlist=True)
386 def bug_rem_string(self, bug):
387 return bug.string(shortlist=True)
388 def bug_mod_string(self, bugs):
389 old_bug,new_bug = bugs
390 return new_bug.string(shortlist=True)
391 def bug_summary_change_string(self, summaries):
392 old_summary,new_summary = summaries
393 return "summary changed:\n %s\n %s" % (old_summary, new_summary)
394 def _comment_summary_string(self, comment):
395 return "from %s on %s" % (comment.author, time_to_str(comment.time))
396 def comment_add_string(self, comment):
397 summary = self._comment_summary_string(comment)
398 first_line = comment.body.splitlines()[0]
399 return "%s\n %s..." % (summary, first_line)
400 def comment_rem_string(self, comment):
401 return self._comment_summary_string(comment)
402 def comment_mod_string(self, comments):
403 old_comment,new_comment = comments
404 return self._comment_summary_string(new_comment)
405 def comment_body_change_string(self, bodies):
406 old_body,new_body = bodies
407 return difflib.unified_diff(old_body, new_body)
410 suite = doctest.DocTestSuite()