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