~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
176 by Aaron Bentley
Added skip labels to edges
59
        new_ancestors[me] = {} 
174 by Aaron Bentley
Added ancestry collapsing to graphs
60
        for parent in my_parents:
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
174 by Aaron Bentley
Added ancestry collapsing to graphs
72
    return new_ancestors    
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:':
80
        return None, 'Null Revision', 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 == '':
189.1.1 by John Arbash Meinel
Adding an html target.
87
                return None, None, None
161 by Aaron Bentley
Added committer names to nodes
88
        except ValueError:
189.1.1 by John Arbash Meinel
Adding an html target.
89
            return None, None, None
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)
166 by Aaron Bentley
Fixed username-in-ancestry-graph issue
96
    if '@' in committer:
97
        try:
98
            committer = mail_map[committer]
99
        except KeyError:
100
            pass
101
    try:
102
        committer = committer_alias[committer]
103
    except KeyError:
104
        pass
189.1.1 by John Arbash Meinel
Adding an html target.
105
    return committer, message, date
161 by Aaron Bentley
Added committer names to nodes
106
177 by Aaron Bentley
Restructured graph code as an object
107
class Grapher(object):
180 by Aaron Bentley
Use Grapher for both kinds of graphs
108
    def __init__(self, branch, other_branch=None):
177 by Aaron Bentley
Restructured graph code as an object
109
        object.__init__(self)
110
        self.branch = branch
111
        self.other_branch = other_branch
208 by Aaron Bentley
Fixed last_revision calls
112
        revision_a = self.branch.last_revision()
180 by Aaron Bentley
Use Grapher for both kinds of graphs
113
        if other_branch is not None:
317 by Aaron Bentley
Fixed fetching code to use Branch.fetch
114
            branch.fetch(other_branch)
208 by Aaron Bentley
Fixed last_revision calls
115
            revision_b = self.other_branch.last_revision()
180 by Aaron Bentley
Use Grapher for both kinds of graphs
116
            try:
117
                self.root, self.ancestors, self.descendants, self.common = \
309 by Aaron Bentley
Fixed graph-ancestry
118
                    combined_graph(revision_a, revision_b,
119
                                   self.branch.repository)
180 by Aaron Bentley
Use Grapher for both kinds of graphs
120
            except bzrlib.errors.NoCommonRoot:
121
                raise bzrlib.errors.NoCommonAncestor(revision_a, revision_b)
122
        else:
123
            self.root, self.ancestors, self.descendants = \
309 by Aaron Bentley
Fixed graph-ancestry
124
                revision_graph(revision_a, branch.repository)
180 by Aaron Bentley
Use Grapher for both kinds of graphs
125
            self.common = []
126
127
        self.n_history = branch.revision_history()
177 by Aaron Bentley
Restructured graph code as an object
128
        self.distances = node_distances(self.descendants, self.ancestors, 
129
                                        self.root)
180 by Aaron Bentley
Use Grapher for both kinds of graphs
130
        if other_branch is not None:
131
            self.base = select_farthest(self.distances, self.common)
132
            self.m_history = other_branch.revision_history() 
133
        else:
134
            self.base = None
135
            self.m_history = []
161 by Aaron Bentley
Added committer names to nodes
136
177 by Aaron Bentley
Restructured graph code as an object
137
    def dot_node(self, node, num):
156 by Aaron Bentley
Switched to merge base pick graphing temporarily
138
        try:
177 by Aaron Bentley
Restructured graph code as an object
139
            n_rev = self.n_history.index(node) + 1
156 by Aaron Bentley
Switched to merge base pick graphing temporarily
140
        except ValueError:
141
            n_rev = None
142
        try:
177 by Aaron Bentley
Restructured graph code as an object
143
            m_rev = self.m_history.index(node) + 1
156 by Aaron Bentley
Switched to merge base pick graphing temporarily
144
        except ValueError:
145
            m_rev = None
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
146
        if (n_rev, m_rev) == (None, None):
168 by Aaron Bentley
lengthened graph node name to include null:
147
            name = node[-5:]
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
148
            cluster = None
