Make Gianluca's bug status display optional for `be depend`.
[be.git] / libbe / command / depend.py
1 # Copyright (C) 2009-2010 Gianluca Montecchi <gian@grys.it>
2 #                         W. Trevor King <wking@drexel.edu>
3 #
4 # This file is part of Bugs Everywhere.
5 #
6 # Bugs Everywhere is free software; you can redistribute it and/or modify it
7 # under the terms of the GNU General Public License as published by the
8 # Free Software Foundation, either version 2 of the License, or (at your
9 # option) any later version.
10 #
11 # Bugs Everywhere is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with Bugs Everywhere.  If not, see <http://www.gnu.org/licenses/>.
18
19 import copy
20 import os
21
22 import libbe
23 import libbe.bug
24 import libbe.command
25 import libbe.command.util
26 import libbe.util.tree
27
28 BLOCKS_TAG="BLOCKS:"
29 BLOCKED_BY_TAG="BLOCKED-BY:"
30
31 class BrokenLink (Exception):
32     def __init__(self, blocked_bug, blocking_bug, blocks=True):
33         if blocks == True:
34             msg = "Missing link: %s blocks %s" \
35                 % (blocking_bug.id.user(), blocked_bug.id.user())
36         else:
37             msg = "Missing link: %s blocked by %s" \
38                 % (blocked_bug.id.user(), blocking_bug.id.user())
39         Exception.__init__(self, msg)
40         self.blocked_bug = blocked_bug
41         self.blocking_bug = blocking_bug
42
43 class Depend (libbe.command.Command):
44     """Add/remove bug dependencies
45
46     >>> import sys
47     >>> import libbe.bugdir
48     >>> bd = libbe.bugdir.SimpleBugDir(memory=False)
49     >>> io = libbe.command.StringInputOutput()
50     >>> io.stdout = sys.stdout
51     >>> ui = libbe.command.UserInterface(io=io)
52     >>> ui.storage_callbacks.set_storage(bd.storage)
53     >>> cmd = Depend(ui=ui)
54
55     >>> ret = ui.run(cmd, args=['/a', '/b'])
56     abc/a blocked by:
57     abc/b
58     >>> ret = ui.run(cmd, args=['/a'])
59     abc/a blocked by:
60     abc/b
61     >>> ret = ui.run(cmd, {'show-status':True}, ['/a']) # doctest: +NORMALIZE_WHITESPACE
62     abc/a blocked by:
63     abc/b closed
64     >>> ret = ui.run(cmd, args=['/b', '/a'])
65     abc/b blocked by:
66     abc/a
67     abc/b blocks:
68     abc/a
69     >>> ret = ui.run(cmd, {'show-status':True}, ['/a']) # doctest: +NORMALIZE_WHITESPACE
70     abc/a blocked by:
71     abc/b closed
72     abc/a blocks:
73     abc/b closed
74     >>> ret = ui.run(cmd, {'show-summary':True}, ['/a']) # doctest: +NORMALIZE_WHITESPACE
75     abc/a blocked by:
76     abc/b       Bug B
77     abc/a blocks:
78     abc/b       Bug B
79     >>> ret = ui.run(cmd, {'repair':True})
80     >>> ret = ui.run(cmd, {'remove':True}, ['/b', '/a'])
81     abc/b blocks:
82     abc/a
83     >>> ret = ui.run(cmd, {'remove':True}, ['/a', '/b'])
84     >>> ui.cleanup()
85     >>> bd.cleanup()
86     """
87     name = 'depend'
88
89     def __init__(self, *args, **kwargs):
90         libbe.command.Command.__init__(self, *args, **kwargs)
91         self.options.extend([
92                 libbe.command.Option(name='remove', short_name='r',
93                     help='Remove dependency (instead of adding it)'),
94                 libbe.command.Option(name='show-status', short_name='s',
95                     help='Show status of blocking bugs'),
96                 libbe.command.Option(name='show-summary', short_name='S',
97                     help='Show summary of blocking bugs'),
98                 libbe.command.Option(name='status',
99                     help='Only show bugs matching the STATUS specifier',
100                     arg=libbe.command.Argument(
101                         name='status', metavar='STATUS', default=None,
102                         completion_callback=libbe.command.util.complete_status)),
103                 libbe.command.Option(name='severity',
104                     help='Only show bugs matching the SEVERITY specifier',
105                     arg=libbe.command.Argument(
106                         name='severity', metavar='SEVERITY', default=None,
107                         completion_callback=libbe.command.util.complete_severity)),
108                 libbe.command.Option(name='tree-depth', short_name='t',
109                     help='Print dependency tree rooted at BUG-ID with DEPTH levels of both blockers and blockees.  Set DEPTH <= 0 to disable the depth limit.',
110                     arg=libbe.command.Argument(
111                         name='tree-depth', metavar='INT', type='int',
112                         completion_callback=libbe.command.util.complete_severity)),
113                 libbe.command.Option(name='repair',
114                     help='Check for and repair one-way links'),
115                 ])
116         self.args.extend([
117                 libbe.command.Argument(
118                     name='bug-id', metavar='BUG-ID', default=None,
119                     optional=True,
120                     completion_callback=libbe.command.util.complete_bug_id),
121                 libbe.command.Argument(
122                     name='blocking-bug-id', metavar='BUG-ID', default=None,
123                     optional=True,
124                     completion_callback=libbe.command.util.complete_bug_id),
125                 ])
126
127     def _run(self, **params):
128         if params['repair'] == True and params['bug-id'] != None:
129             raise libbe.command.UserError(
130                 'No arguments with --repair calls.')
131         if params['repair'] == False and params['bug-id'] == None:
132             raise libbe.command.UserError(
133                 'Must specify either --repair or a BUG-ID')
134         if params['tree-depth'] != None \
135                 and params['blocking-bug-id'] != None:
136             raise libbe.command.UserError(
137                 'Only one bug id used in tree mode.')
138         bugdir = self._get_bugdir()
139         if params['repair'] == True:
140             good,fixed,broken = check_dependencies(bugdir, repair_broken_links=True)
141             assert len(broken) == 0, broken
142             if len(fixed) > 0:
143                 print >> self.stdout, 'Fixed the following links:'
144                 print >> self.stdout, \
145                     '\n'.join(['%s |-- %s' % (blockee.id.user(), blocker.id.user())
146                                for blockee,blocker in fixed])
147             return 0
148         allowed_status_values = \
149             libbe.command.util.select_values(
150                 params['status'], libbe.bug.status_values)
151         allowed_severity_values = \
152             libbe.command.util.select_values(
153                 params['severity'], libbe.bug.severity_values)
154
155         bugA, dummy_comment = libbe.command.util.bug_comment_from_user_id(
156             bugdir, params['bug-id'])
157
158         if params['tree-depth'] != None:
159             dtree = DependencyTree(bugdir, bugA, params['tree-depth'],
160                                    allowed_status_values,
161                                    allowed_severity_values)
162             if len(dtree.blocked_by_tree()) > 0:
163                 print >> self.stdout, '%s blocked by:' % bugA.id.user()
164                 for depth,node in dtree.blocked_by_tree().thread():
165                     if depth == 0: continue
166                     print >> self.stdout, \
167                         '%s%s' % (' '*(depth),
168                         node.bug.string(shortlist=True))
169             if len(dtree.blocks_tree()) > 0:
170                 print >> self.stdout, '%s blocks:' % bugA.id.user()
171                 for depth,node in dtree.blocks_tree().thread():
172                     if depth == 0: continue
173                     print >> self.stdout, \
174                         '%s%s' % (' '*(depth),
175                         node.bug.string(shortlist=True))
176             return 0
177
178         if params['blocking-bug-id'] != None:
179             bugB,dummy_comment = libbe.command.util.bug_comment_from_user_id(
180                 bugdir, params['blocking-bug-id'])
181             if params['remove'] == True:
182                 remove_block(bugA, bugB)
183             else: # add the dependency
184                 add_block(bugA, bugB)
185
186         blocked_by = get_blocked_by(bugdir, bugA)
187
188         if len(blocked_by) > 0:
189             print >> self.stdout, '%s blocked by:' % bugA.id.user()
190             print >> self.stdout, \
191                 '\n'.join([self.bug_string(_bug, params)
192                            for _bug in blocked_by])
193         blocks = get_blocks(bugdir, bugA)
194         if len(blocks) > 0:
195             print >> self.stdout, '%s blocks:' % bugA.id.user()
196             print >> self.stdout, \
197                 '\n'.join([self.bug_string(_bug, params)
198                            for _bug in blocks])
199         return 0
200
201     def bug_string(self, _bug, params):
202         fields = [_bug.id.user()]
203         if params['show-status'] == True:
204             fields.append(_bug.status)
205         if params['show-summary'] == True:
206             fields.append(_bug.summary)
207         return '\t'.join(fields)
208
209     def _long_help(self):
210         return """
211 Set a dependency with the second bug (B) blocking the first bug (A).
212 If bug B is not specified, just print a list of bugs blocking (A).
213
214 To search for bugs blocked by a particular bug, try
215   $ be list --extra-strings BLOCKED-BY:<your-bug-uuid>
216
217 The --status and --severity options allow you to either blacklist or
218 whitelist values, for example
219   $ be list --status open,assigned
220 will only follow and print dependencies with open or assigned status.
221 You select blacklist mode by starting the list with a minus sign, for
222 example
223   $ be list --severity -target
224 which will only follow and print dependencies with non-target severity.
225
226 If neither bug A nor B is specified, check for and repair the missing
227 side of any one-way links.
228
229 The "|--" symbol in the repair-mode output is inspired by the
230 "negative feedback" arrow common in biochemistry.  See, for example
231   http://www.nature.com/nature/journal/v456/n7223/images/nature07513-f5.0.jpg
232 """
233
234 # internal helper functions
235
236 def _generate_blocks_string(blocked_bug):
237     return '%s%s' % (BLOCKS_TAG, blocked_bug.uuid)
238
239 def _generate_blocked_by_string(blocking_bug):
240     return '%s%s' % (BLOCKED_BY_TAG, blocking_bug.uuid)
241
242 def _parse_blocks_string(string):
243     assert string.startswith(BLOCKS_TAG)
244     return string[len(BLOCKS_TAG):]
245
246 def _parse_blocked_by_string(string):
247     assert string.startswith(BLOCKED_BY_TAG)
248     return string[len(BLOCKED_BY_TAG):]
249
250 def _add_remove_extra_string(bug, string, add):
251     estrs = bug.extra_strings
252     if add == True:
253         estrs.append(string)
254     else: # remove the string
255         estrs.remove(string)
256     bug.extra_strings = estrs # reassign to notice change
257
258 def _get_blocks(bug):
259     uuids = []
260     for line in bug.extra_strings:
261         if line.startswith(BLOCKS_TAG):
262             uuids.append(_parse_blocks_string(line))
263     return uuids
264
265 def _get_blocked_by(bug):
266     uuids = []
267     for line in bug.extra_strings:
268         if line.startswith(BLOCKED_BY_TAG):
269             uuids.append(_parse_blocked_by_string(line))
270     return uuids
271
272 def _repair_one_way_link(blocked_bug, blocking_bug, blocks=None):
273     if blocks == True: # add blocks link
274         blocks_string = _generate_blocks_string(blocked_bug)
275         _add_remove_extra_string(blocking_bug, blocks_string, add=True)
276     else: # add blocked by link
277         blocked_by_string = _generate_blocked_by_string(blocking_bug)
278         _add_remove_extra_string(blocked_bug, blocked_by_string, add=True)
279
280 # functions exposed to other modules
281
282 def add_block(blocked_bug, blocking_bug):
283     blocked_by_string = _generate_blocked_by_string(blocking_bug)
284     _add_remove_extra_string(blocked_bug, blocked_by_string, add=True)
285     blocks_string = _generate_blocks_string(blocked_bug)
286     _add_remove_extra_string(blocking_bug, blocks_string, add=True)
287
288 def remove_block(blocked_bug, blocking_bug):
289     blocked_by_string = _generate_blocked_by_string(blocking_bug)
290     _add_remove_extra_string(blocked_bug, blocked_by_string, add=False)
291     blocks_string = _generate_blocks_string(blocked_bug)
292     _add_remove_extra_string(blocking_bug, blocks_string, add=False)
293
294 def get_blocks(bugdir, bug):
295     """
296     Return a list of bugs that the given bug blocks.
297     """
298     blocks = []
299     for uuid in _get_blocks(bug):
300         blocks.append(bugdir.bug_from_uuid(uuid))
301     return blocks
302
303 def get_blocked_by(bugdir, bug):
304     """
305     Return a list of bugs blocking the given bug.
306     """
307     blocked_by = []
308     for uuid in _get_blocked_by(bug):
309         blocked_by.append(bugdir.bug_from_uuid(uuid))
310     return blocked_by
311
312 def check_dependencies(bugdir, repair_broken_links=False):
313     """
314     Check that links are bi-directional for all bugs in bugdir.
315
316     >>> import libbe.bugdir
317     >>> bd = libbe.bugdir.SimpleBugDir()
318     >>> a = bd.bug_from_uuid("a")
319     >>> b = bd.bug_from_uuid("b")
320     >>> blocked_by_string = _generate_blocked_by_string(b)
321     >>> _add_remove_extra_string(a, blocked_by_string, add=True)
322     >>> good,repaired,broken = check_dependencies(bd, repair_broken_links=False)
323     >>> good
324     []
325     >>> repaired
326     []
327     >>> broken
328     [(Bug(uuid='a'), Bug(uuid='b'))]
329     >>> _get_blocks(b)
330     []
331     >>> good,repaired,broken = check_dependencies(bd, repair_broken_links=True)
332     >>> _get_blocks(b)
333     ['a']
334     >>> good
335     []
336     >>> repaired
337     [(Bug(uuid='a'), Bug(uuid='b'))]
338     >>> broken
339     []
340     """
341     if bugdir.storage != None:
342         bugdir.load_all_bugs()
343     good_links = []
344     fixed_links = []
345     broken_links = []
346     for bug in bugdir:
347         for blocker in get_blocked_by(bugdir, bug):
348             blocks = get_blocks(bugdir, blocker)
349             if (bug, blocks) in good_links+fixed_links+broken_links:
350                 continue # already checked that link
351             if bug not in blocks:
352                 if repair_broken_links == True:
353                     _repair_one_way_link(bug, blocker, blocks=True)
354                     fixed_links.append((bug, blocker))
355                 else:
356                     broken_links.append((bug, blocker))
357             else:
358                 good_links.append((bug, blocker))
359         for blockee in get_blocks(bugdir, bug):
360             blocked_by = get_blocked_by(bugdir, blockee)
361             if (blockee, bug) in good_links+fixed_links+broken_links:
362                 continue # already checked that link
363             if bug not in blocked_by:
364                 if repair_broken_links == True:
365                     _repair_one_way_link(blockee, bug, blocks=False)
366                     fixed_links.append((blockee, bug))
367                 else:
368                     broken_links.append((blockee, bug))
369             else:
370                 good_links.append((blockee, bug))
371     return (good_links, fixed_links, broken_links)
372
373 class DependencyTree (object):
374     """
375     Note: should probably be DependencyDiGraph.
376     """
377     def __init__(self, bugdir, root_bug, depth_limit=0,
378                  allowed_status_values=None,
379                  allowed_severity_values=None):
380         self.bugdir = bugdir
381         self.root_bug = root_bug
382         self.depth_limit = depth_limit
383         self.allowed_status_values = allowed_status_values
384         self.allowed_severity_values = allowed_severity_values
385
386     def _build_tree(self, child_fn):
387         root = libbe.util.tree.Tree()
388         root.bug = self.root_bug
389         root.depth = 0
390         stack = [root]
391         while len(stack) > 0:
392             node = stack.pop()
393             if self.depth_limit > 0 and node.depth == self.depth_limit:
394                 continue
395             for bug in child_fn(self.bugdir, node.bug):
396                 if self.allowed_status_values != None \
397                         and not bug.status in self.allowed_status_values:
398                     continue
399                 if self.allowed_severity_values != None \
400                         and not bug.severity in self.allowed_severity_values:
401                     continue
402                 child = libbe.util.tree.Tree()
403                 child.bug = bug
404                 child.depth = node.depth+1
405                 node.append(child)
406                 stack.append(child)
407         return root
408
409     def blocks_tree(self):
410         if not hasattr(self, "_blocks_tree"):
411             self._blocks_tree = self._build_tree(get_blocks)
412         return self._blocks_tree
413
414     def blocked_by_tree(self):
415         if not hasattr(self, "_blocked_by_tree"):
416             self._blocked_by_tree = self._build_tree(get_blocked_by)
417         return self._blocked_by_tree