/b-gtk/fix-viz

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/b-gtk/fix-viz
1 by Scott James Remnant
Commit the first version of bzrk.
1
#!/usr/bin/python
2
# -*- coding: UTF-8 -*-
3
"""Directed graph production.
4
5
This module contains the code to produce an ordered directed graph of a
6
bzr branch, such as we display in the tree view at the top of the bzrk
7
window.
8
"""
9
10
__copyright__ = "Copyright © 2005 Canonical Ltd."
11
__author__    = "Scott James Remnant <scott@ubuntu.com>"
12
13
14
from bzrlib.errors import NoSuchRevision
15
16
17
class DummyRevision(object):
18
    """Dummy bzr revision.
19
20
    Sometimes, especially in older bzr branches, a revision is referenced
21
    as the parent of another but not actually present in the branch's store.
22
    When this happens we use an instance of this class instead of the real
23
    Revision object (which we can't get).
24
    """
25
26
    def __init__(self, revid):
27
        self.revision_id = revid
28
        self.parent_ids = []
29
        self.committer = None
30
        self.message = self.revision_id
31
32
27 by David Allouche
refactor distances
33
class DistanceMethod(object):
34
35
    def __init__(self, branch, start):
36
        self.branch = branch
37
        self.start = start
38
        self.revisions = {}
39
        self.children = {}
40
        self.children_of_id = {start: set()}
41
        self.parent_ids_of = {}
42
        self.colours = { start: 0 }
43
        self.last_colour = 0
44
        self.direct_parent_of = {}
45
28 by David Allouche
optimise by filling caches first
46
    def fill_caches(self):
47
        branch = self.branch
48
        revisions = self.revisions
49
        todo = set([self.start])
50
        while todo:
51
            revid = todo.pop()
27 by David Allouche
refactor distances
52
            try:
35 by Jamie Wilkinson
[merge conflict] merge in 0.8pre API fix from Jelmer
53
                revision = branch.repository.get_revision(revid)
27 by David Allouche
refactor distances
54
            except NoSuchRevision:
55
                revision = DummyRevision(revid)
28 by David Allouche
optimise by filling caches first
56
            self.cache_revision(revid, revision)
57
            for parent_id in revision.parent_ids:
58
                if parent_id not in revisions:
59
                    todo.add(parent_id)
27 by David Allouche
refactor distances
60
28 by David Allouche
optimise by filling caches first
61
    def cache_revision(self, revid, revision):
27 by David Allouche
refactor distances
62
        "Set the caches for a newly retrieved revision."""
63
        # Build a revision cache
64
        self.revisions[revid] = revision
65
        # Build a children dictionnary
1 by Scott James Remnant
Commit the first version of bzrk.
66
        for parent_id in revision.parent_ids:
27 by David Allouche
refactor distances
67
            self.children_of_id.setdefault(parent_id, set()).add(revision)
68
        # Build a parents dictionnary, where redundant parents will be removed,
69
        # and that will be passed along tothe rest of program.
20 by David Allouche
ignore redundent parents
70
        if len(revision.parent_ids) == len(set(revision.parent_ids)):
27 by David Allouche
refactor distances
71
            self.parent_ids_of[revision] = list(revision.parent_ids)
20 by David Allouche
ignore redundent parents
72
        else:
27 by David Allouche
refactor distances
73
            # Remove duplicate parents
20 by David Allouche
ignore redundent parents
74
            parent_ids = []
75
            parent_ids_set = set()
76
            for parent_id in revision.parent_ids:
77
                if parent_id in parent_ids_set:
78
                    continue
79
                parent_ids.append(parent_id)
80
                parent_ids_set.add(parent_id)
27 by David Allouche
refactor distances
81
            self.parent_ids_of[revision] = parent_ids
82
83
    def make_children_map(self):
84
        revisions = self.revisions
85
        return dict((revisions[revid], c)
86
                    for (revid, c) in self.children_of_id.iteritems())
87
88
    def first_ancestry_traversal(self):
31 by David Allouche
fix a bug with fast sorting
89
        distances = {}
29 by David Allouche
optimise initial sorting
90
        todo = [self.start]
28 by David Allouche
optimise by filling caches first
91
        revisions = self.revisions
