~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.graph import node_distances, select_farthest
24
from bzrlib.revision import NULL_REVISION
25
292 by Aaron Bentley
Introduced branch-history command
26
from bzrtools import short_committer
646 by Aaron Bentley
More import fixes
27
from dotgraph import (
28
    dot_output,
29
    DOT_OUTPUT_TYPES,
30
    Edge,
31
    invoke_dot,
32
    invoke_dot_aa,
33
    invoke_dot_html,
34
    Node,
35
    NoDot,
36
    NoRsvg,
37
    RSVG_OUTPUT_TYPES,
38
    )
39
128 by Aaron Bentley
Got initial graphing functionality working
40
183 by Aaron Bentley
Code cleanup
41
mail_map = {'aaron.bentley@utoronto.ca'     : 'Aaron Bentley',
42
            'abentley@panoramicfeedback.com': 'Aaron Bentley',
43
            'abentley@lappy'                : 'Aaron Bentley',
44
            'john@arbash-meinel.com'        : 'John Arbash Meinel',
45
            'mbp@sourcefrog.net'            : 'Martin Pool',
46
            'robertc@robertcollins.net'     : 'Robert Collins',
47
            }
140 by Aaron Bentley
Mapped some email addresses to names
48
166 by Aaron Bentley
Fixed username-in-ancestry-graph issue
49
committer_alias = {'abentley': 'Aaron Bentley'}
145 by aaron.bentley at utoronto
Reduced graph-collapsing aggressivness
50
def can_skip(rev_id, descendants, ancestors):
51
    if rev_id not in descendants:
52
        return False
174 by Aaron Bentley
Added ancestry collapsing to graphs
53
    elif rev_id not in ancestors:
54
        return False
145 by aaron.bentley at utoronto
Reduced graph-collapsing aggressivness
55
    elif len(ancestors[rev_id]) != 1:
56
        return False
174 by Aaron Bentley
Added ancestry collapsing to graphs
57
    elif len(descendants[list(ancestors[rev_id])[0]]) != 1:
145 by aaron.bentley at utoronto
Reduced graph-collapsing aggressivness
58
        return False
59
    elif len(descendants[rev_id]) != 1:
60
        return False
61
    else:
62
        return True
136 by Aaron Bentley
Allowed disabling ancestry collapsing
63
176 by Aaron Bentley
Added skip labels to edges
64
def compact_ancestors(descendants, ancestors, exceptions=()):
174 by Aaron Bentley
Added ancestry collapsing to graphs
65
    new_ancestors={}
66
    skip = set()
67
    for me, my_parents in ancestors.iteritems():
68
        if me in skip:
69
            continue
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
70
        new_ancestors[me] = {}
174 by Aaron Bentley
Added ancestry collapsing to graphs
71
        for parent in my_parents:
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
72
            new_parent = parent
176 by Aaron Bentley
Added skip labels to edges
73
            distance = 0
174 by Aaron Bentley
Added ancestry collapsing to graphs
74
            while can_skip(new_parent, descendants, ancestors):
176 by Aaron Bentley
Added skip labels to edges
75
                if new_parent in exceptions:
76
                    break
174 by Aaron Bentley
Added ancestry collapsing to graphs
77
                skip.add(new_parent)
78
                if new_parent in new_ancestors:
79
                    del new_ancestors[new_parent]
80
                new_parent = list(ancestors[new_parent])[0]
176 by Aaron Bentley
Added skip labels to edges
81
                distance += 1
82
            new_ancestors[me][new_parent] = distance
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
83
    return new_ancestors
174 by Aaron Bentley
Added ancestry collapsing to graphs
84
189.1.1 by John Arbash Meinel
Adding an html target.
85
def get_rev_info(rev_id, source):
86
    """Return the committer, message, and date of a revision."""
87
    committer = None
88
    message = None
89
    date = None
90
    if rev_id == 'null:':
476.1.1 by Aaron Bentley
graph-ancestry shows branch nick
91
        return None, 'Null Revision', None, None
161 by Aaron Bentley
Added committer names to nodes
92
    try:
189.1.1 by John Arbash Meinel
Adding an html target.
93
        rev = source.get_revision(rev_id)
161 by Aaron Bentley
Added committer names to nodes
94
    except NoSuchRevision:
95
        try:
173 by Aaron Bentley
Fixed empty committer for null revision
96
            committer = '-'.join(rev_id.split('-')[:-2]).strip(' ')
97
            if committer == '':
476.1.1 by Aaron Bentley
graph-ancestry shows branch nick
98
                return None, None, None, None
161 by Aaron Bentley
Added committer names to nodes
99
        except ValueError:
476.1.1 by Aaron Bentley
graph-ancestry shows branch nick
100
            return None, None, None, None
189.1.1 by John Arbash Meinel
Adding an html target.
101
    else:
102
        committer = short_committer(rev.committer)
103
        if rev.message is not None:
104
            message = rev.message.split('\n')[0]
194 by Aaron Bentley
tweaked Meinel's changes
105
        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
106
        date = time.strftime('%Y/%m/%d', gmtime)
476.1.1 by Aaron Bentley
graph-ancestry shows branch nick
107
        nick = rev.properties.get('branch-nick')
166 by Aaron Bentley
Fixed username-in-ancestry-graph issue
108
    if '@' in committer:
109
        try:
110
            committer = mail_map[committer]
111
        except KeyError:
112
            pass
113
    try:
114
        committer = committer_alias[committer]
115
    except KeyError:
116
        pass
476.1.1 by Aaron Bentley
graph-ancestry shows branch nick
117
    return committer, message, nick, date
161 by Aaron Bentley
Added committer names to nodes
118
177 by Aaron Bentley
Restructured graph code as an object
119
class Grapher(object):
629 by Aaron Bentley
Use Graph for graph-ancestry
120
180 by Aaron Bentley
Use Grapher for both kinds of graphs
121
    def __init__(self, branch, other_branch=None):
177 by Aaron Bentley
Restructured graph code as an object
122
        object.__init__(self)
123
        self.branch = branch
124
        self.other_branch = other_branch
629 by Aaron Bentley
Use Graph for graph-ancestry
125
        if other_branch is not None:
126
            other_repo = other_branch.repository
127
            revision_b = self.other_branch.last_revision()
128
        else:
129
            other_repo = None
130
            revision_b = None
131
        self.graph = self.branch.repository.get_graph(other_repo)
208 by Aaron Bentley
Fixed last_revision calls
132
        revision_a = self.branch.last_revision()
629 by Aaron Bentley
Use Graph for graph-ancestry
133
        self.scan_graph(revision_a, revision_b)
180 by Aaron Bentley
Use Grapher for both kinds of graphs
134
        self.n_history = branch.revision_history()
595 by Aaron Bentley
Use dotted revnos in graph-ancestry
135
        self.n_revnos = branch.get_revision_id_to_revno_map()
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
136
        self.distances = node_distances(self.descendants, self.ancestors,
177 by Aaron Bentley
Restructured graph code as an object
137
                                        self.root)
180 by Aaron Bentley
Use Grapher for both kinds of graphs
138
        if other_branch is not None:
139
            self.base = select_farthest(self.distances, self.common)
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
140
            self.m_history = other_branch.revision_history()
595 by Aaron Bentley
Use dotted revnos in graph-ancestry
141
            self.m_revnos = other_branch.get_revision_id_to_revno_map()
629 by Aaron Bentley
Use Graph for graph-ancestry
142
            self.new_base = self.graph.find_unique_lca(revision_a,
143
                                                       revision_b)
144
            self.lcas = self.graph.find_lca(revision_a, revision_b)
180 by Aaron Bentley
Use Grapher for both kinds of graphs
145
        else:
146
            self.base = None
544 by Aaron Bentley
Update graph-ancestry to support new graph API
147
            self.new_base = None
148
            self.lcas = set()
180 by Aaron Bentley
Use Grapher for both kinds of graphs
149
            self.m_history = []
