~abentley/bzrtools/bzrtools.dev

646 by Aaron Bentley
More import fixes
1
# Copyright (C) 2005, 2008 Aaron Bentley
612 by Aaron Bentley
Update email address
2
# <aaron@aaronbentley.com>
257.1.2 by Aaron Bentley
Updated GPL notices
3
#
4
#    This program is free software; you can redistribute it and/or modify
5
#    it under the terms of the GNU General Public License as published by
6
#    the Free Software Foundation; either version 2 of the License, or
7
#    (at your option) any later version.
8
#
9
#    This program is distributed in the hope that it will be useful,
10
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
11
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
#    GNU General Public License for more details.
13
#
14
#    You should have received a copy of the GNU General Public License
15
#    along with this program; if not, write to the Free Software
16
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
646 by Aaron Bentley
More import fixes
17
18
19
import time
20
647 by Aaron Bentley
More import tweaks
21
from bzrlib.branch import Branch
22
from bzrlib.errors import BzrCommandError, NoSuchRevision
23
from bzrlib.revision import NULL_REVISION
24
292 by Aaron Bentley
Introduced branch-history command
25
from bzrtools import short_committer
646 by Aaron Bentley
More import fixes
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
128 by Aaron Bentley
Got initial graphing functionality working
39
756.1.1 by Jelmer Vernooij
Merge in bzrlib.deprecated_graph functions still used by bzrtools.
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
183 by Aaron Bentley
Code cleanup
111
mail_map = {'aaron.bentley@utoronto.ca'     : 'Aaron Bentley',
112
            'abentley@panoramicfeedback.com': 'Aaron Bentley',
113
            'abentley@lappy'                : 'Aaron Bentley',
114
            'john@arbash-meinel.com'        : 'John Arbash Meinel',
115
            'mbp@sourcefrog.net'            : 'Martin Pool',
116
            'robertc@robertcollins.net'     : 'Robert Collins',
117
            }
140 by Aaron Bentley
Mapped some email addresses to names
118
166 by Aaron Bentley
Fixed username-in-ancestry-graph issue
119
committer_alias = {'abentley': 'Aaron Bentley'}
145 by aaron.bentley at utoronto
Reduced graph-collapsing aggressivness
120
def can_skip(rev_id, descendants, ancestors):
121
    if rev_id not in descendants:
122
        return False
174 by Aaron Bentley
Added ancestry collapsing to graphs
123
    elif rev_id not in ancestors:
124
        return False
145 by aaron.bentley at utoronto
Reduced graph-collapsing aggressivness
125
    elif len(ancestors[rev_id]) != 1:
126
        return False
174 by Aaron Bentley
Added ancestry collapsing to graphs
127
    elif len(descendants[list(ancestors[rev_id])[0]]) != 1:
145 by aaron.bentley at utoronto
Reduced graph-collapsing aggressivness
128
        return False
129
    elif len(descendants[rev_id]) != 1:
130
        return False
131
    else:
132
        return True
136 by Aaron Bentley
Allowed disabling ancestry collapsing
133
176 by Aaron Bentley
Added skip labels to edges
134
def compact_ancestors(descendants, ancestors, exceptions=()):
174 by Aaron Bentley
Added ancestry collapsing to graphs
135
    new_ancestors={}
136
    skip = set()
137
    for me, my_parents in ancestors.iteritems():
138
        if me in skip:
139
            continue
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
140
        new_ancestors[me] = {}
174 by Aaron Bentley
Added ancestry collapsing to graphs
141
        for parent in my_parents:
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
142
            new_parent = parent
176 by Aaron Bentley
Added skip labels to edges
143
            distance = 0
174 by Aaron Bentley
Added ancestry collapsing to graphs
144
            while can_skip(new_parent, descendants, ancestors):
176 by Aaron Bentley
Added skip labels to edges
145
                if new_parent in exceptions:
146
                    break
174 by Aaron Bentley
Added ancestry collapsing to graphs
147
                skip.add(new_parent)
