~abentley/bzrtools/bzrtools.dev

146 by aaron.bentley at utoronto
Patch for missing NoRsvg
1
from dotgraph import Node, dot_output, invoke_dot, invoke_dot_aa, NoDot, NoRsvg
176 by Aaron Bentley
Added skip labels to edges
2
from dotgraph import mail_map, RSVG_OUTPUT_TYPES, DOT_OUTPUT_TYPES, Edge
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
161 by Aaron Bentley
Added committer names to nodes
7
from bzrlib.revision import combined_graph, MultipleRevisionSources
128 by Aaron Bentley
Got initial graphing functionality working
8
import bzrlib.errors
130 by Aaron Bentley
Added committer to revisions
9
import re
142 by Aaron Bentley
Clustered the branch revision history
10
import os.path
128 by Aaron Bentley
Got initial graphing functionality working
11
140 by Aaron Bentley
Mapped some email addresses to names
12
mail_map.update({'aaron.bentley@utoronto.ca'     : 'Aaron Bentley',
13
                 'abentley@panoramicfeedback.com': 'Aaron Bentley',
151 by Aaron Bentley
Added another address to the table
14
                 'abentley@lappy'                : 'Aaron Bentley',
152.1.1 by Aaron Bentley
Matched John's name to his commit name
15
                 'john@arbash-meinel.com'        : 'John Arbash Meinel',
148 by abentley
Updated email addresses translation in graph-ancestry
16
                 'mbp@sourcefrog.net'            : 'Martin Pool',
17
                 'robertc@robertcollins.net'     : 'Robert Collins',
140 by Aaron Bentley
Mapped some email addresses to names
18
                })
19
166 by Aaron Bentley
Fixed username-in-ancestry-graph issue
20
committer_alias = {'abentley': 'Aaron Bentley'}
128 by Aaron Bentley
Got initial graphing functionality working
21
def add_relations(rev_id):
22
    if rev_id in ancestors:
23
        return
24
    print rev_id
25
    if rev_id not in nodes:
26
        nodes[rev_id] = Node("n%d" % counter, label = rev_id)
27
        counter += 1
28
    revision = branch.get_revision(rev_id)
29
    ancestors [rev_id] = []
30
    for p in (p.revision_id for p in revision.parents):
31
        add_relations(p)
32
        if p not in descendants:
33
            descendants[p] = []
34
        descendants[p].append(rev_id)
35
        ancestors [rev_id].append(rev_id)
36
130 by Aaron Bentley
Added committer to revisions
37
def short_committer(committer):
135 by Aaron Bentley
Enhanced revision-crediting
38
    new_committer = re.sub('<.*>', '', committer).strip(' ')
130 by Aaron Bentley
Added committer to revisions
39
    if len(new_committer) < 2:
40
        return committer
41
    return new_committer
136 by Aaron Bentley
Allowed disabling ancestry collapsing
42
145 by aaron.bentley at utoronto
Reduced graph-collapsing aggressivness
43
def can_skip(rev_id, descendants, ancestors):
44
    if rev_id not in descendants:
45
        return False
174 by Aaron Bentley
Added ancestry collapsing to graphs
46
    elif rev_id not in ancestors:
47
        return False
145 by aaron.bentley at utoronto
Reduced graph-collapsing aggressivness
48
    elif len(ancestors[rev_id]) != 1:
49
        return False
174 by Aaron Bentley
Added ancestry collapsing to graphs
50
    elif len(descendants[list(ancestors[rev_id])[0]]) != 1:
145 by aaron.bentley at utoronto
Reduced graph-collapsing aggressivness
51
        return False
52
    elif len(descendants[rev_id]) != 1:
53
        return False
54
    else:
55
        return True
136 by Aaron Bentley
Allowed disabling ancestry collapsing
56
176 by Aaron Bentley
Added skip labels to edges
57
def compact_ancestors(descendants, ancestors, exceptions=()):
174 by Aaron Bentley
Added ancestry collapsing to graphs
58
    new_ancestors={}
59
    skip = set()
60
    for me, my_parents in ancestors.iteritems():
61
        if me in skip:
62
            continue
176 by Aaron Bentley
Added skip labels to edges
63
        new_ancestors[me] = {} 
174 by Aaron Bentley
Added ancestry collapsing to graphs
64
        for parent in my_parents:
65
            new_parent = parent 
