1
# -*- coding: UTF-8 -*-
 
 
2
"""Directed graph production.
 
 
4
This module contains the code to produce an ordered directed graph of a
 
 
5
bzr branch, such as we display in the tree view at the top of the bzrk
 
 
9
__copyright__ = "Copyright © 2005 Canonical Ltd."
 
 
10
__author__    = "Scott James Remnant <scott@ubuntu.com>"
 
 
12
from bzrlib.revision import NULL_REVISION
 
 
13
from bzrlib.tsort import merge_sort
 
 
15
def linegraph(repository, start_revs, maxnum, broken_line_length = None,
 
 
16
              graph_data = True, mainline_only = False):
 
 
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.
 
 
46
    graph = repository.get_graph()
 
 
50
    for (revid, parent_revids) in graph.iter_ancestry(start_revs):
 
 
51
        if parent_revids is None:
 
 
54
        if parent_revids == (NULL_REVISION,):
 
 
55
            graph_parents[revid] = ()
 
 
57
            graph_parents[revid] = parent_revids
 
 
58
        for parent in parent_revids:
 
 
59
            graph_children.setdefault(parent, []).append(revid)
 
 
60
        graph_children.setdefault(revid, [])
 
 
62
        for ghost_child in graph_children[ghost]:
 
 
63
            graph_parents[ghost_child] = [p for p in graph_parents[ghost_child]
 
 
65
    graph_parents["top:"] = start_revs
 
 
67
    if len(graph_parents)>0:
 
 
68
        merge_sorted_revisions = merge_sort(
 
 
73
        merge_sorted_revisions = ()
 
 
76
        merge_sorted_revisions = [elem for elem in merge_sorted_revisions \
 
 
79
    assert merge_sorted_revisions[0][1] == "top:"
 
 
80
    merge_sorted_revisions = merge_sorted_revisions[1:]
 
 
85
    # This will hold an item for each "branch". For a revisions, the revsion
 
 
86
    # number less the least significant digit is the branch_id, and used as the
 
 
87
    # key for the dict. Hence revision with the same revsion number less the
 
 
88
    # least significant digit are considered to be in the same branch line.
 
 
89
    # e.g.: for revisions 290.12.1 and 290.12.2, the branch_id would be 290.12,
 
 
90
    # and these two revisions will be in the same branch line. Each value is
 
 
91
    # a list of rev_indexes in the branch.
 
 
96
    for (rev_index, (sequence_number,
 
 
100
                     end_of_merge)) in enumerate(merge_sorted_revisions):
 
 
101
        if maxnum and rev_index >= maxnum:
 
 
103
        revid_index[revid] = rev_index
 
 
105
        parents = graph_parents[revid]
 
 
106
        linegraph.append([revid,
 
 
114
            revno_index[revno_sequence] = rev_index
 
 
116
            branch_id = revno_sequence[0:-1]
 
 
119
            if branch_id not in branch_lines:
 
 
121
                branch_lines[branch_id] = branch_line
 
 
123
                branch_line = branch_lines[branch_id]
 
 
125
            branch_line.append(rev_index)        
 
 
128
        branch_ids = branch_lines.keys()
 
 
130
        def branch_id_cmp(x, y):
 
 
131
            """Compaire branch_id's first by the number of digits, then reversed
 
 
137
            return cmp(len_x, len_y)
 
 
139
        branch_ids.sort(branch_id_cmp)
 
 
140
        # This will hold a tuple of (child_index, parent_index, col_index) for each
 
 
141
        # line that needs to be drawn. If col_index is not none, then the line is
 
 
142
        # drawn along that column, else the the line can be drawn directly between
 
 
143
        # the child and parent because either the child and parent are in the same
 
 
144
        # branch line, or the child and parent are 1 row apart.
 
 
146
        empty_column = [False for i in range(len(graph_parents))]
 
 
147
        # This will hold a bit map for each cell. If the cell is true, then the
 
 
148
        # cell allready contains a node or line. This use when deciding what column
 
 
149
        # to place a branch line or line in, without it overlaping something else.
 
 
150
        columns = [list(empty_column)]
 
 
153
        for branch_id in branch_ids:
 
 
154
            branch_line = branch_lines[branch_id]
 
 
156
            # Find the col_index for the direct parent branch. This will be the
 
 
157
            # starting point when looking for a free column.
 
 
160
            if len(branch_id) > 1:
 
 
161
                parent_revno = branch_id[0:-1]
 
 
162
                if parent_revno in revno_index:
 
 
163
                    parent_index = revno_index[parent_revno]
 
 
164
                    parent_node = linegraph[parent_index][1]
 
 
166
                        parent_col_index = parent_node[0]
 
 
