~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to graph.py

  • Committer: Aaron Bentley
  • Date: 2006-02-21 05:08:07 UTC
  • Revision ID: aaron.bentley@utoronto.ca-20060221050807-3cc86585e1ec6edc
Updated to match API changes

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.fetch import greedy_fetch
 
23
from bzrlib.graph import node_distances, select_farthest
 
24
from bzrlib.revision import combined_graph, revision_graph
 
25
from bzrlib.revision import MultipleRevisionSources
 
26
import bzrlib.errors
 
27
import re
 
28
import os.path
19
29
import time
20
30
 
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
31
mail_map = {'aaron.bentley@utoronto.ca'     : 'Aaron Bentley',
112
32
            'abentley@panoramicfeedback.com': 'Aaron Bentley',
113
33
            'abentley@lappy'                : 'Aaron Bentley',
137
57
    for me, my_parents in ancestors.iteritems():
138
58
        if me in skip:
139
59
            continue
140
 
        new_ancestors[me] = {}
 
60
        new_ancestors[me] = {} 
141
61
        for parent in my_parents:
142
 
            new_parent = parent
 
62
            new_parent = parent 
143
63
            distance = 0
144
64
            while can_skip(new_parent, descendants, ancestors):
145
65
                if new_parent in exceptions:
150
70
                new_parent = list(ancestors[new_parent])[0]
151
71
                distance += 1
152
72
            new_ancestors[me][new_parent] = distance
153
 
    return new_ancestors
 
73
    return new_ancestors    
154
74
 
155
75
def get_rev_info(rev_id, source):
156
 
    """Return the committer, message, nick and date of a revision."""
 
76
    """Return the committer, message, and date of a revision."""
157
77
    committer = None
158
78
    message = None
159
79
    date = None
160
 
    nick = None
161
80
    if rev_id == 'null:':
162
 
        return None, 'Null Revision', None, None
 
81
        return None, 'Null Revision', None
163
82
    try:
164
83
        rev = source.get_revision(rev_id)
165
84
    except NoSuchRevision:
166
85
        try:
167
86
            committer = '-'.join(rev_id.split('-')[:-2]).strip(' ')
168
87
            if committer == '':
169
 
                return None, None, None, None
 
88
                return None, None, None
170
89
        except ValueError:
171
 
            return None, None, None, None
 
90
            return None, None, None
172
91
    else:
173
92
        committer = short_committer(rev.committer)
174
93
        if rev.message is not None:
175
94
            message = rev.message.split('\n')[0]
176
95
        gmtime = time.gmtime(rev.timestamp + (rev.timezone or 0))
177
96
        date = time.strftime('%Y/%m/%d', gmtime)
178
 
        nick = rev.properties.get('branch-nick')
179
97
    if '@' in committer:
180
98
        try:
181
99
            committer = mail_map[committer]
185
103
        committer = committer_alias[committer]
186
104
    except KeyError:
187
105
        pass
188
 
    return committer, message, nick, date
 
106
    return committer, message, date
189
107
 
190
108
class Grapher(object):
191
 
 
192
109
    def __init__(self, branch, other_branch=None):
193
110
        object.__init__(self)
194
111
        self.branch = branch
195
112
        self.other_branch = other_branch
 
113
        revision_a = self.branch.last_revision()
196
114
        if other_branch is not None:
197
 
            other_repo = other_branch.repository
 
115
            greedy_fetch(branch, other_branch)
198
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)
199
123
        else:
200
 
            other_repo = None
201
 
            revision_b = None
202
 
        self.graph = self.branch.repository.get_graph(other_repo)
203
 
        revision_a = self.branch.last_revision()
204
 
        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
 
205
128
        self.n_history = branch.revision_history()
206
 
        self.n_revnos = branch.get_revision_id_to_revno_map()
