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
3
from bzrlib.branch import Branch
4
from bzrlib.errors import BzrCommandError, NoCommonRoot, NoSuchRevision
5
from bzrlib.fetch import greedy_fetch
6
from bzrlib.graph import node_distances, select_farthest
7
from bzrlib.revision import combined_graph, revision_graph
8
from bzrlib.revision import MultipleRevisionSources
9
from datetime import datetime
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',
22
committer_alias = {'abentley': 'Aaron Bentley'}
23
def short_committer(committer):
24
new_committer = re.sub('<.*>', '', committer).strip(' ')
25
if len(new_committer) < 2:
29
def can_skip(rev_id, descendants, ancestors):
30
if rev_id not in descendants:
32
elif rev_id not in ancestors:
34
elif len(ancestors[rev_id]) != 1:
36
elif len(descendants[list(ancestors[rev_id])[0]]) != 1:
38
elif len(descendants[rev_id]) != 1:
43
def compact_ancestors(descendants, ancestors, exceptions=()):
46
for me, my_parents in ancestors.iteritems():
49
new_ancestors[me] = {}
50
for parent in my_parents:
53
while can_skip(new_parent, descendants, ancestors):
54
if new_parent in exceptions:
57
if new_parent in new_ancestors:
58
del new_ancestors[new_parent]
59
new_parent = list(ancestors[new_parent])[0]
61
new_ancestors[me][new_parent] = distance
64
def get_committer(rev_id, source):
66
committer = short_committer(source.get_revision(rev_id).committer)
67
except NoSuchRevision:
69
committer = '-'.join(rev_id.split('-')[:-2]).strip(' ')
76
committer = mail_map[committer]
80
committer = committer_alias[committer]
85
class Grapher(object):
86
def __init__(self, branch, other_branch=None):
89
self.other_branch = other_branch
90
revision_a = self.branch.last_patch()
91
if other_branch is not None:
92
greedy_fetch(branch, other_branch)
93
revision_b = self.other_branch.last_patch()
95
self.root, self.ancestors, self.descendants, self.common = \
96
combined_graph(revision_a, revision_b, self.branch)
97
except bzrlib.errors.NoCommonRoot:
98
raise bzrlib.errors.NoCommonAncestor(revision_a, revision_b)
100
self.root, self.ancestors, self.descendants = \
101
revision_graph(revision_a, branch)
104
self.n_history = branch.revision_history()
105
self.distances = node_distances(self.descendants, self.ancestors,
107
if other_branch is not None:
108
self.base = select_farthest(self.distances, self.common)
109
self.m_history = other_branch.revision_history()
114
def get_timestamp(self, revision_id):
116
return float(self.branch.get_revision(revision_id).timestamp)
117
except NoSuchRevision:
120
def dot_node(self, node, num):
122
n_rev = self.n_history.index(node) + 1
126
m_rev = self.m_history.index(node) + 1
129
if (n_rev, m_rev) == (None, None):
133
name = "rR%d" % n_rev
136
for prefix, revno in (('r', n_rev), ('R', m_rev)):
137
if revno is not None:
138
namelist.append("%s%d" % (prefix, revno))
139
name = ' '.join(namelist)
140
if None not in (n_rev, m_rev):
141
cluster = "common_history"
143
elif (None, None) == (n_rev, m_rev):
145
if node in self.common:
149
elif n_rev is not None:
150
cluster = "my_history"
153
assert m_rev is not None
154
cluster = "other_history"
156
if node == self.base:
160
committer = get_committer(node, self.branch)
161
if committer is not None:
162
label.append(committer)
164
timestamp = self.get_timestamp(node)
165
if timestamp is not None:
166
date = datetime.fromtimestamp(timestamp)
167
label.append("%d/%d/%d" % (date.year, date.month, date.day))
169
if node in self.distances:
170
rank = self.distances[node]
171
label.append('d%d' % self.distances[node])
175
d_node = Node("n%d" % num, color=color, label="\\n".join(label),
176
rev_id=node, cluster=cluster)
179
if node not in self.ancestors:
180
d_node.node_style.append('dotted')
185
def get_relations(self, collapse=False):
190
visible_ancestors = compact_ancestors(self.descendants,
191
self.ancestors, (self.base,))
193
visible_ancestors = self.ancestors
194
for node, parents in visible_ancestors.iteritems():
195
if node not in dot_nodes:
196
dot_nodes[node] = self.dot_node(node, num)
198
if visible_ancestors is self.ancestors:
199
parent_iter = ((f, 0) for f in parents)
201
parent_iter = (f for f in parents.iteritems())
202
for parent, skipped in parent_iter:
203
if parent not in dot_nodes:
204
dot_nodes[parent] = self.dot_node(parent, num)
206
edge = Edge(dot_nodes[parent], dot_nodes[node])
208
edge.label = "%d" % skipped
209
node_relations.append(edge)
210
return node_relations
213
def write_ancestry_file(branch, filename, collapse=True, antialias=True,
214
merge_branch=None, ranking="forced"):
215
b = Branch.open_containing(branch)
216
if merge_branch is not None:
217
m = Branch.open_containing(merge_branch)
220
grapher = Grapher(b, m)
221
relations = grapher.get_relations(collapse)
223
ext = filename.split('.')[-1]
224
output = dot_output(relations, ranking)
226
if ext not in RSVG_OUTPUT_TYPES:
229
output = list(output)
231
invoke_dot_aa(output, filename, ext)
234
raise BzrCommandError("Can't find 'dot'. Please ensure Graphviz"\
235
" is installed correctly.")
237
print "Not antialiasing because rsvg (from librsvg-bin) is not"\
240
if ext in DOT_OUTPUT_TYPES and not antialias and not done:
242
invoke_dot(output, filename, ext)
245
raise BzrCommandError("Can't find 'dot'. Please ensure Graphviz"\
246
" is installed correctly, or use --noantialias")
247
elif ext=='dot' and not done:
248
my_file = file(filename, 'wb')
249
for fragment in output:
250
my_file.write(fragment)
252
print "Unknown file extension: %s" % ext