148
                if new_parent in new_ancestors:
149
                    del new_ancestors[new_parent]
150
                new_parent = list(ancestors[new_parent])[0]
176 by Aaron Bentley
Added skip labels to edges
151
                distance += 1
152
            new_ancestors[me][new_parent] = distance
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
153
    return new_ancestors
174 by Aaron Bentley
Added ancestry collapsing to graphs
154
189.1.1 by John Arbash Meinel
Adding an html target.
155
def get_rev_info(rev_id, source):
156
    """Return the committer, message, and date of a revision."""
157
    committer = None
158
    message = None
159
    date = None
160
    if rev_id == 'null:':
476.1.1 by Aaron Bentley
graph-ancestry shows branch nick
161
        return None, 'Null Revision', None, None
161 by Aaron Bentley
Added committer names to nodes
162
    try:
189.1.1 by John Arbash Meinel
Adding an html target.
163
        rev = source.get_revision(rev_id)
161 by Aaron Bentley
Added committer names to nodes
164
    except NoSuchRevision:
165
        try:
173 by Aaron Bentley
Fixed empty committer for null revision
166
            committer = '-'.join(rev_id.split('-')[:-2]).strip(' ')
167
            if committer == '':
476.1.1 by Aaron Bentley
graph-ancestry shows branch nick
168
                return None, None, None, None
161 by Aaron Bentley
Added committer names to nodes
169
        except ValueError:
476.1.1 by Aaron Bentley
graph-ancestry shows branch nick
170
            return None, None, None, None
189.1.1 by John Arbash Meinel
Adding an html target.
171
    else:
172
        committer = short_committer(rev.committer)
173
        if rev.message is not None:
174
            message = rev.message.split('\n')[0]
194 by Aaron Bentley
tweaked Meinel's changes
175
        gmtime = time.gmtime(rev.timestamp + (rev.timezone or 0))
197 by Aaron Bentley
Restored slashes in date, because my rsvg-bin is borken wrt hyphens
176
        date = time.strftime('%Y/%m/%d', gmtime)
476.1.1 by Aaron Bentley
graph-ancestry shows branch nick
177
        nick = rev.properties.get('branch-nick')
166 by Aaron Bentley
Fixed username-in-ancestry-graph issue
178
    if '@' in committer:
179
        try:
180
            committer = mail_map[committer]
181
        except KeyError:
182
            pass
183
    try:
184
        committer = committer_alias[committer]
185
    except KeyError:
186
        pass
476.1.1 by Aaron Bentley
graph-ancestry shows branch nick
187
    return committer, message, nick, date
161 by Aaron Bentley
Added committer names to nodes
188
177 by Aaron Bentley
Restructured graph code as an object
189
class Grapher(object):
629 by Aaron Bentley
Use Graph for graph-ancestry
190
180 by Aaron Bentley
Use Grapher for both kinds of graphs
191
    def __init__(self, branch, other_branch=None):
177 by Aaron Bentley
Restructured graph code as an object
192
        object.__init__(self)
193
        self.branch = branch
194
        self.other_branch = other_branch
629 by Aaron Bentley
Use Graph for graph-ancestry
195
        if other_branch is not None:
196
            other_repo = other_branch.repository
197
            revision_b = self.other_branch.last_revision()
198
        else:
199
            other_repo = None
200
            revision_b = None
201
        self.graph = self.branch.repository.get_graph(other_repo)
208 by Aaron Bentley
Fixed last_revision calls
202
        revision_a = self.branch.last_revision()
629 by Aaron Bentley
Use Graph for graph-ancestry
203
        self.scan_graph(revision_a, revision_b)
180 by Aaron Bentley
Use Grapher for both kinds of graphs
204
        self.n_history = branch.revision_history()
595 by Aaron Bentley
Use dotted revnos in graph-ancestry
205
        self.n_revnos = branch.get_revision_id_to_revno_map()
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
206
        self.distances = node_distances(self.descendants, self.ancestors,
177 by Aaron Bentley
Restructured graph code as an object
207
                                        self.root)
