1 # Copyright (C) 2009-2010 Gianluca Montecchi <gian@grys.it>
2 # W. Trevor King <wking@drexel.edu>
4 # This file is part of Bugs Everywhere.
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.
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.
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/>.
25 import libbe.command.util
26 import libbe.util.tree
29 BLOCKED_BY_TAG="BLOCKED-BY:"
31 class BrokenLink (Exception):
32 def __init__(self, blocked_bug, blocking_bug, blocks=True):
34 msg = "Missing link: %s blocks %s" \
35 % (blocking_bug.id.user(), blocked_bug.id.user())
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
43 class Depend (libbe.command.Command):
44 """Add/remove bug dependencies
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)
55 >>> ret = ui.run(cmd, args=['/a', '/b'])
58 >>> ret = ui.run(cmd, args=['/a'])
61 >>> ret = ui.run(cmd, {'show-status':True}, ['/a']) # doctest: +NORMALIZE_WHITESPACE
64 >>> ret = ui.run(cmd, args=['/b', '/a'])
69 >>> ret = ui.run(cmd, {'show-status':True}, ['/a']) # doctest: +NORMALIZE_WHITESPACE
74 >>> ret = ui.run(cmd, {'show-summary':True}, ['/a']) # doctest: +NORMALIZE_WHITESPACE
79 >>> ret = ui.run(cmd, {'repair':True})
80 >>> ret = ui.run(cmd, {'remove':True}, ['/b', '/a'])
83 >>> ret = ui.run(cmd, {'remove':True}, ['/a', '/b'])
89 def __init__(self, *args, **kwargs):
90 libbe.command.Command.__init__(self, *args, **kwargs)
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'),
117 libbe.command.Argument(
118 name='bug-id', metavar='BUG-ID', default=None,
120 completion_callback=libbe.command.util.complete_bug_id),
121 libbe.command.Argument(
122 name='blocking-bug-id', metavar='BUG-ID', default=None,
124 completion_callback=libbe.command.util.complete_bug_id),
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
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])
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)
155 bugA, dummy_comment = libbe.command.util.bug_comment_from_user_id(
156 bugdir, params['bug-id'])
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))
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)
186 blocked_by = get_blocked_by(bugdir, bugA)
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)
195 print >> self.stdout, '%s blocks:' % bugA.id.user()
196 print >> self.stdout, \
197 '\n'.join([self.bug_string(_bug, params)
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)
209 def _long_help(self):
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).
214 To search for bugs blocked by a particular bug, try
215 $ be list --extra-strings BLOCKED-BY:<your-bug-uuid>
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
223 $ be list --severity -target
224 which will only follow and print dependencies with non-target severity.
226 If neither bug A nor B is specified, check for and repair the missing
227 side of any one-way links.
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
234 # internal helper functions
236 def _generate_blocks_string(blocked_bug):
237 return '%s%s' % (BLOCKS_TAG, blocked_bug.uuid)
239 def _generate_blocked_by_string(blocking_bug):
240 return '%s%s' % (BLOCKED_BY_TAG, blocking_bug.uuid)
242 def _parse_blocks_string(string):
243 assert string.startswith(BLOCKS_TAG)
244 return string[len(BLOCKS_TAG):]
246 def _parse_blocked_by_string(string):
247 assert string.startswith(BLOCKED_BY_TAG)
248 return string[len(BLOCKED_BY_TAG):]
250 def _add_remove_extra_string(bug, string, add):
251 estrs = bug.extra_strings
254 else: # remove the string
256 bug.extra_strings = estrs # reassign to notice change
258 def _get_blocks(bug):
260 for line in bug.extra_strings:
261 if line.startswith(BLOCKS_TAG):
262 uuids.append(_parse_blocks_string(line))
265 def _get_blocked_by(bug):
267 for line in bug.extra_strings:
268 if line.startswith(BLOCKED_BY_TAG):
269 uuids.append(_parse_blocked_by_string(line))
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)
280 # functions exposed to other modules
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)
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)
294 def get_blocks(bugdir, bug):
296 Return a list of bugs that the given bug blocks.
299 for uuid in _get_blocks(bug):
300 blocks.append(bugdir.bug_from_uuid(uuid))
303 def get_blocked_by(bugdir, bug):
305 Return a list of bugs blocking the given bug.
308 for uuid in _get_blocked_by(bug):
309 blocked_by.append(bugdir.bug_from_uuid(uuid))
312 def check_dependencies(bugdir, repair_broken_links=False):
314 Check that links are bi-directional for all bugs in bugdir.
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)
328 [(Bug(uuid='a'), Bug(uuid='b'))]
331 >>> good,repaired,broken = check_dependencies(bd, repair_broken_links=True)
337 [(Bug(uuid='a'), Bug(uuid='b'))]
341 if bugdir.storage != None:
342 bugdir.load_all_bugs()
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))
356 broken_links.append((bug, blocker))
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))
368 broken_links.append((blockee, bug))
370 good_links.append((blockee, bug))
371 return (good_links, fixed_links, broken_links)
373 class DependencyTree (object):
375 Note: should probably be DependencyDiGraph.
377 def __init__(self, bugdir, root_bug, depth_limit=0,
378 allowed_status_values=None,
379 allowed_severity_values=None):
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
386 def _build_tree(self, child_fn):
387 root = libbe.util.tree.Tree()
388 root.bug = self.root_bug
391 while len(stack) > 0:
393 if self.depth_limit > 0 and node.depth == self.depth_limit:
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:
399 if self.allowed_severity_values != None \
400 and not bug.severity in self.allowed_severity_values:
402 child = libbe.util.tree.Tree()
404 child.depth = node.depth+1
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
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