~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to graph.py

  • Committer: Aaron Bentley
  • Date: 2012-01-20 02:05:50 UTC
  • Revision ID: aaron@aaronbentley.com-20120120020550-itufdyymm5j1t1w2
RemoveĀ unusedĀ imports

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005 Aaron Bentley
2
 
# <aaron.bentley@utoronto.ca>
 
1
# Copyright (C) 2005, 2008 Aaron Bentley
 
2
# <aaron@aaronbentley.com>
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
 
from dotgraph import Node, dot_output, invoke_dot, invoke_dot_aa, NoDot, NoRsvg
18
 
from dotgraph import RSVG_OUTPUT_TYPES, DOT_OUTPUT_TYPES, Edge, invoke_dot_html
 
17
 
 
18
 
 
19
import time
 
20
 
19
21
from bzrlib.branch import Branch
20
 
from bzrlib.errors import BzrCommandError, NoCommonRoot, NoSuchRevision
21
 
from bzrlib.fetch import greedy_fetch
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
28
 
import time
 
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
 
29
110
 
30
111
mail_map = {'aaron.bentley@utoronto.ca'     : 'Aaron Bentley',
31
112
            'abentley@panoramicfeedback.com': 'Aaron Bentley',
36
117
            }
37
118
 
38
119
committer_alias = {'abentley': 'Aaron Bentley'}
39
 
def short_committer(committer):
40
 
    new_committer = re.sub('<.*>', '', committer).strip(' ')
41
 
    if len(new_committer) < 2:
42
 
        return committer
43
 
    return new_committer
44
 
 
45
120
def can_skip(rev_id, descendants, ancestors):
46
121
    if rev_id not in descendants:
47
122
        return False
62
137
    for me, my_parents in ancestors.iteritems():
63
138
        if me in skip:
64
139
            continue
65
 
        new_ancestors[me] = {} 
 
140
        new_ancestors[me] = {}
66
141
        for parent in my_parents:
67
 
            new_parent = parent 
 
142
            new_parent = parent
68
143
            distance = 0
69
144
            while can_skip(new_parent, descendants, ancestors):
70
145
                if new_parent in exceptions:
75
150
                new_parent = list(ancestors[new_parent])[0]
76
151
                distance += 1
77
152
            new_ancestors[me][new_parent] = distance
78
 
    return new_ancestors    
 
153
    return new_ancestors
79
154
 
80
155
def get_rev_info(rev_id, source):
81
 
    """Return the committer, message, and date of a revision."""
 
156
    """Return the committer, message, nick and date of a revision."""
82
157
    committer = None
83
158
    message = None
84
159
    date = None
 
160
    nick = None
85
161
    if rev_id == 'null:':
86
 
        return None, 'Null Revision', None
 
162
        return None, 'Null Revision', None, None
87
163
    try:
88
164
        rev = source.get_revision(rev_id)
89
165
    except NoSuchRevision:
90
166
        try:
91
167
            committer = '-'.join(rev_id.split('-')[:-2]).strip(' ')
92
168
            if committer == '':
93
 
                return None, None, None
 
169
                return None, None, None, None
94
170
        except ValueError:
95
 
            return None, None, None
 
171
            return None, None, None, None
96
172
    else:
97
173
        committer = short_committer(rev.committer)
98
174
        if rev.message is not None:
99
175
            message = rev.message.split('\n')[0]
100
176
        gmtime = time.gmtime(rev.timestamp + (rev.timezone or 0))
101
177
        date = time.strftime('%Y/%m/%d', gmtime)
 
178
        nick = rev.properties.get('branch-nick')
102
179
    if '@' in committer:
103
180
        try:
104
181
            committer = mail_map[committer]
108
185
        committer = committer_alias[committer]
109
186
    except KeyError:
110
187
        pass
111
 
    return committer, message, date
 
188
    return committer, message, nick, date
112
189
 
113
190
class Grapher(object):
 
191
 
114
192
    def __init__(self, branch, other_branch=None):
115
193
        object.__init__(self)
116
194
        self.branch = branch
117
195
        self.other_branch = other_branch
 
196
        if other_branch is not None:
 
197
            other_repo = other_branch.repository
 
198
            revision_b = self.other_branch.last_revision()
 
199
        else:
 
200
            other_repo = None
 
201
            revision_b = None
 
202
        self.graph = self.branch.repository.get_graph(other_repo)
118
203
        revision_a = self.branch.last_revision()
119
 
        if other_branch is not None:
120
 
            greedy_fetch(branch, other_branch)
121
 
            revision_b = self.other_branch.last_revision()
122
 
            try:
123
 
                self.root, self.ancestors, self.descendants, self.common = \
124
 
                    combined_graph(revision_a, revision_b, self.branch)
125
 
            except bzrlib.errors.NoCommonRoot:
126
 
                raise bzrlib.errors.NoCommonAncestor(revision_a, revision_b)
127
 
        else:
128
 
            self.root, self.ancestors, self.descendants = \
129
 
                revision_graph(revision_a, branch)
130
 
            self.common = []
131
 
 
 
204
        self.scan_graph(revision_a, revision_b)
132
205
        self.n_history = branch.revision_history()
133
 
        self.distances = node_distances(self.descendants, self.ancestors, 
 
206
        self.n_revnos = branch.get_revision_id_to_revno_map()
 
207
        self.distances = node_distances(self.descendants, self.ancestors,
134
208
                                        self.root)
135
209
        if other_branch is not None:
136
210
            self.base = select_farthest(self.distances, self.common)
137
 
            self.m_history = other_branch.revision_history() 
 
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)
138
216
        else:
139
217
            self.base = None
 
218
            self.new_base = None
 
219
            self.lcas = set()
140
220
            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))
141
254
 
142
255
    def dot_node(self, node, num):
143
256
        try:
149
262
        except ValueError:
150
263
            m_rev = None
151
264
        if (n_rev, m_rev) == (None, None):
152
 
            name = node[-5:]
 
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:]
153
270
            cluster = None
154
271
        elif n_rev == m_rev:
155
272
            name = "rR%d" % n_rev
175
292
            assert m_rev is not None
176
293
            cluster = "other_history"
177
294
            color = "#ff0000"
 
295
        if node in self.lcas:
 
296
            color = "#9933cc"
178
297
        if node == self.base:
179
 
            color = "#33ff99"
 
298
            color = "#669933"
 
299
            if node == self.new_base:
 
300
                color = "#33ff33"
 
301
        if node == self.new_base:
 
302
            color = '#33cc99'
180
303
 
181
304
        label = [name]
182
 
        committer, message, date = get_rev_info(node, self.branch)
 
305
        committer, message, nick, date = get_rev_info(node,
 
306
                                                      self.branch.repository)
183
307
        if committer is not None:
184
308
            label.append(committer)
185
309
 
 
310
        if nick is not None:
 
311
            label.append(nick)
 
312
 
186
313
        if date is not None:
187
314
            label.append(date)
188
315
 
192
319
        else:
193
320
            rank = None
194
321
 
195
 
        d_node = Node("n%d" % num, color=color, label="\\n".join(label), 
 
322
        d_node = Node("n%d" % num, color=color, label="\\n".join(label),
196
323
                    rev_id=node, cluster=cluster, message=message,
197
324
                    date=date)
198
325
        d_node.rank = rank
201
328
            d_node.node_style.append('dotted')
202
329
 
203
330
        return d_node
204
 
        
205
 
    def get_relations(self, collapse=False):
 
331
 
 
332
    def get_relations(self, collapse=False, max_distance=None):
206
333
        dot_nodes = {}
207
334
        node_relations = []
208
335
        num = 0
209
336
        if collapse:
210
 
            visible_ancestors = compact_ancestors(self.descendants, 
211
 
                                                  self.ancestors, (self.base,))
 
337
            exceptions = self.lcas.union([self.base, self.new_base])
 
338
            visible_ancestors = compact_ancestors(self.descendants,
 
339
                                                  self.ancestors,
 
340
                                                  exceptions)
212
341
        else:
213
 
            visible_ancestors = self.ancestors
 
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)
214
350
        for node, parents in visible_ancestors.iteritems():
215
351
            if node not in dot_nodes:
216
352
                dot_nodes[node] = self.dot_node(node, num)
217
353
                num += 1
218
 
            if visible_ancestors is self.ancestors:
219
 
                parent_iter = ((f, 0) for f in parents)
220
 
            else:
221
 
                parent_iter = (f for f in parents.iteritems())
222
 
            for parent, skipped in parent_iter:
 
354
            for parent, skipped in parents.iteritems():
223
355
                if parent not in dot_nodes:
224
356
                    dot_nodes[parent] = self.dot_node(parent, num)
225
357
                    num += 1
231
363
 
232
364
 
233
365
def write_ancestry_file(branch, filename, collapse=True, antialias=True,
234
 
                        merge_branch=None, ranking="forced"):
 
366
                        merge_branch=None, ranking="forced", max_distance=None):
235
367
    b = Branch.open_containing(branch)[0]
236
368
    if merge_branch is not None:
237
369
        m = Branch.open_containing(merge_branch)[0]
238
370
    else:
239
371
        m = None
240
 
    b.lock_read()
 
372
    b.lock_write()
241
373
    try:
242
374
        if m is not None:
243
375
            m.lock_read()
244
376
        try:
245
377
            grapher = Grapher(b, m)
246
 
            relations = grapher.get_relations(collapse)
 
378
            relations = grapher.get_relations(collapse, max_distance)
247
379
        finally:
248
380
            if m is not None:
249
381
                m.unlock()
255
387
    done = False
256
388
    if ext not in RSVG_OUTPUT_TYPES:
257
389
        antialias = False
258
 
    if antialias: 
 
390
    if antialias:
259
391
        output = list(output)
260
392
        try:
261
393
            invoke_dot_aa(output, filename, ext)
273
405
            done = True
274
406
        except NoDot, e:
275
407
            raise BzrCommandError("Can't find 'dot'.  Please ensure Graphviz"\
276
 
                " is installed correctly, or use --noantialias")
 
408
                " is installed correctly.")
277
409
    elif ext == 'dot' and not done:
278
410
        my_file = file(filename, 'wb')
279
411
        for fragment in output:
280
 
            my_file.write(fragment)
 
412
            my_file.write(fragment.encode('utf-8'))
281
413
    elif ext == 'html':
282
414
        try:
283
415
            invoke_dot_html(output, filename)
284
416
        except NoDot, e:
285
417
            raise BzrCommandError("Can't find 'dot'.  Please ensure Graphviz"\
286
 
                " is installed correctly, or use --noantialias")
 
418
                " is installed correctly.")
287
419
    elif not done:
288
420
        print "Unknown file extension: %s" % ext
289