180 by Aaron Bentley
Use Grapher for both kinds of graphs
208
        if other_branch is not None:
209
            self.base = select_farthest(self.distances, self.common)
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
210
            self.m_history = other_branch.revision_history()
595 by Aaron Bentley
Use dotted revnos in graph-ancestry
211
            self.m_revnos = other_branch.get_revision_id_to_revno_map()
629 by Aaron Bentley
Use Graph for graph-ancestry
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)
180 by Aaron Bentley
Use Grapher for both kinds of graphs
215
        else:
216
            self.base = None
544 by Aaron Bentley
Update graph-ancestry to support new graph API
217
            self.new_base = None
218
            self.lcas = set()
180 by Aaron Bentley
Use Grapher for both kinds of graphs
219
            self.m_history = []
595 by Aaron Bentley
Use dotted revnos in graph-ancestry
220
            self.m_revnos = {}
221
629 by Aaron Bentley
Use Graph for graph-ancestry
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
595 by Aaron Bentley
Use dotted revnos in graph-ancestry
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))
161 by Aaron Bentley
Added committer names to nodes
253
177 by Aaron Bentley
Restructured graph code as an object
254
    def dot_node(self, node, num):
156 by Aaron Bentley
Switched to merge base pick graphing temporarily
255
        try:
177 by Aaron Bentley
Restructured graph code as an object
256
            n_rev = self.n_history.index(node) + 1
156 by Aaron Bentley
Switched to merge base pick graphing temporarily
257
        except ValueError:
258
            n_rev = None
259
        try:
177 by Aaron Bentley
Restructured graph code as an object
260
            m_rev = self.m_history.index(node) + 1
156 by Aaron Bentley
Switched to merge base pick graphing temporarily
261
        except ValueError:
262
            m_rev = None
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
263
        if (n_rev, m_rev) == (None, None):
595 by Aaron Bentley
Use dotted revnos in graph-ancestry
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:]
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
269
            cluster = None
270
        elif n_rev == m_rev:
271
            name = "rR%d" % n_rev
272
        else:
273
            namelist = []
164 by Aaron Bentley
Fixed varname reuse generating nodes
274
            for prefix, revno in (('r', n_rev), ('R', m_rev)):
275
                if revno is not None:
276
                    namelist.append("%s%d" % (prefix, revno))
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
277
            name = ' '.join(namelist)
278
        if None not in (n_rev, m_rev):
279
            cluster = "common_history"
280
            color = "#ff9900"
281
        elif (None, None) == (n_rev, m_rev):
282
            cluster = None
177 by Aaron Bentley
Restructured graph code as an object
283
            if node in self.common:
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
284
                color = "#6699ff"
285
            else:
190 by Aaron Bentley
Set normal revisions to white
286
                color = "white"
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
287
        elif n_rev is not None:
288
            cluster = "my_history"
289
            color = "#ffff00"
290
        else:
291
            assert m_rev is not None
292
            cluster = "other_history"
293
            color = "#ff0000"
544 by Aaron Bentley
Update graph-ancestry to support new graph API
294
        if node in self.lcas:
295
            color = "#9933cc"
177 by Aaron Bentley
Restructured graph code as an object
296
        if node == self.base:
544 by Aaron Bentley
Update graph-ancestry to support new graph API
297
            color = "#669933"
298
            if node == self.new_base:
299
                color = "#33ff33"
300
        if node == self.new_base:
301
            color = '#33cc99'
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
302
303
        label = [name]
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
304
        committer, message, nick, date = get_rev_info(node,
476.1.1 by Aaron Bentley
graph-ancestry shows branch nick
305
                                                      self.branch.repository)
161 by Aaron Bentley
Added committer names to nodes
306
        if committer is not None:
307
            label.append(committer)
308
476.1.1 by Aaron Bentley
graph-ancestry shows branch nick
309
        if nick is not None:
310
            label.append(nick)
311
189.1.1 by John Arbash Meinel
Adding an html target.
312
        if date is not None:
313
            label.append(date)
188 by Aaron Bentley
Added dates to graph
314
177 by Aaron Bentley
Restructured graph code as an object
315
        if node in self.distances:
178 by Aaron Bentley
Switched from clusters to forced ranking
316
            rank = self.distances[node]
177 by Aaron Bentley
Restructured graph code as an object
317
            label.append('d%d' % self.distances[node])
178 by Aaron Bentley
Switched from clusters to forced ranking
318
        else:
319
            rank = None
172 by Aaron Bentley
Marked missing nodes
320
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
321
        d_node = Node("n%d" % num, color=color, label="\\n".join(label),
189.1.1 by John Arbash Meinel
Adding an html target.
322
                    rev_id=node, cluster=cluster, message=message,
323
                    date=date)
178 by Aaron Bentley
Switched from clusters to forced ranking
324
        d_node.rank = rank
325
177 by Aaron Bentley
Restructured graph code as an object
326
        if node not in self.ancestors:
172 by Aaron Bentley
Marked missing nodes
327
            d_node.node_style.append('dotted')
328
329
        return d_node
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
330
476.1.2 by Aaron Bentley
graph-ancestry can restrict the number of nodes shown by distance
331
    def get_relations(self, collapse=False, max_distance=None):
177 by Aaron Bentley
Restructured graph code as an object
332
        dot_nodes = {}
333
        node_relations = []
334
        num = 0
335
        if collapse:
544 by Aaron Bentley
Update graph-ancestry to support new graph API
336
            exceptions = self.lcas.union([self.base, self.new_base])
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
337
            visible_ancestors = compact_ancestors(self.descendants,
544 by Aaron Bentley
Update graph-ancestry to support new graph API
338
                                                  self.ancestors,
339
                                                  exceptions)
176 by Aaron Bentley
Added skip labels to edges
340
        else:
547 by Aaron Bentley
Fix no-collapse behavior
341
            visible_ancestors = {}
342
            for revision, parents in self.ancestors.iteritems():
343
                visible_ancestors[revision] = dict((p, 0) for p in parents)
476.1.2 by Aaron Bentley
graph-ancestry can restrict the number of nodes shown by distance
344
        if max_distance is not None:
345
            min_distance = max(self.distances.values()) - max_distance
547 by Aaron Bentley
Fix no-collapse behavior
346
            visible_ancestors = dict((n, p) for n, p in
347
                                     visible_ancestors.iteritems() if
348
                                     self.distances[n] >= min_distance)
177 by Aaron Bentley
Restructured graph code as an object
349
        for node, parents in visible_ancestors.iteritems():
350
            if node not in dot_nodes:
351
                dot_nodes[node] = self.dot_node(node, num)
176 by Aaron Bentley
Added skip labels to edges
352
                num += 1
547 by Aaron Bentley
Fix no-collapse behavior
353
            for parent, skipped in parents.iteritems():
177 by Aaron Bentley
Restructured graph code as an object
354
                if parent not in dot_nodes:
355
                    dot_nodes[parent] = self.dot_node(parent, num)
356
                    num += 1
357
                edge = Edge(dot_nodes[parent], dot_nodes[node])
358
                if skipped != 0:
359
                    edge.label = "%d" % skipped
360
                node_relations.append(edge)
361
        return node_relations
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
362
363
160 by Aaron Bentley
Restored old graph-ancestry functionality
364
def write_ancestry_file(branch, filename, collapse=True, antialias=True,
476.1.2 by Aaron Bentley
graph-ancestry can restrict the number of nodes shown by distance
365
                        merge_branch=None, ranking="forced", max_distance=None):
234 by Aaron Bentley
Adopted Branch.open_containing's new format
366
    b = Branch.open_containing(branch)[0]
180 by Aaron Bentley
Use Grapher for both kinds of graphs
367
    if merge_branch is not None:
234 by Aaron Bentley
Adopted Branch.open_containing's new format
368
        m = Branch.open_containing(merge_branch)[0]
180 by Aaron Bentley
Use Grapher for both kinds of graphs
369
    else:
370
        m = None
309 by Aaron Bentley
Fixed graph-ancestry
371
    b.lock_write()
258 by Aaron Bentley
Added locking to graph-ancestry
372
    try:
373
        if m is not None:
374
            m.lock_read()
375
        try:
376
            grapher = Grapher(b, m)
476.1.2 by Aaron Bentley
graph-ancestry can restrict the number of nodes shown by distance
377
            relations = grapher.get_relations(collapse, max_distance)
258 by Aaron Bentley
Added locking to graph-ancestry
378
        finally:
379
            if m is not None:
380
                m.unlock()
381
    finally:
382
        b.unlock()
160 by Aaron Bentley
Restored old graph-ancestry functionality
383
134 by Aaron Bentley
support multiple image formats for graph-ancestry
384
    ext = filename.split('.')[-1]
178 by Aaron Bentley
Switched from clusters to forced ranking
385
    output = dot_output(relations, ranking)
186 by Aaron Bentley
Warn instead of failing when librsvg is not installed
386
    done = False
187 by Aaron Bentley
Fixed output handling for non-antialiased types
387
    if ext not in RSVG_OUTPUT_TYPES:
388
        antialias = False
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
389
    if antialias:
186 by Aaron Bentley
Warn instead of failing when librsvg is not installed
390
        output = list(output)
143 by Aaron Bentley
Used rsvga for nice antialiasing
391
        try:
178 by Aaron Bentley
Switched from clusters to forced ranking
392
            invoke_dot_aa(output, filename, ext)
186 by Aaron Bentley
Warn instead of failing when librsvg is not installed
393
            done = True
143 by Aaron Bentley
Used rsvga for nice antialiasing
394
        except NoDot, e:
395
            raise BzrCommandError("Can't find 'dot'.  Please ensure Graphviz"\
396
                " is installed correctly.")
397
        except NoRsvg, e:
186 by Aaron Bentley
Warn instead of failing when librsvg is not installed
398
            print "Not antialiasing because rsvg (from librsvg-bin) is not"\
399
                " installed."
400
            antialias = False
401
    if ext in DOT_OUTPUT_TYPES and not antialias and not done:
139 by Aaron Bentley
Tweaked missing-dot handling
402
        try:
178 by Aaron Bentley
Switched from clusters to forced ranking
403
            invoke_dot(output, filename, ext)
186 by Aaron Bentley
Warn instead of failing when librsvg is not installed
404
            done = True
139 by Aaron Bentley
Tweaked missing-dot handling
405
        except NoDot, e:
406
            raise BzrCommandError("Can't find 'dot'.  Please ensure Graphviz"\
590 by Aaron Bentley
Fix error when dot not found
407
                " is installed correctly.")
194 by Aaron Bentley
tweaked Meinel's changes
408
    elif ext == 'dot' and not done:
178 by Aaron Bentley
Switched from clusters to forced ranking
409
        my_file = file(filename, 'wb')
410
        for fragment in output:
321.1.1 by Aaron Bentley
Forced dot output to UTF-8
411
            my_file.write(fragment.encode('utf-8'))
194 by Aaron Bentley
tweaked Meinel's changes
412
    elif ext == 'html':
189.1.1 by John Arbash Meinel
Adding an html target.
413
        try:
414
            invoke_dot_html(output, filename)
415
        except NoDot, e:
416
            raise BzrCommandError("Can't find 'dot'.  Please ensure Graphviz"\
590 by Aaron Bentley
Fix error when dot not found
417
                " is installed correctly.")
186 by Aaron Bentley
Warn instead of failing when librsvg is not installed
418
    elif not done:
134 by Aaron Bentley
support multiple image formats for graph-ancestry
419
        print "Unknown file extension: %s" % ext