169
            col_search_order = _branch_line_col_search_order(columns,
 
 
171
            color = reduce(lambda x, y: x+y, branch_id, 0)
 
 
175
            last_rev_index = None
 
 
176
            for rev_index in branch_line:
 
 
178
                    if broken_line_length and \
 
 
179
                       rev_index - last_rev_index > broken_line_length:
 
 
180
                        line_range.append(last_rev_index+1)
 
 
181
                        line_range.append(rev_index-1)
 
 
183
                        line_range.extend(range(last_rev_index+1, rev_index))
 
 
185
                line_range.append(rev_index)
 
 
186
                last_rev_index = rev_index
 
 
189
                if broken_line_length and \
 
 
190
                   parent_index - last_rev_index > broken_line_length:
 
 
191
                    line_range.append(last_rev_index+1)
 
 
193
                    line_range.extend(range(last_rev_index+1, parent_index))
 
 
195
            col_index = _find_free_column(columns,
 
 
199
            node = (col_index, color)
 
 
200
            for rev_index in branch_line:
 
 
201
                linegraph[rev_index][1] = node
 
 
202
                columns[col_index][rev_index] = True
 
 
204
            for rev_index in branch_line:
 
 
209
                     end_of_merge) = merge_sorted_revisions[rev_index]
 
 
211
                linegraph[rev_index][4] = graph_children[revid]
 
 
212
                col_index = linegraph[rev_index][1][0]
 
 
214
                for parent_revid in graph_parents[revid]:
 
 
215
                    if parent_revid in revid_index:
 
 
217
                        parent_index = revid_index[parent_revid]                            
 
 
218
                        parent_node = linegraph[parent_index][1]
 
 
220
                            parent_col_index = parent_node[0]
 
 
222
                            parent_col_index = None
 
 
224
                                _line_col_search_order(columns,
 
 
228
                        # If this line is really long, break it.
 
 
229
                        if len(branch_id) > 0 and \
 
 
230
                           broken_line_length and \
 
 
231
                           parent_index - rev_index > broken_line_length:
 
 
232
                            child_line_col_index = \
 
 
233
                                _find_free_column(columns,
 
 
237
                            _mark_column_as_used(columns,
 
 
238
                                                 child_line_col_index,
 
 
241
                            # Recall _line_col_search_order to reset it back to
 
 
244
                                    _line_col_search_order(columns,
 
 
247
                            parent_col_line_index = \
 
 
248
                                _find_free_column(columns,
 
 
252
                            _mark_column_as_used(columns,
 
 
253
                                                 parent_col_line_index,
 
 
255
                            lines.append((rev_index,
 
 
257
                                          (child_line_col_index,
 
 
258
                                           parent_col_line_index)))
 
 
260
                            line_col_index = col_index
 
 
261
                            if parent_index - rev_index >1:
 
 
262
                                line_range = range(rev_index + 1, parent_index)
 
 
264
                                    _find_free_column(columns,
 
 
268
                                _mark_column_as_used(columns,
 
 
271
                            lines.append((rev_index,
 
 
275
        for (child_index, parent_index, line_col_indexes) in lines:
 
 
276
            (child_col_index, child_color) = linegraph[child_index][1]
 
 
277
            (parent_col_index, parent_color) = linegraph[parent_index][1]
 
 
279
            if len(line_col_indexes) == 1:
 
 
280
                if parent_index - child_index == 1:
 
 
281
                    linegraph[child_index][2].append(
 
 
286
                    # line from the child's column to the lines column
 
 
287
                    linegraph[child_index][2].append(
 
 
291
                    # lines down the line's column
 
 
292
                    for line_part_index in range(child_index+1, parent_index-1):
 
 
293
                        linegraph[line_part_index][2].append(
 
 
294
                            (line_col_indexes[0],   
 
 
297
                    # line from the line's column to the parent's column
 
 
298
                    linegraph[parent_index-1][2].append(
 
 
299
                        (line_col_indexes[0],
 
 
304
                # line from the child's column to the lines column
 
 
305
                linegraph[child_index][2].append(
 
 
310
                linegraph[child_index+1][2].append(
 
 
311
                    (line_col_indexes[0],
 
 
316
                linegraph[parent_index-2][2].append(
 
 
320
                # line from the line's column to the parent's column
 
 
321
                linegraph[parent_index-1][2].append(
 
 
322
                    (line_col_indexes[1],
 
 
325
        return (linegraph, revid_index, len(columns))
 
 
327
        return (linegraph, revid_index, 0)
 
 
330
def _branch_line_col_search_order(columns, parent_col_index):
 
 
331
    for col_index in range(parent_col_index, len(columns)):
 
 
333
    for col_index in range(parent_col_index-1, -1, -1):
 
 
336
def _line_col_search_order(columns, parent_col_index, child_col_index):
 
 
337
    if parent_col_index is not None:
 
 
338
        max_index = max(parent_col_index, child_col_index)
 
 
339
        min_index = min(parent_col_index, child_col_index)
 
 
340
        for col_index in range(max_index, min_index -1, -1):
 
 
343
        max_index = child_col_index
 
 
344
        min_index = child_col_index
 
 
345
        yield child_col_index
 
 
347
    while max_index + i < len(columns) or \
 
 
349
        if max_index + i < len(columns):
 
 
351
        if min_index - i > -1:
 
 
355
def _find_free_column(columns, empty_column, col_search_order, line_range):
 
 
356
    for col_index in col_search_order:
 
 
357
        column = columns[col_index]
 
 
358
        has_overlaping_line = False
 
 
359
        for row_index in line_range:
 
 
360
            if column[row_index]:
 
 
361
                has_overlaping_line = True
 
 
363
        if not has_overlaping_line:
 
 
366
        col_index = len(columns)
 
 
367
        column = list(empty_column)
 
 
368
        columns.append(column)
 
 
371
def _mark_column_as_used(columns, col_index, line_range):
 
 
372
    column = columns[col_index]
 
 
373
    for row_index in line_range:
 
 
374
        column[row_index] = True    
 
 
376
def same_branch(a, b):
 
 
377
    """Return whether we think revisions a and b are on the same branch."""
 
 
378
    if len(a.parent_ids) == 1:
 
 
379
        # Defacto same branch if only parent
 
 
381
    elif a.committer == b.committer:
 
 
382
        # Same committer so may as well be