~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
194 by Aaron Bentley
tweaked Meinel's changes
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
128 by Aaron Bentley
Got initial graphing functionality working
19
from bzrlib.branch import Branch
161 by Aaron Bentley
Added committer names to nodes
20
from bzrlib.errors import BzrCommandError, NoCommonRoot, NoSuchRevision
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
21
from bzrlib.fetch import greedy_fetch
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'}
130 by Aaron Bentley
Added committer to revisions
39
def short_committer(committer):
135 by Aaron Bentley
Enhanced revision-crediting
40
    new_committer = re.sub('<.*>', '', committer).strip(' ')
130 by Aaron Bentley
Added committer to revisions
41
    if len(new_committer) < 2:
42
        return committer
43
    return new_committer
136 by Aaron Bentley
Allowed disabling ancestry collapsing
44
145 by aaron.bentley at utoronto
Reduced graph-collapsing aggressivness
45
def can_skip(rev_id, descendants, ancestors):
46
    if rev_id not in descendants:
47
        return False
174 by Aaron Bentley
Added ancestry collapsing to graphs
48
    elif rev_id not in ancestors:
49
        return False
145 by aaron.bentley at utoronto
Reduced graph-collapsing aggressivness
50
    elif len(ancestors[rev_id]) != 1:
51
        return False
174 by Aaron Bentley
Added ancestry collapsing to graphs
52
    elif len(descendants[list(ancestors[rev_id])[0]]) != 1:
145 by aaron.bentley at utoronto
Reduced graph-collapsing aggressivness
53
        return False
54
    elif len(descendants[rev_id]) != 1:
55
        return False
56
    else:
57
        return True
136 by Aaron Bentley
Allowed disabling ancestry collapsing
58
176 by Aaron Bentley
Added skip labels to edges
59
def compact_ancestors(descendants, ancestors, exceptions=()):
174 by Aaron Bentley
Added ancestry collapsing to graphs
60
    new_ancestors={}
61
    skip = set()
62
    for me, my_parents in ancestors.iteritems():
63
        if me in skip:
64
            continue
176 by Aaron Bentley
Added skip labels to edges
65
        new_ancestors[me] = {} 
174 by Aaron Bentley
Added ancestry collapsing to graphs
66
        for parent in my_parents:
67
            new_parent = parent 
176 by Aaron Bentley
Added skip labels to edges
68
            distance = 0
174 by Aaron Bentley
Added ancestry collapsing to graphs
69
            while can_skip(new_parent, descendants, ancestors):
176 by Aaron Bentley
Added skip labels to edges
70
                if new_parent in exceptions:
71
                    break
174 by Aaron Bentley
Added ancestry collapsing to graphs
72
                skip.add(new_parent)
73
                if new_parent in new_ancestors:
74
                    del new_ancestors[new_parent]
75
                new_parent = list(ancestors[new_parent])[0]
176 by Aaron Bentley
Added skip labels to edges
76
                distance += 1
77
            new_ancestors[me][new_parent] = distance
174 by Aaron Bentley
Added ancestry collapsing to graphs
78
    return new_ancestors    
79
189.1.1 by John Arbash Meinel
Adding an html target.
80
def get_rev_info(rev_id, source):
81
    """Return the committer, message, and date of a revision."""
82
    committer = None
83
    message = None
84
    date = None
85
    if rev_id == 'null:':
86
        return None, 'Null Revision', None
161 by Aaron Bentley
Added committer names to nodes
87
    try:
189.1.1 by John Arbash Meinel
Adding an html target.
88
        rev = source.get_revision(rev_id)
161 by Aaron Bentley
Added committer names to nodes
89
    except NoSuchRevision:
90
        try:
173 by Aaron Bentley
Fixed empty committer for null revision
91
            committer = '-'.join(rev_id.split('-')[:-2]).strip(' ')
92
            if committer == '':
189.1.1 by John Arbash Meinel
Adding an html target.
93
                return None, None, None
161 by Aaron Bentley
Added committer names to nodes
94
        except ValueError:
189.1.1 by John Arbash Meinel
Adding an html target.
95
            return None, None, None
96
    else:
97
        committer = short_committer(rev.committer)
98
        if rev.message is not None:
99
            message = rev.message.split('\n')[0]
194 by Aaron Bentley
tweaked Meinel's changes
100
        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
101
        date = time.strftime('%Y/%m/%d', gmtime)
166 by Aaron Bentley
Fixed username-in-ancestry-graph issue
102
    if '@' in committer:
103
        try:
104
            committer = mail_map[committer]
105
        except KeyError:
106
            pass
107
    try:
108
        committer = committer_alias[committer]
109
    except KeyError:
110
        pass
189.1.1 by John Arbash Meinel
Adding an html target.
111
    return committer, message, date
161 by Aaron Bentley
Added committer names to nodes
112
177 by Aaron Bentley
Restructured graph code as an object
113
class Grapher(object):
180 by Aaron Bentley
Use Grapher for both kinds of graphs
114
    def __init__(self, branch, other_branch=None):
