~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
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
22
from bzrlib.fetch import greedy_fetch
156 by Aaron Bentley
Switched to merge base pick graphing temporarily
23
from bzrlib.graph import node_distances, select_farthest
180 by Aaron Bentley
Use Grapher for both kinds of graphs
24
from bzrlib.revision import combined_graph, revision_graph
25
from bzrlib.revision import MultipleRevisionSources
128 by Aaron Bentley
Got initial graphing functionality working
26
import bzrlib.errors
130 by Aaron Bentley
Added committer to revisions
27
import re
142 by Aaron Bentley
Clustered the branch revision history
28
import os.path
189.1.1 by John Arbash Meinel
Adding an html target.
29
import time
128 by Aaron Bentley
Got initial graphing functionality working
30
183 by Aaron Bentley
Code cleanup
31
mail_map = {'aaron.bentley@utoronto.ca'     : 'Aaron Bentley',
32
            'abentley@panoramicfeedback.com': 'Aaron Bentley',
33
            'abentley@lappy'                : 'Aaron Bentley',
34
            'john@arbash-meinel.com'        : 'John Arbash Meinel',
35
            'mbp@sourcefrog.net'            : 'Martin Pool',
36
            'robertc@robertcollins.net'     : 'Robert Collins',
37
            }
140 by Aaron Bentley
Mapped some email addresses to names
38
166 by Aaron Bentley
Fixed username-in-ancestry-graph issue
39
committer_alias = {'abentley': 'Aaron Bentley'}
145 by aaron.bentley at utoronto
Reduced graph-collapsing aggressivness
40
def can_skip(rev_id, descendants, ancestors):
41
    if rev_id not in descendants:
42
        return False
174 by Aaron Bentley
Added ancestry collapsing to graphs
43
    elif rev_id not in ancestors:
44
        return False
145 by aaron.bentley at utoronto
Reduced graph-collapsing aggressivness
45
    elif len(ancestors[rev_id]) != 1:
46
        return False
174 by Aaron Bentley
Added ancestry collapsing to graphs
47
    elif len(descendants[list(ancestors[rev_id])[0]]) != 1:
145 by aaron.bentley at utoronto
Reduced graph-collapsing aggressivness
48
        return False
49
    elif len(descendants[rev_id]) != 1:
50
        return False
51
    else:
52
        return True
136 by Aaron Bentley
Allowed disabling ancestry collapsing
53
176 by Aaron Bentley
Added skip labels to edges
54
def compact_ancestors(descendants, ancestors, exceptions=()):
174 by Aaron Bentley
Added ancestry collapsing to graphs
55
    new_ancestors={}
56
    skip = set()
57
    for me, my_parents in ancestors.iteritems():
58
        if me in skip:
59
            continue
176 by Aaron Bentley
Added skip labels to edges
60
        new_ancestors[me] = {} 
174 by Aaron Bentley
Added ancestry collapsing to graphs
61
        for parent in my_parents:
62
            new_parent = parent 
176 by Aaron Bentley
Added skip labels to edges
63
            distance = 0
174 by Aaron Bentley
Added ancestry collapsing to graphs
64
            while can_skip(new_parent, descendants, ancestors):
176 by Aaron Bentley
Added skip labels to edges
65
                if new_parent in exceptions:
66
                    break
174 by Aaron Bentley
Added ancestry collapsing to graphs
67
                skip.add(new_parent)
68
                if new_parent in new_ancestors:
69
                    del new_ancestors[new_parent]
70
                new_parent = list(ancestors[new_parent])[0]
176 by Aaron Bentley
Added skip labels to edges
71
                distance += 1
72
            new_ancestors[me][new_parent] = distance
174 by Aaron Bentley
Added ancestry collapsing to graphs
73
    return new_ancestors    
74
189.1.1 by John Arbash Meinel
Adding an html target.
75
def get_rev_info(rev_id, source):
76
    """Return the committer, message, and date of a revision."""
77
    committer = None
78
    message = None
79
    date = None
80
    if rev_id == 'null:':
81
        return None, 'Null Revision', None
161 by Aaron Bentley
Added committer names to nodes
82
    try:
189.1.1 by John Arbash Meinel
Adding an html target.
83
        rev = source.get_revision(rev_id)
161 by Aaron Bentley
Added committer names to nodes
84
    except NoSuchRevision:
85
        try:
173 by Aaron Bentley
Fixed empty committer for null revision
86
            committer = '-'.join(rev_id.split('-')[:-2]).strip(' ')
87
            if committer == '':
189.1.1 by John Arbash Meinel
Adding an html target.
88
                return None, None, None
161 by Aaron Bentley
Added committer names to nodes
89
        except ValueError:
189.1.1 by John Arbash Meinel
Adding an html target.
90
            return None, None, None
91
    else:
92
        committer = short_committer(rev.committer)
93
        if rev.message is not None:
94
            message = rev.message.split('\n')[0]
194 by Aaron Bentley
tweaked Meinel's changes
95
        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
96
        date = time.strftime('%Y/%m/%d', gmtime)
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
189.1.1 by John Arbash Meinel
Adding an html target.
106
    return committer, message, 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:
115
            greedy_fetch(branch, 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 = \
119
                    combined_graph(revision_a, revision_b, self.branch)
120
            except bzrlib.errors.NoCommonRoot:
121
                raise bzrlib.errors.NoCommonAncestor(revision_a, revision_b)
122
        else:
123
            self.root, self.ancestors, self.descendants = \
124
                revision_graph(revision_a, branch)
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]
189.1.1 by John Arbash Meinel
Adding an html target.
177
        committer, message, date = get_rev_info(node, self.branch)
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
258 by Aaron Bentley
Added locking to graph-ancestry
235
    b.lock_read()
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:
275
            my_file.write(fragment)
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