1
"""Directed graph production.
3
This module contains the code to produce an ordered directed graph of a
4
bzr branch, such as we display in the tree view at the top of the bzrk
8
__copyright__ = "Copyright 2005 Canonical Ltd."
9
__author__ = "Scott James Remnant <scott@ubuntu.com>"
11
from bzrlib.revision import NULL_REVISION
12
from bzrlib.tsort import merge_sort
15
def linegraph(graph, start_revs, maxnum=None, broken_line_length=None,
16
graph_data=True, mainline_only=False, root_progress=None):
17
"""Produce a directed graph of a bzr repository.
19
Returns a tuple of (line_graph, revid_index, columns_len) where
20
* line_graph is a list of tuples of (revid,
26
* revid_index is a dict of each revision with the key being the revid, and
27
the value the row index, and
28
* columns_len is the number of columns need to draw the line graph.
31
Node is a tuple of (column, colour) with column being a zero-indexed
32
column number of the graph that this revision represents and colour
33
being a zero-indexed colour (which doesn't specify any actual colour
34
in particular) to draw the node in.
36
Lines is a list of tuples which represent lines you should draw away
37
from the revision, if you also need to draw lines into the revision
38
you should use the lines list from the previous iteration. Each
39
typle in the list is in the form (start, end, colour) with start and
40
end being zero-indexed column numbers and colour as in node.
42
It's up to you how to actually draw the nodes and lines (straight,
43
curved, kinked, etc.) and to pick the actual colours for each index.
45
assert isinstance(start_revs, list)
46
def update_root_progress(step_number):
47
"""IFF our container received a root progress bar, then update it."""
48
if root_progress is not None:
49
root_progress.update(None, step_number)
54
update_root_progress(1)
55
progress_bar = ui.ui_factory.nested_progress_bar()
57
progress_bar.update("Arranging tree fragments")
58
for i, (revid, parent_revids) in enumerate(graph.iter_ancestry(start_revs)):
61
if parent_revids is None:
64
if parent_revids == (NULL_REVISION,):
65
graph_parents[revid] = ()
67
graph_parents[revid] = parent_revids
68
for parent in parent_revids:
69
graph_children.setdefault(parent, []).append(revid)
70
graph_children.setdefault(revid, [])
72
progress_bar.finished()
74
update_root_progress(2)
75
progress_bar = ui.ui_factory.nested_progress_bar()
77
progress_bar.update("Removing ghosts", 0, len(ghosts))
78
for i, ghost in enumerate(ghosts):
80
progress_bar.update(None, i)
81
for ghost_child in graph_children[ghost]:
82
graph_parents[ghost_child] = [p for p in graph_parents[ghost_child]
85
progress_bar.finished()
86
graph_parents["top:"] = start_revs
88
if len(graph_parents)>0:
89
merge_sorted_revisions = merge_sort(
94
merge_sorted_revisions = ()
97
merge_sorted_revisions = [elem for elem in merge_sorted_revisions \
100
assert merge_sorted_revisions[0][1] == "top:"
101
merge_sorted_revisions = merge_sorted_revisions[1:]
106
# This will hold an item for each "branch". For a revisions, the revsion
107
# number less the least significant digit is the branch_id, and used as the
108
# key for the dict. Hence revision with the same revsion number less the
109
# least significant digit are considered to be in the same branch line.
110
# e.g.: for revisions 290.12.1 and 290.12.2, the branch_id would be 290.12,
111
# and these two revisions will be in the same branch line. Each value is
112
# a list of rev_indexes in the branch.
117
update_root_progress(3)
118
progress_bar = ui.ui_factory.nested_progress_bar()
120
progress_bar.update("Finding nodes", 0, len(merge_sorted_revisions))
121
for (rev_index, (sequence_number,
125
end_of_merge)) in enumerate(merge_sorted_revisions):
127
if rev_index % 25 == 0:
128
progress_bar.update(None, rev_index)
129
if maxnum and rev_index >= maxnum:
131
revid_index[revid] = rev_index
133
parents = graph_parents[revid]
134
linegraph.append([revid,
142
revno_index[revno_sequence] = rev_index
144
branch_id = revno_sequence[0:-1]
147
if branch_id not in branch_lines:
149
branch_lines[branch_id] = branch_line
151
branch_line = branch_lines[branch_id]
153
branch_line.append(rev_index)
155
progress_bar.finished()
158
branch_ids = branch_lines.keys()
160
def branch_id_cmp(x, y):
161
"""Compaire branch_id's first by the number of digits, then reversed
167
return cmp(len_x, len_y)
169
branch_ids.sort(branch_id_cmp)
170
# This will hold a tuple of (child_index, parent_index, col_index) for each
171
# line that needs to be drawn. If col_index is not none, then the line is
172
# drawn along that column, else the the line can be drawn directly between
173
# the child and parent because either the child and parent are in the same
174
# branch line, or the child and parent are 1 row apart.
176
empty_column = [False for i in range(len(graph_parents))]
177
# This will hold a bit map for each cell. If the cell is true, then the
178
# cell allready contains a node or line. This use when deciding what column
179
# to place a branch line or line in, without it overlaping something else.
180
columns = [list(empty_column)]
183
update_root_progress(4)
184
progress_bar = ui.ui_factory.nested_progress_bar()
186
progress_bar.update("Organizing edges", 0, len(branch_ids))
187
for i, branch_id in enumerate(branch_ids):
189
progress_bar.update(None, i)
190
branch_line = branch_lines[branch_id]
192
# Find the col_index for the direct parent branch. This will be the
193
# starting point when looking for a free column.
196
if len(branch_id) > 1:
197
parent_revno = branch_id[0:-1]
198
if parent_revno in revno_index:
199
parent_index = revno_index[parent_revno]
200
parent_node = linegraph[parent_index][1]
202
parent_col_index = parent_node[0]
205
col_search_order = _branch_line_col_search_order(columns,
207
color = reduce(lambda x, y: x+y, branch_id, 0)
211
last_rev_index = None
212
for rev_index in branch_line:
214
if broken_line_length and \
215
rev_index - last_rev_index > broken_line_length:
216
line_range.append(last_rev_index+1)
217
line_range.append(rev_index-1)
219
line_range.extend(range(last_rev_index+1, rev_index))
221
line_range.append(rev_index)
222
last_rev_index = rev_index
225
if broken_line_length and \
226
parent_index - last_rev_index > broken_line_length:
227
line_range.append(last_rev_index+1)
229
line_range.extend(range(last_rev_index+1, parent_index))
231
col_index = _find_free_column(columns,
235
node = (col_index, color)
236
for rev_index in branch_line:
237
linegraph[rev_index][1] = node
238
columns[col_index][rev_index] = True
240
for rev_index in branch_line:
245
end_of_merge) = merge_sorted_revisions[rev_index]
247
linegraph[rev_index][4] = graph_children[revid]
248
col_index = linegraph[rev_index][1][0]
250
for parent_revid in graph_parents[revid]:
251
if parent_revid in revid_index:
253
parent_index = revid_index[parent_revid]
254
parent_node = linegraph[parent_index][1]
256
parent_col_index = parent_node[0]
258
parent_col_index = None
260
_line_col_search_order(columns,
264
# If this line is really long, break it.
265
if len(branch_id) > 0 and \
266
broken_line_length and \
267
parent_index - rev_index > broken_line_length:
268
child_line_col_index = \
269
_find_free_column(columns,
273
_mark_column_as_used(columns,
274
child_line_col_index,
277
# Recall _line_col_search_order to reset it back to
280
_line_col_search_order(columns,
283
parent_col_line_index = \
284
_find_free_column(columns,
288
_mark_column_as_used(columns,
289
parent_col_line_index,
291
lines.append((rev_index,
293
(child_line_col_index,
294
parent_col_line_index)))
296
line_col_index = col_index
297
if parent_index - rev_index >1:
298
line_range = range(rev_index + 1, parent_index)
300
_find_free_column(columns,
304
_mark_column_as_used(columns,
307
lines.append((rev_index,
311
progress_bar.finished()
313
update_root_progress(5)
314
progress_bar = ui.ui_factory.nested_progress_bar()
316
progress_bar.update("Prettifying graph", 0, len(lines))
317
for i, (child_index, parent_index, line_col_indexes) in enumerate(lines):
319
progress_bar.update(None, i)
320
(child_col_index, child_color) = linegraph[child_index][1]
321
(parent_col_index, parent_color) = linegraph[parent_index][1]
323
if len(line_col_indexes) == 1:
324
if parent_index - child_index == 1:
325
linegraph[child_index][2].append(
330
# line from the child's column to the lines column
331
linegraph[child_index][2].append(
335
# lines down the line's column
336
for line_part_index in range(child_index+1, parent_index-1):
337
linegraph[line_part_index][2].append(
338
(line_col_indexes[0],
341
# line from the line's column to the parent's column
342
linegraph[parent_index-1][2].append(
343
(line_col_indexes[0],
348
# line from the child's column to the lines column
349
linegraph[child_index][2].append(
354
linegraph[child_index+1][2].append(
355
(line_col_indexes[0],
360
linegraph[parent_index-2][2].append(
364
# line from the line's column to the parent's column
365
linegraph[parent_index-1][2].append(
366
(line_col_indexes[1],
370
progress_bar.finished()
371
return (linegraph, revid_index, len(columns))
373
return (linegraph, revid_index, 0)
376
def _branch_line_col_search_order(columns, parent_col_index):
377
for col_index in range(parent_col_index, len(columns)):
379
for col_index in range(parent_col_index-1, -1, -1):
382
def _line_col_search_order(columns, parent_col_index, child_col_index):
383
if parent_col_index is not None:
384
max_index = max(parent_col_index, child_col_index)
385
min_index = min(parent_col_index, child_col_index)
386
for col_index in range(max_index, min_index -1, -1):
389
max_index = child_col_index
390
min_index = child_col_index
391
yield child_col_index
393
while max_index + i < len(columns) or \
395
if max_index + i < len(columns):
397
if min_index - i > -1:
401
def _find_free_column(columns, empty_column, col_search_order, line_range):
402
for col_index in col_search_order:
403
column = columns[col_index]
404
has_overlaping_line = False
405
for row_index in line_range:
406
if column[row_index]:
407
has_overlaping_line = True
409
if not has_overlaping_line:
412
col_index = len(columns)
413
column = list(empty_column)
414
columns.append(column)
417
def _mark_column_as_used(columns, col_index, line_range):
418
column = columns[col_index]
419
for row_index in line_range:
420
column[row_index] = True
422
def same_branch(a, b):
423
"""Return whether we think revisions a and b are on the same branch."""
424
if len(a.parent_ids) == 1:
425
# Defacto same branch if only parent
427
elif a.committer == b.committer:
428
# Same committer so may as well be