177 by Aaron Bentley
Restructured graph code as an object
115
        object.__init__(self)
116
        self.branch = branch
117
        self.other_branch = other_branch
208 by Aaron Bentley
Fixed last_revision calls
118
        revision_a = self.branch.last_revision()
180 by Aaron Bentley
Use Grapher for both kinds of graphs
119
        if other_branch is not None:
120
            greedy_fetch(branch, other_branch)
208 by Aaron Bentley
Fixed last_revision calls
121
            revision_b = self.other_branch.last_revision()
180 by Aaron Bentley
Use Grapher for both kinds of graphs
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
132
        self.n_history = branch.revision_history()
177 by Aaron Bentley
Restructured graph code as an object
133
        self.distances = node_distances(self.descendants, self.ancestors, 
134
                                        self.root)
180 by Aaron Bentley
Use Grapher for both kinds of graphs
135
        if other_branch is not None:
136
            self.base = select_farthest(self.distances, self.common)
137
            self.m_history = other_branch.revision_history() 
138
        else:
139
            self.base = None
140
            self.m_history = []
161 by Aaron Bentley
Added committer names to nodes
141
177 by Aaron Bentley
Restructured graph code as an object
142
    def dot_node(self, node, num):
156 by Aaron Bentley
Switched to merge base pick graphing temporarily
143
        try:
177 by Aaron Bentley
Restructured graph code as an object
144
            n_rev = self.n_history.index(node) + 1
156 by Aaron Bentley
Switched to merge base pick graphing temporarily
145
        except ValueError:
146
            n_rev = None
147
        try:
177 by Aaron Bentley
Restructured graph code as an object
148
            m_rev = self.m_history.index(node) + 1
156 by Aaron Bentley
Switched to merge base pick graphing temporarily
149
        except ValueError:
150
            m_rev = None
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
151
        if (n_rev, m_rev) == (None, None):
168 by Aaron Bentley
lengthened graph node name to include null:
152
            name = node[-5:]
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
153
            cluster = None
154
        elif n_rev == m_rev:
155
            name = "rR%d" % n_rev
156
        else:
157
            namelist = []
164 by Aaron Bentley
Fixed varname reuse generating nodes
158
            for prefix, revno in (('r', n_rev), ('R', m_rev)):
159
                if revno is not None:
160
                    namelist.append("%s%d" % (prefix, revno))
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
161
            name = ' '.join(namelist)
162
        if None not in (n_rev, m_rev):
163
            cluster = "common_history"
164
            color = "#ff9900"
165
        elif (None, None) == (n_rev, m_rev):
166
            cluster = None
177 by Aaron Bentley
Restructured graph code as an object
167
            if node in self.common:
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
168
                color = "#6699ff"
169
            else:
190 by Aaron Bentley
Set normal revisions to white
170
                color = "white"
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
171
        elif n_rev is not None:
172
            cluster = "my_history"
173
            color = "#ffff00"
174
        else:
175
            assert m_rev is not None
176
            cluster = "other_history"
177
            color = "#ff0000"
177 by Aaron Bentley
Restructured graph code as an object
178
        if node == self.base:
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
179
            color = "#33ff99"
180
181
        label = [name]
189.1.1 by John Arbash Meinel
Adding an html target.
182
        committer, message, date = get_rev_info(node, self.branch)
161 by Aaron Bentley
Added committer names to nodes
183
        if committer is not None:
184
            label.append(committer)
185
189.1.1 by John Arbash Meinel
Adding an html target.
186
        if date is not None:
187
            label.append(date)
188 by Aaron Bentley
Added dates to graph
188
177 by Aaron Bentley
Restructured graph code as an object
189
        if node in self.distances:
178 by Aaron Bentley
Switched from clusters to forced ranking
190
            rank = self.distances[node]
177 by Aaron Bentley
Restructured graph code as an object
191
            label.append('d%d' % self.distances[node])
178 by Aaron Bentley
Switched from clusters to forced ranking
192
        else:
193
            rank = None
172 by Aaron Bentley
Marked missing nodes
194
195
        d_node = Node("n%d" % num, color=color, label="\\n".join(label), 
189.1.1 by John Arbash Meinel
Adding an html target.
196
                    rev_id=node, cluster=cluster, message=message,
197
                    date=date)
178 by Aaron Bentley
Switched from clusters to forced ranking
198
        d_node.rank = rank
199
177 by Aaron Bentley
Restructured graph code as an object
200
        if node not in self.ancestors:
172 by Aaron Bentley
Marked missing nodes
201
            d_node.node_style.append('dotted')
202
203
        return d_node
177 by Aaron Bentley
Restructured graph code as an object
204
        
205
    def get_relations(self, collapse=False):
206
        dot_nodes = {}
207
        node_relations = []
208
        num = 0
209
        if collapse:
210
            visible_ancestors = compact_ancestors(self.descendants, 
211
                                                  self.ancestors, (self.base,))