176 by Aaron Bentley
Added skip labels to edges
66
            distance = 0
174 by Aaron Bentley
Added ancestry collapsing to graphs
67
            while can_skip(new_parent, descendants, ancestors):
176 by Aaron Bentley
Added skip labels to edges
68
                if new_parent in exceptions:
69
                    break
174 by Aaron Bentley
Added ancestry collapsing to graphs
70
                skip.add(new_parent)
71
                if new_parent in new_ancestors:
72
                    del new_ancestors[new_parent]
73
                new_parent = list(ancestors[new_parent])[0]
176 by Aaron Bentley
Added skip labels to edges
74
                distance += 1
75
            new_ancestors[me][new_parent] = distance
174 by Aaron Bentley
Added ancestry collapsing to graphs
76
    return new_ancestors    
77
136 by Aaron Bentley
Allowed disabling ancestry collapsing
78
def compact_descendants(descendants, ancestors):
79
    new_descendants={}
80
    skip = set()
81
    for me, my_descendants in descendants.iteritems():
82
        if me in skip:
83
            continue
84
        new_descendants[me] = []
85
        for descendant in my_descendants:
86
            new_descendant = descendant
145 by aaron.bentley at utoronto
Reduced graph-collapsing aggressivness
87
            while can_skip(new_descendant, descendants, ancestors):
136 by Aaron Bentley
Allowed disabling ancestry collapsing
88
                skip.add(new_descendant)
89
                if new_descendant in new_descendants:
90
                    del new_descendants[new_descendant]
91
                new_descendant = descendants[new_descendant][0]
92
            new_descendants[me].append(new_descendant)
93
    return new_descendants    
94
95
96
def graph_ancestry(branch, collapse=True):
128 by Aaron Bentley
Got initial graphing functionality working
97
    nodes = {}
98
    q = ((i+1, n) for (i, n) in enumerate(branch.revision_history()))
99
    r = 1
142 by Aaron Bentley
Clustered the branch revision history
100
    try:
101
        branch_name = os.path.basename(branch.base)
102
    except AttributeError:
103
        branch_name = "main"
128 by Aaron Bentley
Got initial graphing functionality working
104
    for (revno, rev_id) in q:
142 by Aaron Bentley
Clustered the branch revision history
105
        nodes[rev_id] = Node("R%d" % revno, color="#ffff00", rev_id=rev_id, 
106
                             cluster=branch_name)
128 by Aaron Bentley
Got initial graphing functionality working
107
108
    ancestors = {} 
109
    descendants = {}
110
    counter = 0
111
    lines = [branch.last_patch()]
112
    while len(lines) > 0:
113
        new_lines = set()
114
        for rev_id in lines:
115
            if rev_id not in nodes:
135 by Aaron Bentley
Enhanced revision-crediting
116
                nodes[rev_id] = Node("n%d" % counter, label=rev_id, 
117
                                     rev_id=rev_id)
128 by Aaron Bentley
Got initial graphing functionality working
118
                counter+=1
119
                
120
            try:
121
                revision = branch.get_revision(rev_id)
122
            except bzrlib.errors.NoSuchRevision:
137 by Aaron Bentley
Put dotted outlines on missing revisions
123
                nodes[rev_id].node_style.append('dotted')
128 by Aaron Bentley
Got initial graphing functionality working
124
                continue
130 by Aaron Bentley
Added committer to revisions
125
            if nodes[rev_id].committer is None:
126
                nodes[rev_id].committer = short_committer(revision.committer)
128 by Aaron Bentley
Got initial graphing functionality working
127
            parent_ids = [r.revision_id for r in revision.parents]
128
            ancestors [rev_id] = parent_ids
129
            for parent in parent_ids:
130
                if parent not in ancestors:
131
                    new_lines.add(parent)
132
                    descendants[parent] = []
133
                descendants[parent].append(rev_id)
134
        lines = new_lines
135
    node_relations = []
136 by Aaron Bentley
Allowed disabling ancestry collapsing
136
160 by Aaron Bentley
Restored old graph-ancestry functionality
137
    for node in nodes.itervalues():
138
        node.label = node.get_label()
136 by Aaron Bentley
Allowed disabling ancestry collapsing
139
    if collapse:
140
        visible_descendants = compact_descendants(descendants, ancestors)
141
    else:
142
        visible_descendants = descendants
