~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to graph.py

  • Committer: Aaron Bentley
  • Date: 2007-03-09 18:33:22 UTC
  • mfrom: (517.1.4 bzrtools-0.15)
  • Revision ID: abentley@panoramicfeedback.com-20070309183322-lsjv4t29wkppzda7
Merge fixes from 0.15

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2008 Aaron Bentley
2
 
# <aaron@aaronbentley.com>
 
1
# Copyright (C) 2005 Aaron Bentley
 
2
# <aaron.bentley@utoronto.ca>
3
3
#
4
4
#    This program is free software; you can redistribute it and/or modify
5
5
#    it under the terms of the GNU General Public License as published by
14
14
#    You should have received a copy of the GNU General Public License
15
15
#    along with this program; if not, write to the Free Software
16
16
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17
 
 
18
 
 
 
17
from bzrtools import short_committer
 
18
from dotgraph import Node, dot_output, invoke_dot, invoke_dot_aa, NoDot, NoRsvg
 
19
from dotgraph import RSVG_OUTPUT_TYPES, DOT_OUTPUT_TYPES, Edge, invoke_dot_html
 
20
from bzrlib.branch import Branch
 
21
from bzrlib.errors import BzrCommandError, NoCommonRoot, NoSuchRevision
 
22
from bzrlib.graph import node_distances, select_farthest
 
23
from bzrlib.revision import combined_graph, revision_graph
 
24
from bzrlib.revision import MultipleRevisionSources
 
25
import bzrlib.errors
 
26
import re
 
27
import os.path
19
28
import time
20
29
 
21
 
from bzrlib.branch import Branch
22
 
from bzrlib.errors import BzrCommandError, NoSuchRevision
23
 
from bzrlib.revision import NULL_REVISION
24
 
 
25
 
from bzrtools import short_committer
26
 
from dotgraph import (
27
 
    dot_output,
28
 
    DOT_OUTPUT_TYPES,
29
 
    Edge,
30
 
    invoke_dot,
31
 
    invoke_dot_aa,
32
 
    invoke_dot_html,
33
 
    Node,
34
 
    NoDot,
35
 
    NoRsvg,
36
 
    RSVG_OUTPUT_TYPES,
37
 
    )
38
 
 
39
 
 
40
 
def max_distance(node, ancestors, distances, root_descendants):
41
 
    """Calculate the max distance to an ancestor.
42
 
    Return None if not all possible ancestors have known distances"""
43
 
    best = None
44
 
    if node in distances:
45
 
        best = distances[node]
46
 
    for ancestor in ancestors[node]:
47
 
        # skip ancestors we will never traverse:
48
 
        if root_descendants is not None and ancestor not in root_descendants:
49
 
            continue
50
 
        # An ancestor which is not listed in ancestors will never be in
51
 
        # distances, so we pretend it never existed.
52
 
        if ancestor not in ancestors:
53
 
            continue
54
 
        if ancestor not in distances:
55
 
            return None
56
 
        if best is None or distances[ancestor]+1 > best:
57
 
            best = distances[ancestor] + 1
58
 
    return best
59
 
 
60
 
 
61
 
def node_distances(graph, ancestors, start, root_descendants=None):
62
 
    """Produce a list of nodes, sorted by distance from a start node.
63
 
    This is an algorithm devised by Aaron Bentley, because applying Dijkstra
64
 
    backwards seemed too complicated.
65
 
 
66
 
    For each node, we walk its descendants.  If all the descendant's ancestors
67
 
    have a max-distance-to-start, (excluding ones that can never reach start),
68
 
    we calculate their max-distance-to-start, and schedule their descendants.
69
 
 
70
 
    So when a node's last parent acquires a distance, it will acquire a
71
 
    distance on the next iteration.
72
 
 
73
 
    Once we know the max distances for all nodes, we can return a list sorted
74
 
    by distance, farthest first.
75
 
    """
76
 
    distances = {start: 0}
77
 
    lines = set([start])
78
 
    while len(lines) > 0:
79
 
        new_lines = set()
80
 
        for line in lines:
81
 
            line_descendants = graph[line]
82
 
            for descendant in line_descendants:
83
 
                distance = max_distance(descendant, ancestors, distances,
84
 
                                        root_descendants)
85
 
                if distance is None:
86
 
                    continue
87
 
                distances[descendant] = distance
88
 
                new_lines.add(descendant)
89
 
        lines = new_lines
90
 
    return distances
91
 
 
92
 
 
93
 
def nodes_by_distance(distances):
94
 
    """Return a list of nodes sorted by distance"""
95
 
    def by_distance(n):
96
 
        return distances[n],n
97
 
 
98
 
    node_list = distances.keys()
99
 
    node_list.sort(key=by_distance, reverse=True)
100
 
    return node_list
101
 
 
102
 
 
103
 
