~abentley/bzrtools/bzrtools.dev

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