129 by Aaron Bentley
Added collapsing for lines of development.
143
                
136 by Aaron Bentley
Allowed disabling ancestry collapsing
144
    for key, values in visible_descendants.iteritems():
128 by Aaron Bentley
Got initial graphing functionality working
145
        for value in values:
146
            node_relations.append((nodes[key], nodes[value]))
147
    return node_relations
131 by Aaron Bentley
Added required filename parameter
148
161 by Aaron Bentley
Added committer names to nodes
149
def get_committer(rev_id, source):
150
    try:
151
        committer = short_committer(source.get_revision(rev_id).committer)
152
    except NoSuchRevision:
153
        try:
173 by Aaron Bentley
Fixed empty committer for null revision
154
            committer = '-'.join(rev_id.split('-')[:-2]).strip(' ')
155
            if committer == '':
156
                return None
161 by Aaron Bentley
Added committer names to nodes
157
        except ValueError:
173 by Aaron Bentley
Fixed empty committer for null revision
158
            return None
166 by Aaron Bentley
Fixed username-in-ancestry-graph issue
159
    if '@' in committer:
160
        try:
161
            committer = mail_map[committer]
162
        except KeyError:
163
            pass
164
    try:
165
        committer = committer_alias[committer]
166
    except KeyError:
167
        pass
168
    return committer
161 by Aaron Bentley
Added committer names to nodes
169
170
174 by Aaron Bentley
Added ancestry collapsing to graphs
171
def graph_merge_pick(branch, other_branch, collapse=False):
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
172
    greedy_fetch(branch, other_branch)
156 by Aaron Bentley
Switched to merge base pick graphing temporarily
173
    revision_a = branch.last_patch()
174
    revision_b = other_branch.last_patch()
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
175
    try:
176
        root, ancestors, descendants, common = \
162 by Aaron Bentley
reverted to using branch
177
            combined_graph(revision_a, revision_b, branch)
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
178
    except bzrlib.errors.NoCommonRoot:
179
        raise bzrlib.errors.NoCommonAncestor(revision_a, revision_b)
156 by Aaron Bentley
Switched to merge base pick graphing temporarily
180
    distances = node_distances(descendants, ancestors, root)
181
    base = select_farthest(distances, common)
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
182
    n_history = branch.revision_history()
169 by Aaron Bentley
Got ancestry-graph showing common and revision-history nodes properly
183
    m_history = other_branch.revision_history() 
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
184
    dot_nodes = {}
185
    def dot_node(node, num):
156 by Aaron Bentley
Switched to merge base pick graphing temporarily
186
        try:
187
            n_rev = n_history.index(node) + 1
188
        except ValueError:
189
            n_rev = None
190
        try:
191
            m_rev = m_history.index(node) + 1
192
        except ValueError:
193
            m_rev = None
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
194
        if (n_rev, m_rev) == (None, None):
168 by Aaron Bentley
lengthened graph node name to include null:
195
            name = node[-5:]
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
196
            cluster = None
197
        elif n_rev == m_rev:
198
            name = "rR%d" % n_rev
199
        else:
200
            namelist = []
164 by Aaron Bentley
Fixed varname reuse generating nodes
201
            for prefix, revno in (('r', n_rev), ('R', m_rev)):
202
                if revno is not None:
203
                    namelist.append("%s%d" % (prefix, revno))
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
204
            name = ' '.join(namelist)
205
        if None not in (n_rev, m_rev):
206
            cluster = "common_history"
207
            color = "#ff9900"
208
        elif (None, None) == (n_rev, m_rev):
209
            cluster = None
210
            if node in common:
211
                color = "#6699ff"
212
            else:
213
                color = None
214
        elif n_rev is not None:
215
            cluster = "my_history"
216
            color = "#ffff00"
217
        else:
218
            assert m_rev is not None
219
            cluster = "other_history"
220
            color = "#ff0000"
221
        if node == base:
222
            color = "#33ff99"
223
224
        label = [name]
162 by Aaron Bentley
reverted to using branch
225
        committer = get_committer(node, branch)
161 by Aaron Bentley
Added committer names to nodes
226
        if committer is not None:
227
            label.append(committer)
228
156 by Aaron Bentley
Switched to merge base pick graphing temporarily
229
        if node in distances:
157 by Aaron Bentley
Got graph showing merge selection decently
230
            label.append('d%d' % distances[node])