def select_farthest(distances, common):
104
 
    """Return the farthest common node, or None if no node qualifies."""
105
 
    node_list = nodes_by_distance(distances)
106
 
    for node in node_list:
107
 
        if node in common:
108
 
            return node
109
 
 
110
 
 
111
30
mail_map = {'aaron.bentley@utoronto.ca'     : 'Aaron Bentley',
112
31
            'abentley@panoramicfeedback.com': 'Aaron Bentley',
113
32
            'abentley@lappy'                : 'Aaron Bentley',
137
56
    for me, my_parents in ancestors.iteritems():
138
57
        if me in skip:
139
58
            continue
140
 
        new_ancestors[me] = {}
 
59
        new_ancestors[me] = {} 
141
60
        for parent in my_parents:
142
 
            new_parent = parent
 
61
            new_parent = parent 
143
62
            distance = 0
144
63
            while can_skip(new_parent, descendants, ancestors):
145
64
                if new_parent in exceptions:
150
69
                new_parent = list(ancestors[new_parent])[0]
151
70
                distance += 1
152
71
            new_ancestors[me][new_parent] = distance
153
 
    return new_ancestors
 
72
    return new_ancestors    
154
73
 
155
74
def get_rev_info(rev_id, source):
156
75
    """Return the committer, message, and date of a revision."""
187
106
    return committer, message, nick, date
188
107
 
189
108
class Grapher(object):
190
 
 
191
109
    def __init__(self, branch, other_branch=None):
192
110
        object.__init__(self)
193
111
        self.branch = branch
194
112
        self.other_branch = other_branch
 
113
        revision_a = self.branch.last_revision()
195
114
        if other_branch is not None:
196
 
            other_repo = other_branch.repository
 
115
            branch.fetch(other_branch)
197
116
            revision_b = self.other_branch.last_revision()
 
117
            try:
 
118
                self.root, self.ancestors, self.descendants, self.common = \
 
119
                    combined_graph(revision_a, revision_b,
 
120
                                   self.branch.repository)
 
121
            except bzrlib.errors.NoCommonRoot:
 
122
                raise bzrlib.errors.NoCommonAncestor(revision_a, revision_b)
198
123
        else:
199
 
            other_repo = None
200
 
            revision_b = None
201
 
        self.graph = self.branch.repository.get_graph(other_repo)
202
 
        revision_a = self.branch.last_revision()
203
 
        self.scan_graph(revision_a, revision_b)
 
124
            self.root, self.ancestors, self.descendants = \
 
125
                revision_graph(revision_a, branch.repository)
 
126
            self.common = []
 
127
 
204
128
        self.n_history = branch.revision_history()
205
 
        self.n_revnos = branch.get_revision_id_to_revno_map()
206
 
        self.distances = node_distances(self.descendants, self.ancestors,
 
129
        self.distances = node_distances(self.descendants, self.ancestors, 
207
130
                                        self.root)
208
131
        if other_branch is not None:
209
132
            self.base = select_farthest(self.distances, self.common)
210
 
            self.m_history = other_branch.revision_history()
211
 
            self.m_revnos = other_branch.get_revision_id_to_revno_map()
212
 
            self.new_base = self.graph.find_unique_lca(revision_a,
213
 
                                                       revision_b)
214
 
            self.lcas = self.graph.find_lca(revision_a, revision_b)
 
133
            self.m_history = other_branch.revision_history() 
215
134
        else:
216
135
            self.base = None
217
 
            self.new_base = None
218
 
            self.lcas = set()
219
136
            self.m_history = []
220
 
            self.m_revnos = {}
221
 
 
222
 
    def scan_graph(self, revision_a, revision_b):
223
 
        a_ancestors = dict(self.graph.iter_ancestry([revision_a]))
224
 
        self.ancestors = a_ancestors
225
 
        self.root = NULL_REVISION
226
 
        if revision_b is not None:
227
 
            b_ancestors = dict(self.graph.iter_ancestry([revision_b]))
228
 
            self.common = set(a_ancestors.keys())
229
 
            self.common.intersection_update(b_ancestors)
230
 
            self.ancestors.update(b_ancestors)
231
 
        else:
232
 
            self.common = []
233
 
            revision_b = None
234
 
        self.descendants = {}
235
 
        ghosts = set()
236
 
        for revision, parents in self.ancestors.iteritems():
237
 
            self.descendants.setdefault(revision, [])
238
 
            if parents is None:
239
 
                ghosts.add(revision)
240
 
                parents = [NULL_REVISION]
241
 
            for parent in parents:
242
 
                self.descendants.setdefault(parent, []).append(revision)
243
 
        for ghost in ghosts:
244
 
            self.ancestors[ghost] = [NULL_REVISION]
245
 
 
246
 
    @staticmethod
247
 
    def _get_revno_str(prefix, revno_map, revision_id):
248
 
        try:
249
 
            revno = revno_map[revision_id]
250
 
        except KeyError:
251
 
            return None
252
 
        return '%s%s' % (prefix, '.'.join(str(n) for n in revno))
253
137
 
254
138
    def dot_node(self, node, num):
255
139
        try:
261
145
        except ValueError:
262
146
            m_rev = None
263
147
        if (n_rev, m_rev) == (None, None):
264
 
            name = self._get_revno_str('r', self.n_revnos, node)
265
 
            if name is None:
266
 
                name = self._get_revno_str('R', self.m_revnos, node)
267
 
            if name is None:
268
 
                name = node[-5:]
 
148
            name = node[-5:]
269
149
            cluster = None
270
150
        elif n_rev == m_rev:
271
151
            name = "rR%d" % n_rev
291
171
            assert m_rev is not None
292
172
            cluster = "other_history"
293
173
            color = "#ff0000"
294
 
        if node in self.lcas:
295
 
            color = "#9933cc"
296
174
        if node == self.base:
297
 
            color = "#669933"
298
 
            if node == self.new_base:
299
 
                color = "#33ff33"
300
 
        if node == self.new_base:
301
 
            color = '#33cc99'
 
175
            color = "#33ff99"
302
176
 
303
177
        label = [name]
304
 
        committer, message, nick, date = get_rev_info(node,
 
178
        committer, message, nick, date = get_rev_info(node, 
305
179
                                                      self.branch.repository)
306
180
        if committer is not None:
307
181
            label.append(committer)
318
192
        else:
319
193
            rank = None
320
194
 
321
 
        d_node = Node("n%d" % num, color=color, label="\\n".join(label),
 
195
        d_node = Node("n%d" % num, color=color, label="\\n".join(label), 
322
196
                    rev_id=node, cluster=cluster, message=message,
323
197
                    date=date)
324
198
        d_node.rank = rank
327
201
            d_node.node_style.append('dotted')
328
202
 
329
203
        return d_node
330
 
 
 
204
       
331
205
    def get_relations(self, collapse=False, max_distance=None):
332
206
        dot_nodes = {}
333
207
        node_relations = []
334
208
        num = 0
335
209
        if collapse:
336
 
            exceptions = self.lcas.union([self.base, self.new_base])
337
 
            visible_ancestors = compact_ancestors(self.descendants,
338
 
                                                  self.ancestors,
339
 
                                                  exceptions)
 
210
            visible_ancestors = compact_ancestors(self.descendants, 
 
211
                                                  self.ancestors, (self.base,))
340
212
        else:
341
 
            visible_ancestors = {}
342
 
            for revision, parents in self.ancestors.iteritems():
343
 
                visible_ancestors[revision] = dict((p, 0) for p in parents)
 
213
            visible_ancestors = self.ancestors
344
214
        if max_distance is not None:
345
215
            min_distance = max(self.distances.values()) - max_distance
346
 
            visible_ancestors = dict((n, p) for n, p in
347
 
                                     visible_ancestors.iteritems() if
348
 
                                     self.distances[n] >= min_distance)
 
216
            visible_ancestors = dict((n, p) for n, p in visible_ancestors.iteritems() if
 
217
                    self.distances[n] >= min_distance)
349
218
        for node, parents in visible_ancestors.iteritems():
350
219
            if node not in dot_nodes:
351
220
                dot_nodes[node] = self.dot_node(node, num)
352
221
                num += 1
353
 
            for parent, skipped in parents.iteritems():
 
222
            if visible_ancestors is self.ancestors:
 
223
                parent_iter = ((f, 0) for f in parents)
 
224
            else:
 
225
                parent_iter = (f for f in parents.iteritems())
 
226
            for parent, skipped in parent_iter:
354
227
                if parent not in dot_nodes:
355
228
                    dot_nodes[parent] = self.dot_node(parent, num)
356
229
                    num += 1
386
259
    done = False
387
260
    if ext not in RSVG_OUTPUT_TYPES:
388
261
        antialias = False
389
 
    if antialias:
 
262
    if antialias: 
390
263
        output = list(output)
391
264
        try:
392
265
            invoke_dot_aa(output, filename, ext)
404
277
            done = True
405
278
        except NoDot, e:
406
279
            raise BzrCommandError("Can't find 'dot'.  Please ensure Graphviz"\
407
 
                " is installed correctly.")
 
280
                " is installed correctly, or use --noantialias")
408
281
    elif ext == 'dot' and not done:
409
282
        my_file = file(filename, 'wb')
410
283
        for fragment in output:
414
287
            invoke_dot_html(output, filename)
415
288
        except NoDot, e:
416
289
            raise BzrCommandError("Can't find 'dot'.  Please ensure Graphviz"\
417
 
                " is installed correctly.")
 
290
                " is installed correctly, or use --noantialias")
418
291
    elif not done:
419
292
        print "Unknown file extension: %s" % ext
 
293