29 by David Allouche
optimise initial sorting
92
        children_of_id = self.children_of_id
27 by David Allouche
refactor distances
93
        while todo:
29 by David Allouche
optimise initial sorting
94
            revid = todo.pop(0)
95
            for child in children_of_id[revid]:
96
                if child.revision_id not in distances:
97
                    todo.append(revid)
98
                    break
99
            else:
100
                distances[revid] = len(distances)
101
                for parent_id in revisions[revid].parent_ids:
102
                    if parent_id not in todo:
103
                        todo.insert(0, parent_id)
27 by David Allouche
refactor distances
104
        # Topologically sorted revids, with the most recent revisions first.
105
        # A revision occurs only after all of its children.
31 by David Allouche
fix a bug with fast sorting
106
        self.distances = distances
27 by David Allouche
refactor distances
107
        return sorted(distances, key=distances.get)
108
109
    def remove_redundant_parents(self, sorted_revids):
110
        children_of_id = self.children_of_id
111
        revisions = self.revisions
112
        parent_ids_of = self.parent_ids_of
113
        
114
        # Count the number of children of each revision, so we can release
115
        # memory for ancestry data as soon as it's not going to be needed
116
        # anymore.
117
        pending_count_of = {}
118
        for parent_id, children in children_of_id.iteritems():
119
            pending_count_of[parent_id] = len(children)
120
121
        # Build the ancestry dictionnary by examining older revisions first,
122
        # and remove revision parents that are ancestors of other parents of
123
        # the same revision.
124
        ancestor_ids_of = {}
125
        for revid in reversed(sorted_revids):
126
            revision = revisions[revid]
127
            parent_ids = parent_ids_of[revision]
128
            # ignore candidate parents which are an ancestor of another parent,
129
            # but never ignore the leftmost parent
130
            redundant_ids = []
131
            ignorable_parent_ids = parent_ids[1:] # never ignore leftmost
132
            for candidate_id in ignorable_parent_ids: 
133
                for parent_id in list(parent_ids):
134
                    if candidate_id in ancestor_ids_of[parent_id]:
135
                        redundant_ids.append(candidate_id)
136
                        parent_ids.remove(candidate_id)
137
                        children_of_candidate = children_of_id[candidate_id]
138
                        children_of_candidate.remove(revision)
139
                        break
140
            # save the set of ancestors of that revision
141
            ancestor_ids = set(parent_ids)
142
            for parent_id in parent_ids:
143
                ancestor_ids.update(ancestor_ids_of[parent_id])
144
            ancestor_ids_of[revid] = ancestor_ids
145
            # discard ancestry data for revisions whose children are already
146
            # done
147
            for parent_id in parent_ids + redundant_ids:
148
                pending_count = pending_count_of[parent_id] - 1
149
                pending_count_of[parent_id] = pending_count
150
                if pending_count == 0:
151
                    ancestor_ids_of[parent_id] = None
152
30 by David Allouche
separate sorting and colouring
153
    def sort_revisions(self, sorted_revids):
27 by David Allouche
refactor distances
154
        revisions = self.revisions
155
        parent_ids_of = self.parent_ids_of
156
        children_of_id = self.children_of_id
157
        # Try to compact sequences of revisions on the same branch.
158
        distances = {}
159
        skipped_revids = []
160
        expected_id = sorted_revids[0]
161
        pending_ids = []
162
        while True:
163
            revid = sorted_revids.pop(0)
164
            if revid != expected_id:
165
                skipped_revids.append(revid)
166
                continue
167
            revision = revisions[revid]
168
            for child in children_of_id[revid]:
169
                # postpone if any child is missing
170
                if child.revision_id not in distances:
171
                    if expected_id not in pending_ids:
172
                        pending_ids.append(expected_id)
173
                    expected_id = pending_ids.pop(0)
174
                    skipped_revids.append(revid)
175
                    sorted_revids[:0] = skipped_revids
30 by David Allouche
separate sorting and colouring
176
                    del skipped_revids[:]
27 by David Allouche
refactor distances
177
                    break
178
            else:
179
                # all children are here, push!
180
                distances[revid] = len(distances)
181
                # all parents will need to be pushed as soon as possible
182
                for parent in parent_ids_of[revision]:
183
                    if parent not in pending_ids:
184
                        pending_ids.insert(0, parent)
185
                if not pending_ids:
186
                    break
22 by David Allouche
sort revisions to be grouped by branch
187
                expected_id = pending_ids.pop(0)
27 by David Allouche
refactor distances
188
                # if the next expected revid has already been skipped, requeue
31 by David Allouche
fix a bug with fast sorting
189
                # the skipped ids, except those that would go right back to the
190
                # skipped list.
27 by David Allouche
refactor distances
191
                if expected_id in skipped_revids:
192
                    pos = skipped_revids.index(expected_id)
193
                    sorted_revids[:0] = skipped_revids[pos:]
194
                    del skipped_revids[pos:]
30 by David Allouche
separate sorting and colouring
195
        self.distances = distances
27 by David Allouche
refactor distances
196
        return sorted(distances, key=distances.get)
197
30 by David Allouche
separate sorting and colouring
198
    def choose_colour(self, revid):
199
        revision = self.revisions[revid]
27 by David Allouche
refactor distances
200
        children_of_id = self.children_of_id
201
        parent_ids_of = self.parent_ids_of
202
        colours = self.colours
203
        # choose colour
204
        the_children = children_of_id[revid]
205
        if len(the_children) == 1:
206
            [child] = the_children
207
            if len(parent_ids_of[child]) == 1:
208
                # one-one relationship between parent and child, same
209
                # colour
210
                colours[revid] = colours[child.revision_id]
211
            else:
212
                self.choose_colour_one_child(revision, child)
213
        else:
30 by David Allouche
separate sorting and colouring
214
            self.choose_colour_many_children(revision, the_children)
27 by David Allouche
refactor distances
215
216
    def choose_colour_one_child(self, revision, child):
217
        revid = revision.revision_id
218
        direct_parent_of = self.direct_parent_of
219
        revisions = self.revisions
220
        # one child with multiple parents, the first parent with
221
        # the same committer gets the colour
222
        direct_parent = direct_parent_of.get(child)
223
        if direct_parent is None:
224
            # if it has not been found yet, find it now and remember
225
            for parent_id in self.parent_ids_of[child]:
226
                parent_revision = revisions[parent_id]
227
                if parent_revision.committer == child.committer:
228
                    # found the first parent with the same committer
229
                    direct_parent = parent_revision
230
                    direct_parent_of[child] = direct_parent
231
                    break
232
        if direct_parent == revision:
233
            self.colours[revid] = self.colours[child.revision_id]
234
        else:
235
            self.colours[revid] = self.last_colour = self.last_colour + 1
236
30 by David Allouche
separate sorting and colouring
237
    def choose_colour_many_children(self, revision, the_children):
238
        distances = self.distances
27 by David Allouche
refactor distances
239
        revid = revision.revision_id
240
        direct_parent_of = self.direct_parent_of
241
        # multiple children, get the colour of the last displayed child
242
        # with the same committer which does not already have its colour
243
        # taken
244
        available = {}
245
        for child in the_children:
246
            if child.committer != revision.committer:
247
                continue
248
            direct_parent = direct_parent_of.get(child)
249
            if direct_parent == revision:
250
                self.colours[revid] = self.colours[child.revision_id]
251
                break
252
            if direct_parent is None:
253
                available[child] = distances[child.revision_id]
254
        else:
255
            if available:
256
                sorted_children = sorted(available, key=available.get)
257
                child = sorted_children[-1]
258
                direct_parent_of[child] = revision
259
                self.colours[revid] = self.colours[child.revision_id]
260
            else:
261
                # no candidate children is available, pick the next
262
                # colour
263
                self.colours[revid] = self.last_colour = self.last_colour + 1
264
265
33 by David Allouche
add --maxnum option to cut-off long histories
266
def distances(branch, start, robust, accurate, maxnum):
27 by David Allouche
refactor distances
267
    """Sort the revisions.
268
269
    Traverses the branch revision tree starting at start and produces an
270
    ordered list of revisions such that a revision always comes after
271
    any revision it is the parent of.
272
273
    Returns a tuple of (revids, revisions, colours, children)
274
    """
28 by David Allouche
optimise by filling caches first
275
    distance = DistanceMethod(branch, start)
276
    distance.fill_caches()
277
    sorted_revids = distance.first_ancestry_traversal()