176 by Aaron Bentley
Added skip labels to edges
212
        else:
177 by Aaron Bentley
Restructured graph code as an object
213
            visible_ancestors = self.ancestors
214
        for node, parents in visible_ancestors.iteritems():
215
            if node not in dot_nodes:
216
                dot_nodes[node] = self.dot_node(node, num)
176 by Aaron Bentley
Added skip labels to edges
217
                num += 1
177 by Aaron Bentley
Restructured graph code as an object
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:
223
                if parent not in dot_nodes:
224
                    dot_nodes[parent] = self.dot_node(parent, num)
225
                    num += 1
226
                edge = Edge(dot_nodes[parent], dot_nodes[node])
227
                if skipped != 0:
228
                    edge.label = "%d" % skipped
229
                node_relations.append(edge)
230
        return node_relations
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
231
232
160 by Aaron Bentley
Restored old graph-ancestry functionality
233
def write_ancestry_file(branch, filename, collapse=True, antialias=True,
178 by Aaron Bentley
Switched from clusters to forced ranking
234
                        merge_branch=None, ranking="forced"):
234 by Aaron Bentley
Adopted Branch.open_containing's new format
235
    b = Branch.open_containing(branch)[0]
180 by Aaron Bentley
Use Grapher for both kinds of graphs
236
    if merge_branch is not None:
234 by Aaron Bentley
Adopted Branch.open_containing's new format
237
        m = Branch.open_containing(merge_branch)[0]
180 by Aaron Bentley
Use Grapher for both kinds of graphs
238
    else:
239
        m = None
258 by Aaron Bentley
Added locking to graph-ancestry
240
    b.lock_read()
241
    try:
242
        if m is not None:
243
            m.lock_read()
244
        try:
245
            grapher = Grapher(b, m)
246
            relations = grapher.get_relations(collapse)
247
        finally:
248
            if m is not None:
249
                m.unlock()
250
    finally:
251
        b.unlock()
160 by Aaron Bentley
Restored old graph-ancestry functionality
252
134 by Aaron Bentley
support multiple image formats for graph-ancestry
253
    ext = filename.split('.')[-1]
178 by Aaron Bentley
Switched from clusters to forced ranking
254
    output = dot_output(relations, ranking)
186 by Aaron Bentley
Warn instead of failing when librsvg is not installed
255
    done = False
187 by Aaron Bentley
Fixed output handling for non-antialiased types
256
    if ext not in RSVG_OUTPUT_TYPES:
257
        antialias = False
258
    if antialias: 
186 by Aaron Bentley
Warn instead of failing when librsvg is not installed
259
        output = list(output)
143 by Aaron Bentley
Used rsvga for nice antialiasing
260
        try:
178 by Aaron Bentley
Switched from clusters to forced ranking
261
            invoke_dot_aa(output, filename, ext)
186 by Aaron Bentley
Warn instead of failing when librsvg is not installed
262
            done = True
143 by Aaron Bentley
Used rsvga for nice antialiasing
263
        except NoDot, e:
264
            raise BzrCommandError("Can't find 'dot'.  Please ensure Graphviz"\
265
                " is installed correctly.")
266
        except NoRsvg, e:
186 by Aaron Bentley
Warn instead of failing when librsvg is not installed
267
            print "Not antialiasing because rsvg (from librsvg-bin) is not"\
268
                " installed."
269
            antialias = False
270
    if ext in DOT_OUTPUT_TYPES and not antialias and not done:
139 by Aaron Bentley
Tweaked missing-dot handling
271
        try:
178 by Aaron Bentley
Switched from clusters to forced ranking
272
            invoke_dot(output, filename, ext)
186 by Aaron Bentley
Warn instead of failing when librsvg is not installed
273
            done = True
139 by Aaron Bentley
Tweaked missing-dot handling
274
        except NoDot, e:
275
            raise BzrCommandError("Can't find 'dot'.  Please ensure Graphviz"\
143 by Aaron Bentley
Used rsvga for nice antialiasing
276
                " is installed correctly, or use --noantialias")
194 by Aaron Bentley
tweaked Meinel's changes
277
    elif ext == 'dot' and not done:
178 by Aaron Bentley
Switched from clusters to forced ranking
278
        my_file = file(filename, 'wb')
279
        for fragment in output:
280
            my_file.write(fragment)
194 by Aaron Bentley
tweaked Meinel's changes
281
    elif ext == 'html':
189.1.1 by John Arbash Meinel
Adding an html target.
282
        try:
283
            invoke_dot_html(output, filename)
284
        except NoDot, e:
285
            raise BzrCommandError("Can't find 'dot'.  Please ensure Graphviz"\
286
                " is installed correctly, or use --noantialias")
186 by Aaron Bentley
Warn instead of failing when librsvg is not installed
287
    elif not done:
134 by Aaron Bentley
support multiple image formats for graph-ancestry
288
        print "Unknown file extension: %s" % ext
131 by Aaron Bentley
Added required filename parameter
289