Merged updated READMEs and new update_copyright.py (and its results).
[be.git] / libbe / diff.py
1 # Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc.
2 #                         Gianluca Montecchi <gian@grys.it>
3 #                         W. Trevor King <wking@drexel.edu>
4 #
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.
9 #
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.
14 #
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.
18
19 """Compare two bug trees."""
20
21 import difflib
22 import doctest
23
24 from libbe import bugdir, bug, settings_object, tree
25 from libbe.utility import time_to_str
26
27
28 class DiffTree (tree.Tree):
29     """
30     A tree holding difference data for easy report generation.
31     >>> bugdir = DiffTree("bugdir")
32     >>> bdsettings = DiffTree("settings", data="target: None -> 1.0")
33     >>> bugdir.append(bdsettings)
34     >>> bugs = DiffTree("bugs", "bug-count: 5 -> 6")
35     >>> bugdir.append(bugs)
36     >>> new = DiffTree("new", "new bugs: ABC, DEF")
37     >>> bugs.append(new)
38     >>> rem = DiffTree("rem", "removed bugs: RST, UVW")
39     >>> bugs.append(rem)
40     >>> print bugdir.report_string()
41     target: None -> 1.0
42     bug-count: 5 -> 6
43       new bugs: ABC, DEF
44       removed bugs: RST, UVW
45     >>> print "\\n".join(bugdir.paths())
46     bugdir
47     bugdir/settings
48     bugdir/bugs
49     bugdir/bugs/new
50     bugdir/bugs/rem
51     >>> bugdir.child_by_path("/") == bugdir
52     True
53     >>> bugdir.child_by_path("/bugs") == bugs
54     True
55     >>> bugdir.child_by_path("/bugs/rem") == rem
56     True
57     >>> bugdir.child_by_path("bugdir") == bugdir
58     True
59     >>> bugdir.child_by_path("bugdir/") == bugdir
60     True
61     >>> bugdir.child_by_path("bugdir/bugs") == bugs
62     True
63     >>> bugdir.child_by_path("/bugs").masked = True
64     >>> print bugdir.report_string()
65     target: None -> 1.0
66     """
67     def __init__(self, name, data=None, data_part_fn=str,
68                  requires_children=False, masked=False):
69         tree.Tree.__init__(self)
70         self.name = name
71         self.data = data
72         self.data_part_fn = data_part_fn
73         self.requires_children = requires_children
74         self.masked = masked
75     def paths(self, parent_path=None):
76         paths = []
77         if parent_path == None:
78             path = self.name
79         else:
80             path = "%s/%s" % (parent_path, self.name)
81         paths.append(path)
82         for child in self:
83             paths.extend(child.paths(path))
84         return paths
85     def child_by_path(self, path):
86         if hasattr(path, "split"): # convert string path to a list of names
87             names = path.split("/")
88             if names[0] == "":
89                 names[0] = self.name # replace root with self
90             if len(names) > 1 and names[-1] == "":
91                 names = names[:-1] # strip empty tail
92         else: # it was already an array
93             names = path
94         assert len(names) > 0, path
95         if names[0] == self.name:
96             if len(names) == 1:
97                 return self
98             for child in self:
99                 if names[1] == child.name:
100                     return child.child_by_path(names[1:])
101         if len(names) == 1:
102             raise KeyError, "%s doesn't match '%s'" % (names, self.name)
103         raise KeyError, "%s points to child not in %s" % (names, [c.name for c in self])
104     def report_string(self):
105         return "\n".join(self.report())
106     def report(self, root=None, parent=None, depth=0):
107         if root == None:
108             root = self.make_root()
109         if self.masked == True:
110             return None
111         data_part = self.data_part(depth)
112         if self.requires_children == True and len(self) == 0:
113             pass
114         else:
115             self.join(root, parent, data_part)
116             if data_part != None:
117                 depth += 1
118         for child in self:
119             child.report(root, self, depth)
120         return root
121     def make_root(self):
122         return []
123     def join(self, root, parent, data_part):
124         if data_part != None:
125             root.append(data_part)
126     def data_part(self, depth, indent=True):
127         if self.data == None:
128             return None
129         if hasattr(self, "_cached_data_part"):
130             return self._cached_data_part
131         data_part = self.data_part_fn(self.data)
132         if indent == True:
133             data_part_lines = data_part.splitlines()
134             indent = "  "*(depth)
135             line_sep = "\n"+indent
136             data_part = indent+line_sep.join(data_part_lines)
137         self._cached_data_part = data_part
138         return data_part
139
140 class Diff (object):
141     """
142     Difference tree generator for BugDirs.
143     >>> import copy
144     >>> bd = bugdir.SimpleBugDir(sync_with_disk=False)
145     >>> bd.user_id = "John Doe <j@doe.com>"
146     >>> bd_new = copy.deepcopy(bd)
147     >>> bd_new.target = "1.0"
148     >>> a = bd_new.bug_from_uuid("a")
149     >>> rep = a.comment_root.new_reply("I'm closing this bug")
150     >>> rep.uuid = "acom"
151     >>> rep.date = "Thu, 01 Jan 1970 00:00:00 +0000"
152     >>> a.status = "closed"
153     >>> b = bd_new.bug_from_uuid("b")
154     >>> bd_new.remove_bug(b)
155     >>> c = bd_new.new_bug("c", "Bug C")
156     >>> d = Diff(bd, bd_new)
157     >>> r = d.report_tree()
158     >>> print "\\n".join(r.paths())
159     bugdir
160     bugdir/settings
161     bugdir/bugs
162     bugdir/bugs/new
163     bugdir/bugs/new/c
164     bugdir/bugs/rem
165     bugdir/bugs/rem/b
166     bugdir/bugs/mod
167     bugdir/bugs/mod/a
168     bugdir/bugs/mod/a/settings
169     bugdir/bugs/mod/a/comments
170     bugdir/bugs/mod/a/comments/new
171     bugdir/bugs/mod/a/comments/new/acom
172     bugdir/bugs/mod/a/comments/rem
173     bugdir/bugs/mod/a/comments/mod
174     >>> print r.report_string()
175     Changed bug directory settings:
176       target: None -> 1.0
177     New bugs:
178       c:om: Bug C
179     Removed bugs:
180       b:cm: Bug B
181     Modified bugs:
182       a:cm: Bug A
183         Changed bug settings:
184           status: open -> closed
185         New comments:
186           from John Doe <j@doe.com> on Thu, 01 Jan 1970 00:00:00 +0000
187             I'm closing this bug...
188     >>> bd.cleanup()
189     """
190     def __init__(self, old_bugdir, new_bugdir):
191         self.old_bugdir = old_bugdir
192         self.new_bugdir = new_bugdir
193
194     # data assembly methods
195
196     def _changed_bugs(self):
197         """
198         Search for differences in all bugs between .old_bugdir and
199         .new_bugdir.  Returns
200           (added_bugs, modified_bugs, removed_bugs)
201         where added_bugs and removed_bugs are lists of added and
202         removed bugs respectively.  modified_bugs is a list of
203         (old_bug,new_bug) pairs.
204         """
205         if hasattr(self, "__changed_bugs"):
206             return self.__changed_bugs
207         added = []
208         removed = []
209         modified = []
210         for uuid in self.new_bugdir.list_uuids():
211             new_bug = self.new_bugdir.bug_from_uuid(uuid)
212             try:
213                 old_bug = self.old_bugdir.bug_from_uuid(uuid)
214             except KeyError:
215                 added.append(new_bug)
216             else:
217                 if old_bug.sync_with_disk == True:
218                     old_bug.load_comments()
219                 if new_bug.sync_with_disk == True:
220                     new_bug.load_comments()
221                 if old_bug != new_bug:
222                     modified.append((old_bug, new_bug))
223         for uuid in self.old_bugdir.list_uuids():
224             if not self.new_bugdir.has_bug(uuid):
225                 old_bug = self.old_bugdir.bug_from_uuid(uuid)
226                 removed.append(old_bug)
227         added.sort()
228         removed.sort()
229         modified.sort(self._bug_modified_cmp)
230         self.__changed_bugs = (added, modified, removed)
231         return self.__changed_bugs
232     def _bug_modified_cmp(self, left, right):
233         return cmp(left[1], right[1])
234     def _changed_comments(self, old, new):
235         """
236         Search for differences in all loaded comments between the bugs
237         old and new.  Returns
238           (added_comments, modified_comments, removed_comments)
239         analogous to ._changed_bugs.
240         """
241         if hasattr(self, "__changed_comments"):
242             if new.uuid in self.__changed_comments:
243                 return self.__changed_comments[new.uuid]
244         else:
245             self.__changed_comments = {}
246         added = []
247         removed = []
248         modified = []
249         old.comment_root.sort(key=lambda comm : comm.time)
250         new.comment_root.sort(key=lambda comm : comm.time)
251         old_comment_ids = [c.uuid for c in old.comments()]
252         new_comment_ids = [c.uuid for c in new.comments()]
253         for uuid in new_comment_ids:
254             new_comment = new.comment_from_uuid(uuid)
255             try:
256                 old_comment = old.comment_from_uuid(uuid)
257             except KeyError:
258                 added.append(new_comment)
259             else:
260                 if old_comment != new_comment:
261                     modified.append((old_comment, new_comment))
262         for uuid in old_comment_ids:
263             if uuid not in new_comment_ids:
264                 new_comment = new.comment_from_uuid(uuid)
265                 removed.append(new_comment)
266         self.__changed_comments[new.uuid] = (added, modified, removed)
267         return self.__changed_comments[new.uuid]
268     def _attribute_changes(self, old, new, attributes):
269         """
270         Take two objects old and new, and compare the value of *.attr
271         for attr in the list attribute names.  Returns a list of
272           (attr_name, old_value, new_value)
273         tuples.
274         """
275         change_list = []
276         for attr in attributes:
277             old_value = getattr(old, attr)
278             new_value = getattr(new, attr)
279             if old_value != new_value:
280                 change_list.append((attr, old_value, new_value))
281         if len(change_list) >= 0:
282             return change_list
283         return None
284     def _settings_properties_attribute_changes(self, old, new,
285                                               hidden_properties=[]):
286         properties = sorted(new.settings_properties)
287         for p in hidden_properties:
288             properties.remove(p)
289         attributes = [settings_object.setting_name_to_attr_name(None, p)
290                       for p in properties]
291         return self._attribute_changes(old, new, attributes)
292     def _bugdir_attribute_changes(self):
293         return self._settings_properties_attribute_changes( \
294             self.old_bugdir, self.new_bugdir,
295             ["vcs_name"]) # tweaked by bugdir.duplicate_bugdir
296     def _bug_attribute_changes(self, old, new):
297         return self._settings_properties_attribute_changes(old, new)
298     def _comment_attribute_changes(self, old, new):
299         return self._settings_properties_attribute_changes(old, new)
300
301     # report generation methods
302
303     def report_tree(self, diff_tree=DiffTree):
304         """
305         Pretty bare to make it easy to adjust to specific cases.  You
306         can pass in a DiffTree subclass via diff_tree to override the
307         default report assembly process.
308         """
309         if hasattr(self, "__report_tree"):
310             return self.__report_tree
311         bugdir_settings = sorted(self.new_bugdir.settings_properties)
312         bugdir_settings.remove("vcs_name") # tweaked by bugdir.duplicate_bugdir
313         root = diff_tree("bugdir")
314         bugdir_attribute_changes = self._bugdir_attribute_changes()
315         if len(bugdir_attribute_changes) > 0:
316             bugdir = diff_tree("settings", bugdir_attribute_changes,
317                                self.bugdir_attribute_change_string)
318             root.append(bugdir)
319         bug_root = diff_tree("bugs")
320         root.append(bug_root)
321         add,mod,rem = self._changed_bugs()
322         bnew = diff_tree("new", "New bugs:", requires_children=True)
323         bug_root.append(bnew)
324         for bug in add:
325             b = diff_tree(bug.uuid, bug, self.bug_add_string)
326             bnew.append(b)
327         brem = diff_tree("rem", "Removed bugs:", requires_children=True)
328         bug_root.append(brem)
329         for bug in rem:
330             b = diff_tree(bug.uuid, bug, self.bug_rem_string)
331             brem.append(b)
332         bmod = diff_tree("mod", "Modified bugs:", requires_children=True)
333         bug_root.append(bmod)
334         for old,new in mod:
335             b = diff_tree(new.uuid, (old,new), self.bug_mod_string)
336             bmod.append(b)
337             bug_attribute_changes = self._bug_attribute_changes(old, new)
338             if len(bug_attribute_changes) > 0:
339                 bset = diff_tree("settings", bug_attribute_changes,
340                                  self.bug_attribute_change_string)
341                 b.append(bset)
342             if old.summary != new.summary:
343                 data = (old.summary, new.summary)
344                 bsum = diff_tree("summary", data, self.bug_summary_change_string)
345                 b.append(bsum)
346             cr = diff_tree("comments")
347             b.append(cr)
348             a,m,d = self._changed_comments(old, new)
349             cnew = diff_tree("new", "New comments:", requires_children=True)
350             for comment in a:
351                 c = diff_tree(comment.uuid, comment, self.comment_add_string)
352                 cnew.append(c)
353             crem = diff_tree("rem", "Removed comments:",requires_children=True)
354             for comment in d:
355                 c = diff_tree(comment.uuid, comment, self.comment_rem_string)
356                 crem.append(c)
357             cmod = diff_tree("mod","Modified comments:",requires_children=True)
358             for o,n in m:
359                 c = diff_tree(n.uuid, (o,n), self.comment_mod_string)
360                 cmod.append(c)
361                 comm_attribute_changes = self._comment_attribute_changes(o, n)
362                 if len(comm_attribute_changes) > 0:
363                     cset = diff_tree("settings", comm_attribute_changes,
364                                      self.comment_attribute_change_string)
365                 if o.body != n.body:
366                     data = (o.body, n.body)
367                     cbody = diff_tree("cbody", data,
368                                       self.comment_body_change_string)
369                     c.append(cbody)
370             cr.extend([cnew, crem, cmod])
371         self.__report_tree = root
372         return self.__report_tree
373
374     # change data -> string methods.
375     # Feel free to play with these in subclasses.
376
377     def attribute_change_string(self, attribute_changes, indent=0):
378         indent_string = "  "*indent
379         change_strings = [u"%s: %s -> %s" % f for f in attribute_changes]
380         for i,change_string in enumerate(change_strings):
381             change_strings[i] = indent_string+change_string
382         return u"\n".join(change_strings)
383     def bugdir_attribute_change_string(self, attribute_changes):
384         return "Changed bug directory settings:\n%s" % \
385             self.attribute_change_string(attribute_changes, indent=1)
386     def bug_attribute_change_string(self, attribute_changes):
387         return "Changed bug settings:\n%s" % \
388             self.attribute_change_string(attribute_changes, indent=1)
389     def comment_attribute_change_string(self, attribute_changes):
390         return "Changed comment settings:\n%s" % \
391             self.attribute_change_string(attribute_changes, indent=1)
392     def bug_add_string(self, bug):
393         return bug.string(shortlist=True)
394     def bug_rem_string(self, bug):
395         return bug.string(shortlist=True)
396     def bug_mod_string(self, bugs):
397         old_bug,new_bug = bugs
398         return new_bug.string(shortlist=True)
399     def bug_summary_change_string(self, summaries):
400         old_summary,new_summary = summaries
401         return "summary changed:\n  %s\n  %s" % (old_summary, new_summary)
402     def _comment_summary_string(self, comment):
403         return "from %s on %s" % (comment.author, time_to_str(comment.time))
404     def comment_add_string(self, comment):
405         summary = self._comment_summary_string(comment)
406         first_line = comment.body.splitlines()[0]
407         return "%s\n  %s..." % (summary, first_line)
408     def comment_rem_string(self, comment):
409         summary = self._comment_summary_string(comment)
410         first_line = comment.body.splitlines()[0]
411         return "%s\n  %s..." % (summary, first_line)
412     def comment_mod_string(self, comments):
413         old_comment,new_comment = comments
414         return self._comment_summary_string(new_comment)
415     def comment_body_change_string(self, bodies):
416         old_body,new_body = bodies
417         return difflib.unified_diff(old_body, new_body)
418
419
420 suite = doctest.DocTestSuite()