32 by David Allouche
make expensive sorting and parent filtering optional
278
    if robust:
279
        print 'robust filtering'
280
        distance.remove_redundant_parents(sorted_revids)
29 by David Allouche
optimise initial sorting
281
    children = distance.make_children_map()
32 by David Allouche
make expensive sorting and parent filtering optional
282
    if accurate:
283
        print 'accurate sorting'
284
        sorted_revids = distance.sort_revisions(sorted_revids)
30 by David Allouche
separate sorting and colouring
285
    for revid in sorted_revids:
286
        distance.choose_colour(revid)
27 by David Allouche
refactor distances
287
33 by David Allouche
add --maxnum option to cut-off long histories
288
    if maxnum is not None:
289
        del sorted_revids[maxnum:]
290
28 by David Allouche
optimise by filling caches first
291
    revisions = distance.revisions
292
    colours = distance.colours
293
    parent_ids_of = distance.parent_ids_of
20 by David Allouche
ignore redundent parents
294
    return (sorted_revids, revisions, colours, children, parent_ids_of)
295
296
def graph(revids, revisions, colours, parent_ids):
3 by Scott James Remnant
Split the display in two with a pane, we'll use the bottom half to show
297
    """Produce a directed graph of a bzr branch.
298
299
    For each revision it then yields a tuple of (revision, node, lines).
300
    If the revision is only referenced in the branch and not present in the
301
    store, revision will be a DummyRevision object, otherwise it is the bzr
302
    Revision object with the meta-data for the revision.
303
304
    Node is a tuple of (column, colour) with column being a zero-indexed
305
    column number of the graph that this revision represents and colour
306
    being a zero-indexed colour (which doesn't specify any actual colour
307
    in particular) to draw the node in.
308
309
    Lines is a list of tuples which represent lines you should draw away
310
    from the revision, if you also need to draw lines into the revision
311
    you should use the lines list from the previous iteration.  Each
312
    typle in the list is in the form (start, end, colour) with start and
313
    end being zero-indexed column numbers and colour as in node.
314
315
    It's up to you how to actually draw the nodes and lines (straight,
316
    curved, kinked, etc.) and to pick the actual colours for each index.
317
    """
318
    hanging = revids[:1]
319
    for revid in revids:
1 by Scott James Remnant
Commit the first version of bzrk.
320
        lines = []
321
        node = None
322
323
        new_hanging = []
324
        for h_idx, hang in enumerate(hanging):
325
            if hang == revid:
326
                # We've matched a hanging revision, so need to output a node
327
                # at this point
328
                node = (h_idx, colours[revid])
329
330
                # Now we need to hang its parents, we put them at the point
331
                # the old column was so anything to the right of this has
332
                # to move outwards to make room.  We also try and collapse
333
                # hangs to keep the graph small.
20 by David Allouche
ignore redundent parents
334
                for parent_id in parent_ids[revisions[revid]]:
1 by Scott James Remnant
Commit the first version of bzrk.
335
                    try:
336
                        n_idx = new_hanging.index(parent_id)
337
                    except ValueError:
338
                        n_idx = len(new_hanging)
339
                        new_hanging.append(parent_id)
340
                    lines.append((h_idx, n_idx, colours[parent_id]))
341
            else:
342
                # Revision keeps on hanging, adjust for any change in the
343
                # graph shape and try to collapse hangs to keep the graph
344
                # small.
345
                try:
346
                    n_idx = new_hanging.index(hang)
347
                except ValueError:
348
                    n_idx = len(new_hanging)
349
                    new_hanging.append(hang)
350
                lines.append((h_idx, n_idx, colours[hang]))
351
        hanging = new_hanging
352
353
        yield (revisions[revid], node, lines)
2 by Scott James Remnant
Split the same branch functionality out into a separate function so
354
27 by David Allouche
refactor distances
355
2 by Scott James Remnant
Split the same branch functionality out into a separate function so
356
def same_branch(a, b):
357
    """Return whether we think revisions a and b are on the same branch."""
358
    if len(a.parent_ids) == 1:
359
        # Defacto same branch if only parent
360
        return True
361
    elif a.committer == b.committer:
362
        # Same committer so may as well be
363
        return True
364
    else:
365
        return False