595 by Aaron Bentley
Use dotted revnos in graph-ancestry
150
            self.m_revnos = {}
151
629 by Aaron Bentley
Use Graph for graph-ancestry
152
    def scan_graph(self, revision_a, revision_b):
153
        a_ancestors = dict(self.graph.iter_ancestry([revision_a]))
154
        self.ancestors = a_ancestors
155
        self.root = NULL_REVISION
156
        if revision_b is not None:
157
            b_ancestors = dict(self.graph.iter_ancestry([revision_b]))
158
            self.common = set(a_ancestors.keys())
159
            self.common.intersection_update(b_ancestors)
160
            self.ancestors.update(b_ancestors)
161
        else:
162
            self.common = []
163
            revision_b = None
164
        self.descendants = {}
165
        ghosts = set()
166
        for revision, parents in self.ancestors.iteritems():
167
            self.descendants.setdefault(revision, [])
168
            if parents is None:
169
                ghosts.add(revision)
170
                parents = [NULL_REVISION]
171
            for parent in parents:
172
                self.descendants.setdefault(parent, []).append(revision)
173
        for ghost in ghosts:
174
            self.ancestors[ghost] = [NULL_REVISION]
175
595 by Aaron Bentley
Use dotted revnos in graph-ancestry
176
    @staticmethod
177
    def _get_revno_str(prefix, revno_map, revision_id):
178
        try:
179
            revno = revno_map[revision_id]
180
        except KeyError:
181
            return None
182
        return '%s%s' % (prefix, '.'.join(str(n) for n in revno))
161 by Aaron Bentley
Added committer names to nodes
183
177 by Aaron Bentley
Restructured graph code as an object
184
    def dot_node(self, node, num):
156 by Aaron Bentley
Switched to merge base pick graphing temporarily
185
        try:
177 by Aaron Bentley
Restructured graph code as an object
186
            n_rev = self.n_history.index(node) + 1
156 by Aaron Bentley
Switched to merge base pick graphing temporarily
187
        except ValueError:
188
            n_rev = None
189
        try:
177 by Aaron Bentley
Restructured graph code as an object
190
            m_rev = self.m_history.index(node) + 1
156 by Aaron Bentley
Switched to merge base pick graphing temporarily
191
        except ValueError:
192
            m_rev = None
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
193
        if (n_rev, m_rev) == (None, None):
595 by Aaron Bentley
Use dotted revnos in graph-ancestry
194
            name = self._get_revno_str('r', self.n_revnos, node)
195
            if name is None:
196
                name = self._get_revno_str('R', self.m_revnos, node)
197
            if name is None:
198
                name = node[-5:]
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
199
            cluster = None
200
        elif n_rev == m_rev:
201
            name = "rR%d" % n_rev
202
        else:
203
            namelist = []
164 by Aaron Bentley
Fixed varname reuse generating nodes
204
            for prefix, revno in (('r', n_rev), ('R', m_rev)):
205
                if revno is not None:
206
                    namelist.append("%s%d" % (prefix, revno))
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
207
            name = ' '.join(namelist)
208
        if None not in (n_rev, m_rev):
209
            cluster = "common_history"
210
            color = "#ff9900"
211
        elif (None, None) == (n_rev, m_rev):
212
            cluster = None
177 by Aaron Bentley
Restructured graph code as an object
213
            if node in self.common:
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
214
                color = "#6699ff"
215
            else:
190 by Aaron Bentley
Set normal revisions to white
216
                color = "white"
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
217
        elif n_rev is not None:
218
            cluster = "my_history"
219
            color = "#ffff00"
220
        else:
221
            assert m_rev is not None
222
            cluster = "other_history"
223
            color = "#ff0000"
544 by Aaron Bentley
Update graph-ancestry to support new graph API
224
        if node in self.lcas:
225
            color = "#9933cc"
177 by Aaron Bentley
Restructured graph code as an object
226
        if node == self.base:
544 by Aaron Bentley
Update graph-ancestry to support new graph API
227
            color = "#669933"
228
            if node == self.new_base:
229
                color = "#33ff33"
