~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to graph.py

  • Committer: Aaron Bentley
  • Date: 2006-08-08 02:29:34 UTC
  • mto: This revision was merged to the branch mainline in revision 425.
  • Revision ID: aaron.bentley@utoronto.ca-20060808022934-c5e50b01aefe43d2
More updates for 0.9

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
 
    """Return the committer, message, nick and date of a revision."""
 
75
    """Return the committer, message, and date of a revision."""
157
76
    committer = None
158
77
    message = None
159
78
    date = None
160
 
    nick = None
161
79
    if rev_id == 'null:':
162
 
        return None, 'Null Revision', None, None
 
80
        return None, 'Null Revision', None
163
81
    try:
164
82
        rev = source.get_revision(rev_id)
165
83
    except NoSuchRevision:
166
84
        try:
167
85
            committer = '-'.join(rev_id.split('-')[:-2]).strip(' ')
168
86
            if committer == '':
169
 
                return None, None, None, None
 
87
                return None, None, None
170
88
        except ValueError:
171
 
            return None, None, None, None
 
89
            return None, None, None
172
90
    else:
173
91
        committer = short_committer(rev.committer)
174
92
        if rev.message is not None:
175
93
            message = rev.message.split('\n')[0]
176
94
        gmtime = time.gmtime(rev.timestamp + (rev.timezone or 0))
177
95
        date = time.strftime('%Y/%m/%d', gmtime)
178
 
        nick = rev.properties.get('branch-nick')
179
96
    if '@' in committer:
180
97
        try:
181
98
            committer = mail_map[committer]
185
102
        committer = committer_alias[committer]
186
103
    except KeyError:
187
104
        pass
188
 
    return committer, message, nick, date
 
105
    return committer, message, date
189
106
 
190
107
class Grapher(object):
191
 
 
192
108
    def __init__(self, branch, other_branch=None):
193
109
        object.__init__(self)
194
110
        self.branch = branch
195
111
        self.other_branch = other_branch
 
112
        revision_a = self.branch.last_revision()
196
113
        if other_branch is not None:
197
 
            other_repo = other_branch.repository
 
114
            branch.fetch(other_branch)
198
115
            revision_b = self.other_branch.last_revision()
 
116
            try:
 
117
                self.root, self.ancestors, self.descendants, self.common = \
 
118
                    combined_graph(revision_a, revision_b,
 
119
                                   self.branch.repository)
 
120
            except bzrlib.errors.NoCommonRoot:
 
121
                raise bzrlib.errors.NoCommonAncestor(revision_a, revision_b)
199
122
        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)
205
 
        self.n_history = list(self.graph.iter_lefthand_ancestry(revision_a))
206
 
        self.n_history.reverse()
207
 
        self.n_revnos = branch.get_revision_id_to_revno_map()