149
        elif n_rev == m_rev:
150
            name = "rR%d" % n_rev
151
        else:
152
            namelist = []
164 by Aaron Bentley
Fixed varname reuse generating nodes
153
            for prefix, revno in (('r', n_rev), ('R', m_rev)):
154
                if revno is not None:
155
                    namelist.append("%s%d" % (prefix, revno))
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
156
            name = ' '.join(namelist)
157
        if None not in (n_rev, m_rev):
158
            cluster = "common_history"
159
            color = "#ff9900"
160
        elif (None, None) == (n_rev, m_rev):
161
            cluster = None
177 by Aaron Bentley
Restructured graph code as an object
162
            if node in self.common:
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
163
                color = "#6699ff"
164
            else:
190 by Aaron Bentley
Set normal revisions to white
165
                color = "white"
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
166
        elif n_rev is not None:
167
            cluster = "my_history"
168
            color = "#ffff00"
169
        else:
170
            assert m_rev is not None
171
            cluster = "other_history"
172
            color = "#ff0000"
177 by Aaron Bentley
Restructured graph code as an object
173
        if node == self.base:
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
174
            color = "#33ff99"
175
176
        label = [name]
309 by Aaron Bentley
Fixed graph-ancestry
177
        committer, message, date = get_rev_info(node, self.branch.repository)
161 by Aaron Bentley
Added committer names to nodes
178
        if committer is not None:
179
            label.append(committer)
180
189.1.1 by John Arbash Meinel
Adding an html target.
181
        if date is not None:
182
            label.append(date)
188 by Aaron Bentley
Added dates to graph
183
177 by Aaron Bentley
Restructured graph code as an object
184
        if node in self.distances:
178 by Aaron Bentley
Switched from clusters to forced ranking
185
            rank = self.distances[node]
177 by Aaron Bentley
Restructured graph code as an object
186
            label.append('d%d' % self.distances[node])
178 by Aaron Bentley
Switched from clusters to forced ranking
187
        else:
188
            rank = None
172 by Aaron Bentley
Marked missing nodes
189
190
        d_node = Node("n%d" % num, color=color, label="\\n".join(label), 
189.1.1 by John Arbash Meinel
Adding an html target.
191
                    rev_id=node, cluster=cluster, message=message,
192
                    date=date)
178 by Aaron Bentley
Switched from clusters to forced ranking
193
        d_node.rank = rank
194
177 by Aaron Bentley
Restructured graph code as an object
195
        if node not in self.ancestors:
172 by Aaron Bentley
Marked missing nodes
196
            d_node.node_style.append('dotted')
197
198
        return d_node
177 by Aaron Bentley
Restructured graph code as an object
199
        
200
    def get_relations(self, collapse=False):
201
        dot_nodes = {}
202
        node_relations = []
203
        num = 0
204
        if collapse:
205
            visible_ancestors = compact_ancestors(self.descendants, 
206
                                                  self.ancestors, (self.base,))
176 by Aaron Bentley
Added skip labels to edges
207
        else:
177 by Aaron Bentley
Restructured graph code as an object
208
            visible_ancestors = self.ancestors
209
        for node, parents in visible_ancestors.iteritems():
210
            if node not in dot_nodes:
211
                dot_nodes[node] = self.dot_node(node, num)
176 by Aaron Bentley
Added skip labels to edges
212
                num += 1
177 by Aaron Bentley
Restructured graph code as an object
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:
218
                if parent not in dot_nodes:
219
                    dot_nodes[parent] = self.dot_node(parent, num)
220
                    num += 1
221
                edge = Edge(dot_nodes[parent], dot_nodes[node])
222
                if skipped != 0:
223
                    edge.label = "%d" % skipped
224
                node_relations.append(edge)
225
        return node_relations
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
226
227
160 by Aaron Bentley
Restored old graph-ancestry functionality
228
def write_ancestry_file(branch, filename, collapse=True, antialias=True,
178 by Aaron Bentley
Switched from clusters to forced ranking
229
                        merge_branch=None, ranking="forced"):