207
 
        self.distances = node_distances(self.descendants, self.ancestors,
 
129
        self.distances = node_distances(self.descendants, self.ancestors, 
208
130
                                        self.root)
209
131
        if other_branch is not None:
210
132
            self.base = select_farthest(self.distances, self.common)
211
 
            self.m_history = other_branch.revision_history()
212
 
            self.m_revnos = other_branch.get_revision_id_to_revno_map()
213
 
            self.new_base = self.graph.find_unique_lca(revision_a,
214
 
                                                       revision_b)
215
 
            self.lcas = self.graph.find_lca(revision_a, revision_b)
 
133
            self.m_history = other_branch.revision_history() 
216
134
        else:
217
135
            self.base = None
218
 
            self.new_base = None
219
 
            self.lcas = set()
220
136
            self.m_history = []
221
 
            self.m_revnos = {}
222
 
 
223
 
    def scan_graph(self, revision_a, revision_b):
224
 
        a_ancestors = dict(self.graph.iter_ancestry([revision_a]))
225
 
        self.ancestors = a_ancestors
226
 
        self.root = NULL_REVISION
227
 
        if revision_b is not None:
228
 
            b_ancestors = dict(self.graph.iter_ancestry([revision_b]))
229
 
            self.common = set(a_ancestors.keys())
230
 
            self.common.intersection_update(b_ancestors)
231
 
            self.ancestors.update(b_ancestors)
232
 
        else:
233
 
            self.common = []
234
 
            revision_b = None
235
 
        self.descendants = {}
236
 
        ghosts = set()
237
 
        for revision, parents in self.ancestors.iteritems():
238
 
            self.descendants.setdefault(revision, [])
239
 
            if parents is None:
240
 
                ghosts.add(revision)
241
 
                parents = [NULL_REVISION]
242
 
            for parent in parents:
243
 
                self.descendants.setdefault(parent, []).append(revision)
244
 
        for ghost in ghosts:
245
 
            self.ancestors[ghost] = [NULL_REVISION]
246
 
 
247
 
    @staticmethod
248
 
    def _get_revno_str(prefix, revno_map, revision_id):
249
 
        try:
250
 
            revno = revno_map[revision_id]
251
 
        except KeyError:
252
 
            return None
253
 
        return '%s%s' % (prefix, '.'.join(str(n) for n in revno))
254
137
 
255
138
    def dot_node(self, node, num):
256
139
        try:
262
145
        except ValueError:
263
146
            m_rev = None
264
147
        if (n_rev, m_rev) == (None, None):
265
 
            name = self._get_revno_str('r', self.n_revnos, node)
266
 
            if name is None:
267
 
                name = self._get_revno_str('R', self.m_revnos, node)
268
 
            if name is None:
269
 
                name = node[-5:]
 
148
            name = node[-5:]
270
149
            cluster = None
271
150
        elif n_rev == m_rev:
272
151
            name = "rR%d" % n_rev
292
171
            assert m_rev is not None
293
172
            cluster = "other_history"
294
173
            color = "#ff0000"
295
 
        if node in self.lcas:
296
 
            color = "#9933cc"
297
174
        if node == self.base:
298
 
            color = "#669933"
299
 
            if node == self.new_base:
300
 
                color = "#33ff33"
301
 
        if node == self.new_base:
302
 
            color = '#33cc99'
 
175
            color = "#33ff99"
303
176
 
304
177
        label = [name]
305
 
        committer, message, nick, date = get_rev_info(node,
306
 
                                                      self.branch.repository)
 
178
        committer, message, date = get_rev_info(node, self.branch.repository)
307
179
        if committer is not None:
308
180
            label.append(committer)
309
181
 
310
 
        if nick is not None:
311
 
            label.append(nick)
312
 
 
313
182
        if date is not None:
314
183
            label.append(date)
315
184
 
319
188
        else:
320
189
            rank = None
321
190
 
322
 
        d_node = Node("n%d" % num, color=color, label="\\n".join(label),
 
191
        d_node = Node("n%d" % num, color=color, label="\\n".join(label), 
323
192
                    rev_id=node, cluster=cluster, message=message,
324
193
                    date=date)
325
194
        d_node.rank = rank
328
197
            d_node.node_style.append('dotted')
329
198
 
330
199
        return d_node
331
 
 
332
 
    def get_relations(self, collapse=False, max_distance=None):
 
200
        
 
201
    def get_relations(self, collapse=False):
333
202
        dot_nodes = {}
334
203
        node_relations = []
335
204
        num = 0
336
205
        if collapse:
337
 
            exceptions = self.lcas.union([self.base, self.new_base])
338
 
            visible_ancestors = compact_ancestors(self.descendants,
339
 
                                                  self.ancestors,
340
 
                                                  exceptions)
 
206
            visible_ancestors = compact_ancestors(self.descendants, 
 
207
                                                  self.ancestors, (self.base,))
341
208
        else:
342
 
            visible_ancestors = {}
343
 
            for revision, parents in self.ancestors.iteritems():
344
 
                visible_ancestors[revision] = dict((p, 0) for p in parents)
345
 
        if max_distance is not None:
346
 
            min_distance = max(self.distances.values()) - max_distance
347
 
            visible_ancestors = dict((n, p) for n, p in
348
 
                                     visible_ancestors.iteritems() if
349
 
                                     self.distances[n] >= min_distance)
 
209
            visible_ancestors = self.ancestors
350
210
        for node, parents in visible_ancestors.iteritems():
351
211
            if node not in dot_nodes:
352
212
                dot_nodes[node] = self.dot_node(node, num)
353
213
                num += 1
354
 
            for parent, skipped in parents.iteritems():
 
214
            if visible_ancestors is self.ancestors:
 
215
                parent_iter = ((f, 0) for f in parents)
 
216
            else:
 
217
                parent_iter = (f for f in parents.iteritems())
 
218
            for parent, skipped in parent_iter:
355
219
                if parent not in dot_nodes:
356
220
                    dot_nodes[parent] = self.dot_node(parent, num)
357
221
                    num += 1
363
227
 
364
228
 
365
229
def write_ancestry_file(branch, filename, collapse=True, antialias=True,
366
 
                        merge_branch=None, ranking="forced", max_distance=None):
 
230
                        merge_branch=None, ranking="forced"):
367
231
    b = Branch.open_containing(branch)[0]
368
232
    if merge_branch is not None:
369
233
        m = Branch.open_containing(merge_branch)[0]
375
239
            m.lock_read()
376
240
        try:
377
241
            grapher = Grapher(b, m)
378
 
            relations = grapher.get_relations(collapse, max_distance)
 
242
            relations = grapher.get_relations(collapse)
379
243
        finally:
380
244
            if m is not None:
381
245
                m.unlock()
387
251
    done = False
388
252
    if ext not in RSVG_OUTPUT_TYPES:
389
253
        antialias = False
390
 
    if antialias:
 
254
    if antialias: 
391
255
        output = list(output)
392
256
        try:
393
257
            invoke_dot_aa(output, filename, ext)
405
269
            done = True
406
270
        except NoDot, e:
407
271
            raise BzrCommandError("Can't find 'dot'.  Please ensure Graphviz"\
408
 
                " is installed correctly.")
 
272
                " is installed correctly, or use --noantialias")
409
273
    elif ext == 'dot' and not done:
410
274
        my_file = file(filename, 'wb')
411
275
        for fragment in output:
412
 
            my_file.write(fragment.encode('utf-8'))
 
276
            my_file.write(fragment)
413
277
    elif ext == 'html':
414
278
        try:
415
279
            invoke_dot_html(output, filename)
416
280
        except NoDot, e:
417
281
            raise BzrCommandError("Can't find 'dot'.  Please ensure Graphviz"\
418
 
                " is installed correctly.")
 
282
                " is installed correctly, or use --noantialias")
419
283
    elif not done:
420
284
        print "Unknown file extension: %s" % ext
 
285