~abentley/bzrtools/bzrtools.dev

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