Kiln » TortoiseHg » TortoiseHg
Clone URL:  
revgraph.py
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
"""Directed graph production. This module contains the code to produce an ordered directed graph of a Mercurial repository, such as we display in the tree view at the top of the history window. Original code was from graphlog extension. The generator functions walks through the revision history and for the selected revision emits tuples with the following elements: (curr_rev, node, lines, parents) - Current revision. - node; defined as tuple (rev_column, rev_color) - lines; a list of (col, next_col, color, type) indicating the edges between the current row and the next row - parent revisions of current revision The node tuple has the following elements: - rev_column: Column for the node "circle" - rev_color: Color used to render the circle The lines tuple has the following elements: - col: Column for the upper end of the line. - nextcol: Column for the lower end of the line. - color: Colour used for line - type: 0 for plain line, 1 for loose upper end, 2 for loose lower end loose ends are rendered as dashed lines. They indicate that the edge of the graph does not really end here. This is used to render a branch view where the focus is on branches instead of revisions. The data is used in treeview.populate with the following signature (rev, node, lines, parents) = self.grapher.next() and stored in treeview.graphdata The treeview.model is an instance of treemodel which references treeview.graphdata treemodel stores it in self.line_graph_data, and extracts it in on_get_value, where it is mapped to several columns. REVID, NODE, LINES, PARENTS, LAST_LINES LAST_LINES is a copy of LINES from the previous row treeview maps columns treemodel.NODE, treemodel.LAST_LINES, treemodel.LINES to CellRendererGraph attributes "node", "in-lines", "out-lines" which stores it in varables node, in_lines, out_lines """ __copyright__ = "Copyright 2007 Joel Rosdahl, 2008 Steve Borho" __author__ = "Joel Rosdahl <joel@rosdahl.net>, Steve Borho <steve@borho.org>" from mercurial.node import nullrev from mercurial import cmdutil, util def __get_parents(repo, rev): return [x for x in repo.changelog.parentrevs(rev) if x != nullrev] def _color_of(repo, rev, nextcolor, preferredcolor, branch_color=False): if not branch_color: if preferredcolor[0]: rv = preferredcolor[0] preferredcolor[0] = None else: rv = nextcolor[0] nextcolor[0] = nextcolor[0] + 1 return rv else: return sum([ord(c) for c in repo[rev].branch()]) type_PLAIN = 0 type_LOOSE_LOW = 1 type_LOOSE_HIGH = 2 def revision_grapher(repo, start_rev, stop_rev, branch=None, noheads=False, branch_color=False): """incremental revision grapher This grapher generates a full graph where every edge is visible. This means that repeated merges between two branches may make the graph very wide. if branch is set to the name of a branch, only this branch is shown. if noheads is True, other heads than start_rev is hidden. Use this to show ancestors of a revision. if branch_color is True, the branch colour is determined by a hash of the branch tip, and will thus always be the same. """ assert start_rev >= stop_rev curr_rev = start_rev revs = [] rev_color = {} nextcolor = [0] while curr_rev >= stop_rev: # Compute revs and next_revs. if curr_rev not in revs: if noheads and curr_rev != start_rev: curr_rev -= 1 continue if branch: ctx = repo.changectx(curr_rev) if ctx.branch() != branch: curr_rev -= 1 continue # New head. revs.append(curr_rev) rev_color[curr_rev] = _color_of(repo, curr_rev, nextcolor, [None], branch_color) r = __get_parents(repo, curr_rev) while r: r0 = r[0] if r0 < stop_rev: break if r0 in rev_color: break if not branch_color: rev_color[r0] = rev_color[curr_rev] else: rev_color[r0] = _color_of(repo, r0, None, None, True) r = __get_parents(repo, r0) rev_index = revs.index(curr_rev) next_revs = revs[:] # Add parents to next_revs. parents = __get_parents(repo, curr_rev) parents_to_add = [] preferred_color = [rev_color[curr_rev]] for parent in parents: if parent not in next_revs: parents_to_add.append(parent) if parent not in rev_color: rev_color[parent] = _color_of(repo, parent, nextcolor, preferred_color, branch_color) next_revs[rev_index:rev_index + 1] = parents_to_add lines = [] for i, rev in enumerate(revs): if rev in next_revs: color = rev_color[rev] lines.append( (i, next_revs.index(rev), color, type_PLAIN) ) elif rev == curr_rev: for parent in parents: color = rev_color[parent] lines.append( (i, next_revs.index(parent), color, type_PLAIN) ) yield (curr_rev, (rev_index, rev_color[curr_rev]), lines, parents) revs = next_revs curr_rev -= 1 class BranchGrapher: """Incremental branch grapher This generator function produces a graph that uses loose ends to keep focus on branches. All revisions in the range are shown, but not all edges. The function identifies branches and may use loose ends if an edge links two branches. """ def __init__(self, repo, start_rev, stop_rev): assert start_rev >= stop_rev self.repo = repo # # Iterator content # Variables that keep track of the iterator # # The current revision to process self.curr_rev = start_rev # Process all revs from start_rev to and including stop_rev self.stop_rev = stop_rev # List current branches. For each branch the next rev is listed # The order of branches determine their columns self.curr_branches = [start_rev] # For each branch, tell the next version that will show up self.next_in_branch = {} # # Graph variables # These hold information related to the graph. Most are computed lazily. # The graph is split into a number of "branches", defined as paths within # a named branch. # # Map rev to next rev in branch = parent with same branch name # If two parents, use first that has same branch name. # The parent of last rev in a branch is undefined, # even if the revsion has a parent rev. self.parent_of = {} # Map rev to newest rev in branch. This identifies the branch that the rev # is part of self.branch4rev = {} # Last revision in branch self.branch_tail = {} # Map branch-id (head-rev of branch) to color self.color4branch = {} # Next colour used. for branches self.nextcolor = 0 def _get_parents(self, rev): return [x for x in self.repo.changelog.parentrevs(rev) if x != nullrev] def _branch_name(self, rev): return self.repo[rev].branch() def _covered_rev(self, rev): """True if rev is inside the revision range for the iterator""" return self.stop_rev <= rev def _new_branch(self, branch_head): """Mark all unknown revisions in range that are direct ancestors of branch_head as part of the same branch. Stops when stop_rev is passed or a known revision is found""" assert not branch_head in self.branch4rev self.color4branch[branch_head] = self.nextcolor self.nextcolor += 1 self.next_in_branch[branch_head] = branch_head branch_name = self._branch_name(branch_head) rev = branch_head while not rev in self.branch4rev: # Determine if rev should be used if not self._covered_rev(rev): # rev is outside visible range, so we don't know tail location self.branch_tail[branch_head] = 0 # Prev revs wasn't tail return # Add rev to branch self.branch4rev[rev] = branch_head self.branch_tail[branch_head] = rev # Find next revision in branch self.parent_of[rev] = None for parent in self._get_parents(rev): if self._branch_name(parent) == branch_name: self.parent_of[rev] = parent break if self.parent_of[rev] is None: return rev = self.parent_of[rev] def _get_rev_branch(self, rev): """Find revision branch or create a new branch""" branch = self.branch4rev.get(rev) if branch is None: branch = rev self._new_branch(branch) assert rev in self.branch4rev assert branch in self.branch_tail return branch def _compute_next_branches(self): """Compute next row of branches""" next_branches = self.curr_branches[:] # Find branch used by current revision curr_branch = self._get_rev_branch(self.curr_rev) self.next_in_branch[curr_branch] = self.parent_of[self.curr_rev] branch_index = self.curr_branches.index(curr_branch) # Insert branches if parents point to new branches new_parents = 0 for parent in self._get_parents(self.curr_rev): branch = self._get_rev_branch(parent) if not branch in next_branches: new_parents += 1 next_branches.insert(branch_index + new_parents, branch) # Delete branch if last revision if self.curr_rev == self.branch_tail[curr_branch]: del next_branches[branch_index] # Return result return next_branches def _rev_color(self, rev): """Map a revision to a color""" return self.color4branch[self.branch4rev[rev]] def _compute_lines(self, parents, next_branches): # Compute lines (from CUR to NEXT branch row) lines = [] curr_rev_branch = self.branch4rev[self.curr_rev] for curr_column, branch in enumerate(self.curr_branches): if branch == curr_rev_branch: # Add lines from current branch to parents for par_rev in parents: par_branch = self._get_rev_branch(par_rev) color = self.color4branch[par_branch] next_column = next_branches.index(par_branch) line_type = type_PLAIN if par_rev != self.next_in_branch.get(par_branch): line_type = type_LOOSE_LOW lines.append( (curr_column, next_column, color, line_type) ) else: # Continue unrelated branch color = self.color4branch[branch] next_column = next_branches.index(branch) lines.append( (curr_column, next_column, color, type_PLAIN) ) return lines def more(self): return self.curr_rev >= self.stop_rev def next(self): """Perform one iteration of the branch grapher""" # Compute revision (on CUR branch row) rev = self.curr_rev rev_branch = self._get_rev_branch(rev) if rev_branch not in self.curr_branches: # New head self.curr_branches.append(rev_branch) # Compute parents (indicates the branches on NEXT branch row that curr_rev links to) parents = self._get_parents(rev) # BUG: __get_parents is not defined - why? # Compute lines (from CUR to NEXT branch row) next_branches = self._compute_next_branches() lines = self._compute_lines(parents, next_branches) # Compute node info (for CUR branch row) rev_column = self.curr_branches.index(rev_branch) rev_color = self.color4branch[rev_branch] node = (rev_column, rev_color) # Next loop self.curr_branches = next_branches self.curr_rev -= 1 # Return result # TODO: Refactor parents field away - it is apparently not used anywhere return (rev, node, lines, parents) def branch_grapher(repo, start_rev, stop_rev): grapher = BranchGrapher(repo, start_rev, stop_rev) while grapher.more(): yield grapher.next() def filelog_grapher(repo, path): ''' Graph the ancestry of a single file (log). Deletions show up as breaks in the graph. ''' filerev = len(repo.file(path)) - 1 revs = [] rev_color = {} nextcolor = 0 while filerev >= 0: fctx = repo.filectx(path, fileid=filerev) # Compute revs and next_revs. if filerev not in revs: revs.append(filerev) rev_color[filerev] = nextcolor ; nextcolor += 1 curcolor = rev_color[filerev] index = revs.index(filerev) next_revs = revs[:] # Add parents to next_revs. parents = [f.filerev() for f in fctx.parents() if f.path() == path] parents_to_add = [] for parent in parents: if parent not in next_revs: parents_to_add.append(parent) if len(parents) > 1: rev_color[parent] = nextcolor ; nextcolor += 1 else: rev_color[parent] = curcolor parents_to_add.sort() next_revs[index:index + 1] = parents_to_add lines = [] for i, rev in enumerate(revs): if rev in next_revs: color = rev_color[rev] lines.append( (i, next_revs.index(rev), color, type_PLAIN) ) elif rev == filerev: for parent in parents: color = rev_color[parent] lines.append( (i, next_revs.index(parent), color, type_PLAIN) ) pcrevs = [pfc.rev() for pfc in fctx.parents()] yield (fctx.rev(), (index, curcolor), lines, pcrevs) revs = next_revs filerev -= 1 def dumb_log_generator(repo, revs): for revname in revs: node = repo.lookup(revname) rev = repo.changelog.rev(node) yield (rev, (0,0), [], __get_parents(repo, rev)) def filtered_log_generator(repo, pats, opts): '''Fill view model iteratively repo - Mercurial repository object pats - list of file names or patterns opts - command line options for log command ''' # Log searches: pattern, keyword, date, etc df = False if opts['date']: df = util.matchdate(opts['date']) stack = [] get = util.cachefunc(lambda r: repo.changectx(r).changeset()) if pats != None: pats = ['path:'+p for p in pats] changeiter, matchfn = cmdutil.walkchangerevs(repo.ui, repo, pats, get, opts) for st, rev, fns in changeiter: if st == 'iter': if stack: yield stack.pop() continue if st != 'add': continue parents = __get_parents(repo, rev) if opts['no_merges'] and len(parents) == 2: continue if opts['only_merges'] and len(parents) != 2: continue if df: changes = get(rev) if not df(changes[2][0]): continue # TODO: add copies/renames later if opts['keyword']: changes = get(rev) miss = 0 for k in [kw.lower() for kw in opts['keyword']]: if not (k in changes[1].lower() or k in changes[4].lower() or k in " ".join(changes[3]).lower()): miss = 1 break if miss: continue stack.append((rev, (0,0), [], parents))