1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
|
#!/usr/bin/python
# -*- coding: UTF-8 -*-
"""Directed graph production.
This module contains the code to produce an ordered directed graph of a
bzr branch, such as we display in the tree view at the top of the bzrk
window.
"""
__copyright__ = "Copyright © 2005 Canonical Ltd."
__author__ = "Scott James Remnant <scott@ubuntu.com>"
from bzrlib.tsort import merge_sort
class DummyRevision(object):
"""Dummy bzr revision.
Sometimes, especially in older bzr branches, a revision is referenced
as the parent of another but not actually present in the branch's store.
When this happens we use an instance of this class instead of the real
Revision object (which we can't get).
"""
def __init__(self, revid):
self.revision_id = revid
self.parent_ids = []
self.committer = None
self.message = self.revision_id
class RevisionProxy(object):
"""A revision proxy object.
This will demand load the revision it represents when the committer or
message attributes are accessed in order to populate them. It is
constructed with the revision id and parent_ids list and a repository
object to request the revision from when needed.
"""
def __init__(self, revid, parent_ids, repository):
self.revision_id = revid
self.parent_ids = parent_ids
self._repository = repository
self._revision = None
def _get_attribute_getter(attr):
def get_attribute(self):
if self._revision is None:
self._load()
return getattr(self._revision, attr)
return get_attribute
committer = property(_get_attribute_getter('committer'))
message = property(_get_attribute_getter('message'))
properties = property(_get_attribute_getter('properties'))
timestamp = property(_get_attribute_getter('timestamp'))
timezone = property(_get_attribute_getter('timezone'))
def _load(self):
"""Load the revision object."""
self._revision = self._repository.get_revision(self.revision_id)
class DistanceMethod(object):
def __init__(self, branch, start):
self.branch = branch
self.start = start
self.revisions = {}
self.children = {}
self.children_of_id = {start: set()}
self.parent_ids_of = {}
self.colours = { start: 0 }
self.last_colour = 0
self.direct_parent_of = {}
self.graph = {}
def fill_caches(self):
# FIXME: look at using repository.get_revision_graph_with_ghosts - RBC.
graph = self.branch.repository.get_revision_graph_with_ghosts([self.start])
for revid in graph.ghosts:
self.cache_revision(DummyRevision(revid))
for revid, parents in graph.get_ancestors().items():
self.cache_revision(RevisionProxy(revid, parents, self.branch.repository))
def cache_revision(self, revision):
"Set the caches for a newly retrieved revision."""
revid = revision.revision_id
# Build a revision cache
self.revisions[revid] = revision
# Build a children dictionary
for parent_id in revision.parent_ids:
self.children_of_id.setdefault(parent_id, set()).add(revision)
# Build a parents dictionnary, where redundant parents will be removed,
# and that will be passed along tothe rest of program.
if len(revision.parent_ids) != len(set(revision.parent_ids)):
# fix the parent_ids list.
parent_ids = []
parent_ids_set = set()
for parent_id in revision.parent_ids:
if parent_id in parent_ids_set:
continue
parent_ids.append(parent_id)
parent_ids_set.add(parent_id)
revision.parent_ids = parent_ids
self.parent_ids_of[revision] = list(revision.parent_ids)
self.graph[revid] = revision.parent_ids
def make_children_map(self):
revisions = self.revisions
return dict((revisions[revid], c)
for (revid, c) in self.children_of_id.iteritems())
def sort_revisions(self, sorted_revids, maxnum):
revisions = self.revisions
parent_ids_of = self.parent_ids_of
children_of_id = self.children_of_id
# Try to compact sequences of revisions on the same branch.
distances = {}
skipped_revids = []
expected_id = sorted_revids[0]
pending_ids = []
while True:
revid = sorted_revids.pop(0)
if revid != expected_id:
skipped_revids.append(revid)
continue
revision = revisions[revid]
for child in children_of_id[revid]:
# postpone if any child is missing
if child.revision_id not in distances:
if expected_id not in pending_ids:
pending_ids.append(expected_id)
expected_id = pending_ids.pop(0)
skipped_revids.append(revid)
sorted_revids[:0] = skipped_revids
del skipped_revids[:]
break
else:
# all children are here, push!
distances[revid] = len(distances)
if maxnum is not None and len(distances) > maxnum:
# bail out early if a limit was specified
sorted_revids[:0] = skipped_revids
for revid in sorted_revids:
distances[revid] = len(distances)
break
# all parents will need to be pushed as soon as possible
for parent in parent_ids_of[revision]:
if parent not in pending_ids:
pending_ids.insert(0, parent)
if not pending_ids:
break
expected_id = pending_ids.pop(0)
# if the next expected revid has already been skipped, requeue
# the skipped ids, except those that would go right back to the
# skipped list.
if expected_id in skipped_revids:
pos = skipped_revids.index(expected_id)
sorted_revids[:0] = skipped_revids[pos:]
del skipped_revids[pos:]
self.distances = distances
return sorted(distances, key=distances.get)
def choose_colour(self, revid):
revision = self.revisions[revid]
children_of_id = self.children_of_id
parent_ids_of = self.parent_ids_of
colours = self.colours
# choose colour
the_children = children_of_id[revid]
if len(the_children) == 1:
[child] = the_children
if len(parent_ids_of[child]) == 1:
# one-one relationship between parent and child, same
# colour
colours[revid] = colours[child.revision_id]
else:
self.choose_colour_one_child(revision, child)
else:
self.choose_colour_many_children(revision, the_children)
def choose_colour_one_child(self, revision, child):
revid = revision.revision_id
direct_parent_of = self.direct_parent_of
revisions = self.revisions
# one child with multiple parents, the first parent with
# the same committer gets the colour
direct_parent = direct_parent_of.get(child)
if direct_parent is None:
# if it has not been found yet, find it now and remember
for parent_id in self.parent_ids_of[child]:
parent_revision = revisions[parent_id]
if parent_revision.committer == child.committer:
# found the first parent with the same committer
direct_parent = parent_revision
direct_parent_of[child] = direct_parent
break
if direct_parent == revision:
self.colours[revid] = self.colours[child.revision_id]
else:
self.colours[revid] = self.last_colour = self.last_colour + 1
def choose_colour_many_children(self, revision, the_children):
"""Colour revision revision."""
revid = revision.revision_id
direct_parent_of = self.direct_parent_of
# multiple children, get the colour of the last displayed child
# with the same committer which does not already have its colour
# taken
available = {}
for child in the_children:
if child.committer != revision.committer:
continue
direct_parent = direct_parent_of.get(child)
if direct_parent == revision:
self.colours[revid] = self.colours[child.revision_id]
break
# FIXME: Colouring based on whats been displayed MUST be done with
# knowledge of the revisions being output.
# until the refactoring to fold graph() into this more compactly is
# done, I've disabled this reuse. RBC 20060403
# if direct_parent is None:
# available[child] = distances[child.revision_id]
# .. it will be something like available[child] = \
# revs[child.revision_id][0] - which is the sequence number
else:
if available:
sorted_children = sorted(available, key=available.get)
child = sorted_children[-1]
direct_parent_of[child] = revision
self.colours[revid] = self.colours[child.revision_id]
else:
# no candidate children is available, pick the next
# colour
self.colours[revid] = self.last_colour = self.last_colour + 1
def distances(branch, start, maxnum):
"""Sort the revisions.
Traverses the branch revision tree starting at start and produces an
ordered list of revisions such that a revision always comes after
any revision it is the parent of.
Returns a tuple of (revids, revisions, colours, children)
"""
distance = DistanceMethod(branch, start)
distance.fill_caches()
distance.merge_sorted = merge_sort(distance.graph, distance.start)
children = distance.make_children_map()
for seq, revid, merge_depth, end_of_merge in distance.merge_sorted:
distance.choose_colour(revid)
if maxnum is not None:
print 'FIXME: maxnum disabled.'
revisions = distance.revisions
colours = distance.colours
parent_ids_of = distance.parent_ids_of
return (revisions, colours, children, parent_ids_of, distance.merge_sorted)
def graph(revisions, colours, merge_sorted):
"""Produce a directed graph of a bzr branch.
For each revision it then yields a tuple of (revision, node, lines).
If the revision is only referenced in the branch and not present in the
store, revision will be a DummyRevision object, otherwise it is the bzr
Revision object with the meta-data for the revision.
Node is a tuple of (column, colour) with column being a zero-indexed
column number of the graph that this revision represents and colour
being a zero-indexed colour (which doesn't specify any actual colour
in particular) to draw the node in.
Lines is a list of tuples which represent lines you should draw away
from the revision, if you also need to draw lines into the revision
you should use the lines list from the previous iteration. Each
typle in the list is in the form (start, end, colour) with start and
end being zero-indexed column numbers and colour as in node.
It's up to you how to actually draw the nodes and lines (straight,
curved, kinked, etc.) and to pick the actual colours for each index.
"""
if not len(merge_sorted):
return
# split merge_sorted into a map:
revs = {}
# FIXME: get a hint on this from the merge_sorted data rather than
# calculating it ourselves
# mapping from rev_id to the sequence number of the next lowest rev
next_lower_rev = {}
# mapping from rev_id to next-in-branch-revid - may be None for end
# of branch
next_branch_revid = {}
# the stack we are in in the sorted data for determining which
# next_lower_rev to set. It is a stack which has one list at each
# depth - the ids at that depth that need the same id allocated.
current_stack = [[]]
for seq, revid, indent, end_merge in merge_sorted:
revs[revid] = (seq, indent, end_merge)
if indent == len(current_stack):
# new merge group starts
current_stack.append([revid])
elif indent == len(current_stack) - 1:
# part of the current merge group
current_stack[-1].append(revid)
else:
# end of a merge group
while current_stack[-1]:
stack_rev_id = current_stack[-1].pop()
# record the next lower rev for this rev:
next_lower_rev[stack_rev_id] = seq
# if this followed a non-end-merge rev in this group note that
if len(current_stack[-1]):
if not revs[current_stack[-1][-1]][2]:
next_branch_revid[current_stack[-1][-1]] = stack_rev_id
current_stack.pop()
# append to the now-current merge group
current_stack[-1].append(revid)
# assign a value to all the depth 0 revisions
while current_stack[-1]:
stack_rev_id = current_stack[-1].pop()
# record the next lower rev for this rev:
next_lower_rev[stack_rev_id] = len(merge_sorted)
# if this followed a non-end-merge rev in this group note that
if len(current_stack[-1]):
if not revs[current_stack[-1][-1]][2]:
next_branch_revid[current_stack[-1][-1]] = stack_rev_id
# a list of the current revisions we are drawing lines TO indicating
# the sequence of their lines on the screen.
# i.e. [A, B, C] means that the line to A, to B, and to C are in
# (respectively), 0, 1, 2 on the screen.
hanging = [merge_sorted[0][1]]
for seq, revid, indent, end_merge in merge_sorted:
# a list of the lines to draw: their position in the
# previous row, their position in this row, and the colour
# (which is the colour they are routing to).
lines = []
new_hanging = []
for h_idx, hang in enumerate(hanging):
# one of these will be the current lines node:
# we are drawing a line. h_idx
if hang == revid:
# we have found the current lines node
node = (h_idx, colours[revid])
# note that we might have done the main parent
drawn_parents = set()
def draw_line(from_idx, to_idx, revision_id):
try:
n_idx = new_hanging.index(revision_id)
except ValueError:
# force this to be vertical at the place this rev was
# drawn.
new_hanging.insert(to_idx, revision_id)
n_idx = to_idx
lines.append((from_idx, n_idx, colours[revision_id]))
# we want to draw a line to the next commit on 'this' branch
if not end_merge:
# drop this line first.
parent_id = next_branch_revid[revid]
draw_line(h_idx, h_idx, parent_id)
# we have drawn this parent
drawn_parents.add(parent_id)
else:
# this is the last revision in a 'merge', show where it came from
if len(revisions[revid].parent_ids) > 1:
# having > 1
# parents means this commit was a merge, and being
# the end point of a merge group means that all
# the parent revisions were merged into branches
# to the left of this before this was committed
# - so we want to show this as a new branch from
# those revisions.
# to do this, we show the parent with the lowest
# sequence number, which is the one that this
# branch 'spawned from', and no others.
# If this sounds like a problem, remember that:
# if the parent was not already in our mainline
# it would show up as a merge into this making
# this not the end of a merge-line.
lowest = len(merge_sorted)
for parent_id in revisions[revid].parent_ids:
if revs[parent_id][0] < lowest:
lowest = revs[parent_id][0]
assert lowest != len(merge_sorted)
draw_line(h_idx, len(new_hanging), merge_sorted[lowest][1])
drawn_parents.add(merge_sorted[lowest][1])
elif len(revisions[revid].parent_ids) == 1:
# only one parent, must show this link to be useful.
parent_id = revisions[revid].parent_ids[0]
draw_line(h_idx, len(new_hanging), parent_id)
drawn_parents.add(parent_id)
# what do we want to draw lines to from here:
# each parent IF its relevant.
#
# Now we need to hang its parents, we put them at the point
# the old column was so anything to the right of this has
# to move outwards to make room. We also try and collapse
# hangs to keep the graph small.
# RBC: we do not draw lines to parents that were already merged
# unless its the last revision in a merge group.
for parent_id in revisions[revid].parent_ids:
if parent_id in drawn_parents:
continue
parent_seq = revs[parent_id][0]
parent_depth = revs[parent_id][1]
if parent_depth == indent + 1:
# the parent was a merge into this branch
# determine if it was already merged into the mainline
# via a different merge:
# if all revisions between us and parent_seq have a
# indent greater than there are no revisions with a lower indent than
# us.
# we do not use 'parent_depth < indent' because that would allow
# un-uniqueified merges to show up, and merge_sorted should take
# care of that for us (but does not trim the values)
if parent_seq < next_lower_rev[revid]:
draw_line(h_idx, len(new_hanging), parent_id)
elif parent_depth == indent and parent_seq == seq + 1:
# part of this branch
draw_line(h_idx, len(new_hanging), parent_id)
else:
# draw a line from the previous position of this line to the
# new position.
# h_idx is the old position.
# new_indent is the new position.
draw_line(h_idx, len(new_hanging), hang)
# we've calculated the row, assign new_hanging to hanging to setup for
# the next row
hanging = new_hanging
yield (revisions[revid], node, lines)
def same_branch(a, b):
"""Return whether we think revisions a and b are on the same branch."""
if len(a.parent_ids) == 1:
# Defacto same branch if only parent
return True
elif a.committer == b.committer:
# Same committer so may as well be
return True
else:
return False
|