172 by Aaron Bentley
Marked missing nodes
231
232
        d_node = Node("n%d" % num, color=color, label="\\n".join(label), 
157 by Aaron Bentley
Got graph showing merge selection decently
233
                    rev_id=node, cluster=cluster)
172 by Aaron Bentley
Marked missing nodes
234
        if node not in ancestors:
235
            d_node.node_style.append('dotted')
236
237
        return d_node
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
238
239
    node_relations = []
171 by Aaron Bentley
Reorganized node-adding
240
    num = 0
174 by Aaron Bentley
Added ancestry collapsing to graphs
241
    if collapse:
176 by Aaron Bentley
Added skip labels to edges
242
        visible_ancestors = compact_ancestors(descendants, ancestors, (base,))
174 by Aaron Bentley
Added ancestry collapsing to graphs
243
    else:
244
        visible_ancestors = ancestors
245
    for node, parents in visible_ancestors.iteritems():
157 by Aaron Bentley
Got graph showing merge selection decently
246
        if node not in dot_nodes:
171 by Aaron Bentley
Reorganized node-adding
247
            dot_nodes[node] = dot_node(node, num)
248
            num += 1
176 by Aaron Bentley
Added skip labels to edges
249
        if visible_ancestors is ancestors:
250
            parent_iter = ((f, 0) for f in parents)
251
        else:
252
            parent_iter = (f for f in parents.iteritems())
253
        for parent, skipped in parent_iter:
171 by Aaron Bentley
Reorganized node-adding
254
            if parent not in dot_nodes:
255
                dot_nodes[parent] = dot_node(parent, num)
176 by Aaron Bentley
Added skip labels to edges
256
                num += 1
257
            edge = Edge(dot_nodes[parent], dot_nodes[node])
258
            if skipped != 0:
259
                edge.label = "%d" % skipped
260
            node_relations.append(edge)
156 by Aaron Bentley
Switched to merge base pick graphing temporarily
261
    return node_relations
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
262
263
160 by Aaron Bentley
Restored old graph-ancestry functionality
264
def write_ancestry_file(branch, filename, collapse=True, antialias=True,
265
                        merge_branch=None):
158 by Aaron Bentley
Updated to match API changes
266
    b = Branch.open_containing(branch)
160 by Aaron Bentley
Restored old graph-ancestry functionality
267
    if merge_branch is None:
268
        relations = graph_ancestry(b, collapse)
269
    else:
165 by Aaron Bentley
Got ancestry graph working properly for merges
270
        m = Branch.open_containing(merge_branch)
174 by Aaron Bentley
Added ancestry collapsing to graphs
271
        relations = graph_merge_pick(b, m, collapse)
160 by Aaron Bentley
Restored old graph-ancestry functionality
272
134 by Aaron Bentley
support multiple image formats for graph-ancestry
273
    ext = filename.split('.')[-1]
167 by Aaron Bentley
Moved extention lists to dotgraph
274
    if antialias and ext in RSVG_OUTPUT_TYPES:
143 by Aaron Bentley
Used rsvga for nice antialiasing
275
        try:
276
            invoke_dot_aa(dot_output(relations), filename, ext)
277
        except NoDot, e:
278
            raise BzrCommandError("Can't find 'dot'.  Please ensure Graphviz"\
279
                " is installed correctly.")
280
        except NoRsvg, e:
281
            raise BzrCommandError("Can't find 'rsvg'.  Please ensure "\
282
                "librsvg-bin is installed correctly, or use --noantialias.")
167 by Aaron Bentley
Moved extention lists to dotgraph
283
    elif ext in DOT_OUTPUT_TYPES:
139 by Aaron Bentley
Tweaked missing-dot handling
284
        try:
285
            invoke_dot(dot_output(relations), filename, ext)
286
        except NoDot, e:
287
            raise BzrCommandError("Can't find 'dot'.  Please ensure Graphviz"\
143 by Aaron Bentley
Used rsvga for nice antialiasing
288
                " is installed correctly, or use --noantialias")
134 by Aaron Bentley
support multiple image formats for graph-ancestry
289
    elif ext=='dot':
290
        file(filename, 'wb').write("".join(list(dot_output(relations))))
291
    else:
292
        print "Unknown file extension: %s" % ext
131 by Aaron Bentley
Added required filename parameter
293