230
        if node == self.new_base:
231
            color = '#33cc99'
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
232
233
        label = [name]
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
234
        committer, message, nick, date = get_rev_info(node,
476.1.1 by Aaron Bentley
graph-ancestry shows branch nick
235
                                                      self.branch.repository)
161 by Aaron Bentley
Added committer names to nodes
236
        if committer is not None:
237
            label.append(committer)
238
476.1.1 by Aaron Bentley
graph-ancestry shows branch nick
239
        if nick is not None:
240
            label.append(nick)
241
189.1.1 by John Arbash Meinel
Adding an html target.
242
        if date is not None:
243
            label.append(date)
188 by Aaron Bentley
Added dates to graph
244
177 by Aaron Bentley
Restructured graph code as an object
245
        if node in self.distances:
178 by Aaron Bentley
Switched from clusters to forced ranking
246
            rank = self.distances[node]
177 by Aaron Bentley
Restructured graph code as an object
247
            label.append('d%d' % self.distances[node])
178 by Aaron Bentley
Switched from clusters to forced ranking
248
        else:
249
            rank = None
172 by Aaron Bentley
Marked missing nodes
250
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
251
        d_node = Node("n%d" % num, color=color, label="\\n".join(label),
189.1.1 by John Arbash Meinel
Adding an html target.
252
                    rev_id=node, cluster=cluster, message=message,
253
                    date=date)
178 by Aaron Bentley
Switched from clusters to forced ranking
254
        d_node.rank = rank
255
177 by Aaron Bentley
Restructured graph code as an object
256
        if node not in self.ancestors:
172 by Aaron Bentley
Marked missing nodes
257
            d_node.node_style.append('dotted')
258
259
        return d_node
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
260
476.1.2 by Aaron Bentley
graph-ancestry can restrict the number of nodes shown by distance
261
    def get_relations(self, collapse=False, max_distance=None):
177 by Aaron Bentley
Restructured graph code as an object
262
        dot_nodes = {}
263
        node_relations = []
264
        num = 0
265
        if collapse:
544 by Aaron Bentley
Update graph-ancestry to support new graph API
266
            exceptions = self.lcas.union([self.base, self.new_base])
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
267
            visible_ancestors = compact_ancestors(self.descendants,
544 by Aaron Bentley
Update graph-ancestry to support new graph API
268
                                                  self.ancestors,
269
                                                  exceptions)
176 by Aaron Bentley
Added skip labels to edges
270
        else:
547 by Aaron Bentley
Fix no-collapse behavior
271
            visible_ancestors = {}
272
            for revision, parents in self.ancestors.iteritems():
273
                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
274
        if max_distance is not None:
275
            min_distance = max(self.distances.values()) - max_distance
547 by Aaron Bentley
Fix no-collapse behavior
276
            visible_ancestors = dict((n, p) for n, p in
277
                                     visible_ancestors.iteritems() if
278
                                     self.distances[n] >= min_distance)
177 by Aaron Bentley
Restructured graph code as an object
279
        for node, parents in visible_ancestors.iteritems():
280
            if node not in dot_nodes:
281
                dot_nodes[node] = self.dot_node(node, num)
176 by Aaron Bentley
Added skip labels to edges
282
                num += 1
547 by Aaron Bentley
Fix no-collapse behavior
283
            for parent, skipped in parents.iteritems():
177 by Aaron Bentley
Restructured graph code as an object
284
                if parent not in dot_nodes:
285
                    dot_nodes[parent] = self.dot_node(parent, num)
286
                    num += 1
287
                edge = Edge(dot_nodes[parent], dot_nodes[node])
288
                if skipped != 0:
289
                    edge.label = "%d" % skipped
290
                node_relations.append(edge)
291
        return node_relations
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
292
293
160 by Aaron Bentley
Restored old graph-ancestry functionality
294
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
295
                        merge_branch=None, ranking="forced", max_distance=None):
234 by Aaron Bentley
Adopted Branch.open_containing's new format
296
    b = Branch.open_containing(branch)[0]