234 by Aaron Bentley
Adopted Branch.open_containing's new format
230
    b = Branch.open_containing(branch)[0]
180 by Aaron Bentley
Use Grapher for both kinds of graphs
231
    if merge_branch is not None:
234 by Aaron Bentley
Adopted Branch.open_containing's new format
232
        m = Branch.open_containing(merge_branch)[0]
180 by Aaron Bentley
Use Grapher for both kinds of graphs
233
    else:
234
        m = None
309 by Aaron Bentley
Fixed graph-ancestry
235
    b.lock_write()
258 by Aaron Bentley
Added locking to graph-ancestry
236
    try:
237
        if m is not None:
238
            m.lock_read()
239
        try:
240
            grapher = Grapher(b, m)
241
            relations = grapher.get_relations(collapse)
242
        finally:
243
            if m is not None:
244
                m.unlock()
245
    finally:
246
        b.unlock()
160 by Aaron Bentley
Restored old graph-ancestry functionality
247
134 by Aaron Bentley
support multiple image formats for graph-ancestry
248
    ext = filename.split('.')[-1]
178 by Aaron Bentley
Switched from clusters to forced ranking
249
    output = dot_output(relations, ranking)
186 by Aaron Bentley
Warn instead of failing when librsvg is not installed
250
    done = False
187 by Aaron Bentley
Fixed output handling for non-antialiased types
251
    if ext not in RSVG_OUTPUT_TYPES:
252
        antialias = False
253
    if antialias: 
186 by Aaron Bentley
Warn instead of failing when librsvg is not installed
254
        output = list(output)
143 by Aaron Bentley
Used rsvga for nice antialiasing
255
        try:
178 by Aaron Bentley
Switched from clusters to forced ranking
256
            invoke_dot_aa(output, filename, ext)
186 by Aaron Bentley
Warn instead of failing when librsvg is not installed
257
            done = True
143 by Aaron Bentley
Used rsvga for nice antialiasing
258
        except NoDot, e:
259
            raise BzrCommandError("Can't find 'dot'.  Please ensure Graphviz"\
260
                " is installed correctly.")
261
        except NoRsvg, e:
186 by Aaron Bentley
Warn instead of failing when librsvg is not installed
262
            print "Not antialiasing because rsvg (from librsvg-bin) is not"\
263
                " installed."
264
            antialias = False
265
    if ext in DOT_OUTPUT_TYPES and not antialias and not done:
139 by Aaron Bentley
Tweaked missing-dot handling
266
        try:
178 by Aaron Bentley
Switched from clusters to forced ranking
267
            invoke_dot(output, filename, ext)
186 by Aaron Bentley
Warn instead of failing when librsvg is not installed
268
            done = True
139 by Aaron Bentley
Tweaked missing-dot handling
269
        except NoDot, e:
270
            raise BzrCommandError("Can't find 'dot'.  Please ensure Graphviz"\
143 by Aaron Bentley
Used rsvga for nice antialiasing
271
                " is installed correctly, or use --noantialias")
194 by Aaron Bentley
tweaked Meinel's changes
272
    elif ext == 'dot' and not done:
178 by Aaron Bentley
Switched from clusters to forced ranking
273
        my_file = file(filename, 'wb')
274
        for fragment in output:
321.1.1 by Aaron Bentley
Forced dot output to UTF-8
275
            my_file.write(fragment.encode('utf-8'))
194 by Aaron Bentley
tweaked Meinel's changes
276
    elif ext == 'html':
189.1.1 by John Arbash Meinel
Adding an html target.
277
        try:
278
            invoke_dot_html(output, filename)
279
        except NoDot, e:
280
            raise BzrCommandError("Can't find 'dot'.  Please ensure Graphviz"\
281
                " is installed correctly, or use --noantialias")
186 by Aaron Bentley
Warn instead of failing when librsvg is not installed
282
    elif not done:
134 by Aaron Bentley
support multiple image formats for graph-ancestry
283
        print "Unknown file extension: %s" % ext
131 by Aaron Bentley
Added required filename parameter
284