208
 
        self.distances = node_distances(self.descendants, self.ancestors,
 
123
            self.root, self.ancestors, self.descendants = \
 
124
                revision_graph(revision_a, branch.repository)
 
125
            self.common = []
 
126
 
 
127
        self.n_history = branch.revision_history()
 
128
        self.distances = node_distances(self.descendants, self.ancestors, 
209
129
                                        self.root)
210
130
        if other_branch is not None:
211
131
            self.base = select_farthest(self.distances, self.common)
212
 
            self.m_history = self.graph.iter_lefthand_ancestry(revision_b)
213
 
            self.m_history = list(self.m_history)
214
 
            self.m_history.reverse()
215
 
            self.m_revnos = other_branch.get_revision_id_to_revno_map()
216
 
            self.new_base = self.graph.find_unique_lca(revision_a,
217
 
                                                       revision_b)
218
 
            self.lcas = self.graph.find_lca(revision_a, revision_b)
 
132
            self.m_history = other_branch.revision_history() 
219
133
        else:
220
134
            self.base = None
221
 
            self.new_base = None
222
 
            self.lcas = set()
223
135
            self.m_history = []
224
 
            self.m_revnos = {}
225
 
 
226
 
    def scan_graph(self, revision_a, revision_b):
227
 
        a_ancestors = dict(self.graph.iter_ancestry([revision_a]))
228
 
        self.ancestors = a_ancestors
229
 
        self.root = NULL_REVISION
230
 
        if revision_b is not None:
231
 
            b_ancestors = dict(self.graph.iter_ancestry([revision_b]))
232
 
            self.common = set(a_ancestors.keys())
233
 
            self.common.intersection_update(b_ancestors)
234
 
            self.ancestors.update(b_ancestors)
235
 
        else:
236
 
            self.common = []
237
 
            revision_b = None
238
 
        self.descendants = {}
239
 
        ghosts = set()
240
 
        for revision, parents in self.ancestors.iteritems():
241
 
            self.descendants.setdefault(revision, [])
242
 
            if parents is None:
243
 
                ghosts.add(revision)
244
 
                parents = [NULL_REVISION]
245
 
            for parent in parents:
246
 
                self.descendants.setdefault(parent, []).append(revision)
247
 
        for ghost in ghosts:
248
 
            self.ancestors[ghost] = [NULL_REVISION]
249
 
 
250
 
    @staticmethod
251
 
    def _get_revno_str(prefix, revno_map, revision_id):
252
 
        try:
253
 
            revno = revno_map[revision_id]
254
 
        except KeyError:
255
 
            return None
256
 
        return '%s%s' % (prefix, '.'.join(str(n) for n in revno))
257
136
 
258
137
    def dot_node(self, node, num):
259
138
        try:
265
144
        except ValueError:
266
145
            m_rev = None
267
146
        if (n_rev, m_rev) == (None, None):
268
 
            name = self._get_revno_str('r', self.n_revnos, node)
269
 
            if name is None:
270
 
                name = self._get_revno_str('R', self.m_revnos, node)
271
 
            if name is None:
272
 
                name = node[-5:]
 
147
            name = node[-5:]
273
148
            cluster = None
274
149
        elif n_rev == m_rev:
275
150
            name = "rR%d" % n_rev
295
170
            assert m_rev is not None
296
171
            cluster = "other_history"
297
172
            color = "#ff0000"
298
 
        if node in self.lcas:
299
 
            color = "#9933cc"
300
173
        if node == self.base:
301
 
            color = "#669933"
302
 
            if node == self.new_base:
303
 
                color = "#33ff33"
304
 
        if node == self.new_base:
305
 
            color = '#33cc99'
 
174
            color = "#33ff99"
306
175
 
307
176
        label = [name]
308
 
        committer, message, nick, date = get_rev_info(node,
309
 
                                                      self.branch.repository)
 
177
        committer, message, date = get_rev_info(node, self.branch.repository)
310
178
        if committer is not None:
311
179
            label.append(committer)
312
180
 
313
 
        if nick is not None:
314
 
            label.append(nick)
315
 
 
316
181
        if date is not None:
317
182
            label.append(date)
318
183
 
322
187
        else:
323
188
            rank = None
324
189
 
325
 
        d_node = Node("n%d" % num, color=color, label="\\n".join(label),
 
190
        d_node = Node("n%d" % num, color=color, label="\\n".join(label), 
326
191
                    rev_id=node, cluster=cluster, message=message,
327
192
                    date=date)
328
193
        d_node.rank = rank
331
196
            d_node.node_style.append('dotted')
332
197
 
333
198
        return d_node
334
 
 
335
 
    def get_relations(self, collapse=False, max_distance=None):
 
199
        
 
200
    def get_relations(self, collapse=False):
336
201
        dot_nodes = {}
337
202
        node_relations = []
338
203
        num = 0
339
204
        if collapse:
340
 
            exceptions = self.lcas.union([self.base, self.new_base])
341
 
            visible_ancestors = compact_ancestors(self.descendants,
342
 
                                                  self.ancestors,
343
 
                                                  exceptions)
 
205
            visible_ancestors = compact_ancestors(self.descendants, 
 
206
                                                  self.ancestors, (self.base,))
344
207
        else:
345
 
            visible_ancestors = {}
346
 
            for revision, parents in self.ancestors.iteritems():
347
 
                visible_ancestors[revision] = dict((p, 0) for p in parents)
348
 
        if max_distance is not None:
349
 
            min_distance = max(self.distances.values()) - max_distance
350
 
            visible_ancestors = dict((n, p) for n, p in
351
 
                                     visible_ancestors.iteritems() if
352
 
                                     self.distances[n] >= min_distance)
 
208
            visible_ancestors = self.ancestors
353
209
        for node, parents in visible_ancestors.iteritems():
354
210
            if node not in dot_nodes:
355
211
                dot_nodes[node] = self.dot_node(node, num)
356
212
                num += 1
357
 
            for parent, skipped in parents.iteritems():
 
213
            if visible_ancestors is self.ancestors:
 
214
                parent_iter = ((f, 0) for f in parents)
 
215
            else:
 
216
                parent_iter = (f for f in parents.iteritems())
 
217
            for parent, skipped in parent_iter:
358
218
                if parent not in dot_nodes:
359
219
                    dot_nodes[parent] = self.dot_node(parent, num)
360
220
                    num += 1
366
226
 
367
227
 
368
228
def write_ancestry_file(branch, filename, collapse=True, antialias=True,
369
 
                        merge_branch=None, ranking="forced", max_distance=None):
 
229
                        merge_branch=None, ranking="forced"):
370
230
    b = Branch.open_containing(branch)[0]
371
231
    if merge_branch is not None:
372
232
        m = Branch.open_containing(merge_branch)[0]
378
238
            m.lock_read()
379
239
        try:
380
240
            grapher = Grapher(b, m)
381
 
            relations = grapher.get_relations(collapse, max_distance)
 
241
            relations = grapher.get_relations(collapse)
382
242
        finally:
383
243
            if m is not None:
384
244
                m.unlock()
390
250
    done = False
391
251
    if ext not in RSVG_OUTPUT_TYPES:
392
252
        antialias = False
393
 
    if antialias:
 
253
    if antialias: 
394
254
        output = list(output)
395
255
        try:
396
256
            invoke_dot_aa(output, filename, ext)
408
268
            done = True
409
269
        except NoDot, e:
410
270
            raise BzrCommandError("Can't find 'dot'.  Please ensure Graphviz"\
411
 
                " is installed correctly.")
 
271
                " is installed correctly, or use --noantialias")
412
272
    elif ext == 'dot' and not done:
413
273
        my_file = file(filename, 'wb')
414
274
        for fragment in output:
418
278
            invoke_dot_html(output, filename)
419
279
        except NoDot, e:
420
280
            raise BzrCommandError("Can't find 'dot'.  Please ensure Graphviz"\
421
 
                " is installed correctly.")
 
281
                " is installed correctly, or use --noantialias")
422
282
    elif not done:
423
283
        print "Unknown file extension: %s" % ext
 
284