180 by Aaron Bentley
Use Grapher for both kinds of graphs
297
    if merge_branch is not None:
234 by Aaron Bentley
Adopted Branch.open_containing's new format
298
        m = Branch.open_containing(merge_branch)[0]
180 by Aaron Bentley
Use Grapher for both kinds of graphs
299
    else:
300
        m = None
309 by Aaron Bentley
Fixed graph-ancestry
301
    b.lock_write()
258 by Aaron Bentley
Added locking to graph-ancestry
302
    try:
303
        if m is not None:
304
            m.lock_read()
305
        try:
306
            grapher = Grapher(b, m)
476.1.2 by Aaron Bentley
graph-ancestry can restrict the number of nodes shown by distance
307
            relations = grapher.get_relations(collapse, max_distance)
258 by Aaron Bentley
Added locking to graph-ancestry
308
        finally:
309
            if m is not None:
310
                m.unlock()
311
    finally:
312
        b.unlock()
160 by Aaron Bentley
Restored old graph-ancestry functionality
313
134 by Aaron Bentley
support multiple image formats for graph-ancestry
314
    ext = filename.split('.')[-1]
178 by Aaron Bentley
Switched from clusters to forced ranking
315
    output = dot_output(relations, ranking)
186 by Aaron Bentley
Warn instead of failing when librsvg is not installed
316
    done = False
187 by Aaron Bentley
Fixed output handling for non-antialiased types
317
    if ext not in RSVG_OUTPUT_TYPES:
318
        antialias = False
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
319
    if antialias:
186 by Aaron Bentley
Warn instead of failing when librsvg is not installed
320
        output = list(output)
143 by Aaron Bentley
Used rsvga for nice antialiasing
321
        try:
178 by Aaron Bentley
Switched from clusters to forced ranking
322
            invoke_dot_aa(output, filename, ext)
186 by Aaron Bentley
Warn instead of failing when librsvg is not installed
323
            done = True
143 by Aaron Bentley
Used rsvga for nice antialiasing
324
        except NoDot, e:
325
            raise BzrCommandError("Can't find 'dot'.  Please ensure Graphviz"\
326
                " is installed correctly.")
327
        except NoRsvg, e:
186 by Aaron Bentley
Warn instead of failing when librsvg is not installed
328
            print "Not antialiasing because rsvg (from librsvg-bin) is not"\
329
                " installed."
330
            antialias = False
331
    if ext in DOT_OUTPUT_TYPES and not antialias and not done:
139 by Aaron Bentley
Tweaked missing-dot handling
332
        try:
178 by Aaron Bentley
Switched from clusters to forced ranking
333
            invoke_dot(output, filename, ext)
186 by Aaron Bentley
Warn instead of failing when librsvg is not installed
334
            done = True
139 by Aaron Bentley
Tweaked missing-dot handling
335
        except NoDot, e:
336
            raise BzrCommandError("Can't find 'dot'.  Please ensure Graphviz"\
590 by Aaron Bentley
Fix error when dot not found
337
                " is installed correctly.")
194 by Aaron Bentley
tweaked Meinel's changes
338
    elif ext == 'dot' and not done:
178 by Aaron Bentley
Switched from clusters to forced ranking
339
        my_file = file(filename, 'wb')
340
        for fragment in output:
321.1.1 by Aaron Bentley
Forced dot output to UTF-8
341
            my_file.write(fragment.encode('utf-8'))
194 by Aaron Bentley
tweaked Meinel's changes
342
    elif ext == 'html':
189.1.1 by John Arbash Meinel
Adding an html target.
343
        try:
344
            invoke_dot_html(output, filename)
345
        except NoDot, e:
346
            raise BzrCommandError("Can't find 'dot'.  Please ensure Graphviz"\
590 by Aaron Bentley
Fix error when dot not found
347
                " is installed correctly.")
186 by Aaron Bentley
Warn instead of failing when librsvg is not installed
348
    elif not done:
134 by Aaron Bentley
support multiple image formats for graph-ancestry
349
        print "Unknown file extension: %s" % ext