##// END OF EJS Templates
graph: add outputgraph() function, called by ascii() to print...
John Stiles -
r38167:24e51760 default
parent child Browse files
Show More
@@ -1,478 +1,493 b''
1 1 # Revision graph generator for Mercurial
2 2 #
3 3 # Copyright 2008 Dirkjan Ochtman <dirkjan@ochtman.nl>
4 4 # Copyright 2007 Joel Rosdahl <joel@rosdahl.net>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 """supports walking the history as DAGs suitable for graphical output
10 10
11 11 The most basic format we use is that of::
12 12
13 13 (id, type, data, [parentids])
14 14
15 15 The node and parent ids are arbitrary integers which identify a node in the
16 16 context of the graph returned. Type is a constant specifying the node type.
17 17 Data depends on type.
18 18 """
19 19
20 20 from __future__ import absolute_import
21 21
22 22 from .node import nullrev
23 23 from . import (
24 24 dagop,
25 25 smartset,
26 26 util,
27 27 )
28 28
29 29 CHANGESET = 'C'
30 30 PARENT = 'P'
31 31 GRANDPARENT = 'G'
32 32 MISSINGPARENT = 'M'
33 33 # Style of line to draw. None signals a line that ends and is removed at this
34 34 # point. A number prefix means only the last N characters of the current block
35 35 # will use that style, the rest will use the PARENT style. Add a - sign
36 36 # (so making N negative) and all but the first N characters use that style.
37 37 EDGES = {PARENT: '|', GRANDPARENT: ':', MISSINGPARENT: None}
38 38
39 39 def dagwalker(repo, revs):
40 40 """cset DAG generator yielding (id, CHANGESET, ctx, [parentinfo]) tuples
41 41
42 42 This generator function walks through revisions (which should be ordered
43 43 from bigger to lower). It returns a tuple for each node.
44 44
45 45 Each parentinfo entry is a tuple with (edgetype, parentid), where edgetype
46 46 is one of PARENT, GRANDPARENT or MISSINGPARENT. The node and parent ids
47 47 are arbitrary integers which identify a node in the context of the graph
48 48 returned.
49 49
50 50 """
51 51 gpcache = {}
52 52
53 53 for rev in revs:
54 54 ctx = repo[rev]
55 55 # partition into parents in the rev set and missing parents, then
56 56 # augment the lists with markers, to inform graph drawing code about
57 57 # what kind of edge to draw between nodes.
58 58 pset = set(p.rev() for p in ctx.parents() if p.rev() in revs)
59 59 mpars = [p.rev() for p in ctx.parents()
60 60 if p.rev() != nullrev and p.rev() not in pset]
61 61 parents = [(PARENT, p) for p in sorted(pset)]
62 62
63 63 for mpar in mpars:
64 64 gp = gpcache.get(mpar)
65 65 if gp is None:
66 66 # precompute slow query as we know reachableroots() goes
67 67 # through all revs (issue4782)
68 68 if not isinstance(revs, smartset.baseset):
69 69 revs = smartset.baseset(revs)
70 70 gp = gpcache[mpar] = sorted(set(dagop.reachableroots(
71 71 repo, revs, [mpar])))
72 72 if not gp:
73 73 parents.append((MISSINGPARENT, mpar))
74 74 pset.add(mpar)
75 75 else:
76 76 parents.extend((GRANDPARENT, g) for g in gp if g not in pset)
77 77 pset.update(gp)
78 78
79 79 yield (ctx.rev(), CHANGESET, ctx, parents)
80 80
81 81 def nodes(repo, nodes):
82 82 """cset DAG generator yielding (id, CHANGESET, ctx, [parentids]) tuples
83 83
84 84 This generator function walks the given nodes. It only returns parents
85 85 that are in nodes, too.
86 86 """
87 87 include = set(nodes)
88 88 for node in nodes:
89 89 ctx = repo[node]
90 90 parents = set((PARENT, p.rev()) for p in ctx.parents()
91 91 if p.node() in include)
92 92 yield (ctx.rev(), CHANGESET, ctx, sorted(parents))
93 93
94 94 def colored(dag, repo):
95 95 """annotates a DAG with colored edge information
96 96
97 97 For each DAG node this function emits tuples::
98 98
99 99 (id, type, data, (col, color), [(col, nextcol, color)])
100 100
101 101 with the following new elements:
102 102
103 103 - Tuple (col, color) with column and color index for the current node
104 104 - A list of tuples indicating the edges between the current node and its
105 105 parents.
106 106 """
107 107 seen = []
108 108 colors = {}
109 109 newcolor = 1
110 110 config = {}
111 111
112 112 for key, val in repo.ui.configitems('graph'):
113 113 if '.' in key:
114 114 branch, setting = key.rsplit('.', 1)
115 115 # Validation
116 116 if setting == "width" and val.isdigit():
117 117 config.setdefault(branch, {})[setting] = int(val)
118 118 elif setting == "color" and val.isalnum():
119 119 config.setdefault(branch, {})[setting] = val
120 120
121 121 if config:
122 122 getconf = util.lrucachefunc(
123 123 lambda rev: config.get(repo[rev].branch(), {}))
124 124 else:
125 125 getconf = lambda rev: {}
126 126
127 127 for (cur, type, data, parents) in dag:
128 128
129 129 # Compute seen and next
130 130 if cur not in seen:
131 131 seen.append(cur) # new head
132 132 colors[cur] = newcolor
133 133 newcolor += 1
134 134
135 135 col = seen.index(cur)
136 136 color = colors.pop(cur)
137 137 next = seen[:]
138 138
139 139 # Add parents to next
140 140 addparents = [p for pt, p in parents if p not in next]
141 141 next[col:col + 1] = addparents
142 142
143 143 # Set colors for the parents
144 144 for i, p in enumerate(addparents):
145 145 if not i:
146 146 colors[p] = color
147 147 else:
148 148 colors[p] = newcolor
149 149 newcolor += 1
150 150
151 151 # Add edges to the graph
152 152 edges = []
153 153 for ecol, eid in enumerate(seen):
154 154 if eid in next:
155 155 bconf = getconf(eid)
156 156 edges.append((
157 157 ecol, next.index(eid), colors[eid],
158 158 bconf.get('width', -1),
159 159 bconf.get('color', '')))
160 160 elif eid == cur:
161 161 for ptype, p in parents:
162 162 bconf = getconf(p)
163 163 edges.append((
164 164 ecol, next.index(p), color,
165 165 bconf.get('width', -1),
166 166 bconf.get('color', '')))
167 167
168 168 # Yield and move on
169 169 yield (cur, type, data, (col, color), edges)
170 170 seen = next
171 171
172 172 def asciiedges(type, char, state, rev, parents):
173 173 """adds edge info to changelog DAG walk suitable for ascii()"""
174 174 seen = state['seen']
175 175 if rev not in seen:
176 176 seen.append(rev)
177 177 nodeidx = seen.index(rev)
178 178
179 179 knownparents = []
180 180 newparents = []
181 181 for ptype, parent in parents:
182 182 if parent == rev:
183 183 # self reference (should only be seen in null rev)
184 184 continue
185 185 if parent in seen:
186 186 knownparents.append(parent)
187 187 else:
188 188 newparents.append(parent)
189 189 state['edges'][parent] = state['styles'].get(ptype, '|')
190 190
191 191 ncols = len(seen)
192 192 width = 1 + ncols * 2
193 193 nextseen = seen[:]
194 194 nextseen[nodeidx:nodeidx + 1] = newparents
195 195 edges = [(nodeidx, nextseen.index(p)) for p in knownparents]
196 196
197 197 seen[:] = nextseen
198 198 while len(newparents) > 2:
199 199 # ascii() only knows how to add or remove a single column between two
200 200 # calls. Nodes with more than two parents break this constraint so we
201 201 # introduce intermediate expansion lines to grow the active node list
202 202 # slowly.
203 203 edges.append((nodeidx, nodeidx))
204 204 edges.append((nodeidx, nodeidx + 1))
205 205 nmorecols = 1
206 206 width += 2
207 207 yield (type, char, width, (nodeidx, edges, ncols, nmorecols))
208 208 char = '\\'
209 209 nodeidx += 1
210 210 ncols += 1
211 211 edges = []
212 212 del newparents[0]
213 213
214 214 if len(newparents) > 0:
215 215 edges.append((nodeidx, nodeidx))
216 216 if len(newparents) > 1:
217 217 edges.append((nodeidx, nodeidx + 1))
218 218 nmorecols = len(nextseen) - ncols
219 219 if nmorecols > 0:
220 220 width += 2
221 221 # remove current node from edge characters, no longer needed
222 222 state['edges'].pop(rev, None)
223 223 yield (type, char, width, (nodeidx, edges, ncols, nmorecols))
224 224
225 225 def _fixlongrightedges(edges):
226 226 for (i, (start, end)) in enumerate(edges):
227 227 if end > start:
228 228 edges[i] = (start, end + 1)
229 229
230 230 def _getnodelineedgestail(
231 231 echars, idx, pidx, ncols, coldiff, pdiff, fix_tail):
232 232 if fix_tail and coldiff == pdiff and coldiff != 0:
233 233 # Still going in the same non-vertical direction.
234 234 if coldiff == -1:
235 235 start = max(idx + 1, pidx)
236 236 tail = echars[idx * 2:(start - 1) * 2]
237 237 tail.extend(["/", " "] * (ncols - start))
238 238 return tail
239 239 else:
240 240 return ["\\", " "] * (ncols - idx - 1)
241 241 else:
242 242 remainder = (ncols - idx - 1)
243 243 return echars[-(remainder * 2):] if remainder > 0 else []
244 244
245 245 def _drawedges(echars, edges, nodeline, interline):
246 246 for (start, end) in edges:
247 247 if start == end + 1:
248 248 interline[2 * end + 1] = "/"
249 249 elif start == end - 1:
250 250 interline[2 * start + 1] = "\\"
251 251 elif start == end:
252 252 interline[2 * start] = echars[2 * start]
253 253 else:
254 254 if 2 * end >= len(nodeline):
255 255 continue
256 256 nodeline[2 * end] = "+"
257 257 if start > end:
258 258 (start, end) = (end, start)
259 259 for i in range(2 * start + 1, 2 * end):
260 260 if nodeline[i] != "+":
261 261 nodeline[i] = "-"
262 262
263 263 def _getpaddingline(echars, idx, ncols, edges):
264 264 # all edges up to the current node
265 265 line = echars[:idx * 2]
266 266 # an edge for the current node, if there is one
267 267 if (idx, idx - 1) in edges or (idx, idx) in edges:
268 268 # (idx, idx - 1) (idx, idx)
269 269 # | | | | | | | |
270 270 # +---o | | o---+
271 271 # | | X | | X | |
272 272 # | |/ / | |/ /
273 273 # | | | | | |
274 274 line.extend(echars[idx * 2:(idx + 1) * 2])
275 275 else:
276 276 line.extend([' ', ' '])
277 277 # all edges to the right of the current node
278 278 remainder = ncols - idx - 1
279 279 if remainder > 0:
280 280 line.extend(echars[-(remainder * 2):])
281 281 return line
282 282
283 283 def _drawendinglines(lines, extra, edgemap, seen):
284 284 """Draw ending lines for missing parent edges
285 285
286 286 None indicates an edge that ends at between this node and the next
287 287 Replace with a short line ending in ~ and add / lines to any edges to
288 288 the right.
289 289
290 290 """
291 291 if None not in edgemap.values():
292 292 return
293 293
294 294 # Check for more edges to the right of our ending edges.
295 295 # We need enough space to draw adjustment lines for these.
296 296 edgechars = extra[::2]
297 297 while edgechars and edgechars[-1] is None:
298 298 edgechars.pop()
299 299 shift_size = max((edgechars.count(None) * 2) - 1, 0)
300 300 while len(lines) < 3 + shift_size:
301 301 lines.append(extra[:])
302 302
303 303 if shift_size:
304 304 empties = []
305 305 toshift = []
306 306 first_empty = extra.index(None)
307 307 for i, c in enumerate(extra[first_empty::2], first_empty // 2):
308 308 if c is None:
309 309 empties.append(i * 2)
310 310 else:
311 311 toshift.append(i * 2)
312 312 targets = list(range(first_empty, first_empty + len(toshift) * 2, 2))
313 313 positions = toshift[:]
314 314 for line in lines[-shift_size:]:
315 315 line[first_empty:] = [' '] * (len(line) - first_empty)
316 316 for i in range(len(positions)):
317 317 pos = positions[i] - 1
318 318 positions[i] = max(pos, targets[i])
319 319 line[pos] = '/' if pos > targets[i] else extra[toshift[i]]
320 320
321 321 map = {1: '|', 2: '~'}
322 322 for i, line in enumerate(lines):
323 323 if None not in line:
324 324 continue
325 325 line[:] = [c or map.get(i, ' ') for c in line]
326 326
327 327 # remove edges that ended
328 328 remove = [p for p, c in edgemap.items() if c is None]
329 329 for parent in remove:
330 330 del edgemap[parent]
331 331 seen.remove(parent)
332 332
333 333 def asciistate():
334 334 """returns the initial value for the "state" argument to ascii()"""
335 335 return {
336 336 'seen': [],
337 337 'edges': {},
338 338 'lastcoldiff': 0,
339 339 'lastindex': 0,
340 340 'styles': EDGES.copy(),
341 341 'graphshorten': False,
342 342 }
343 343
344 def outputgraph(ui, graph):
345 """outputs an ASCII graph of a DAG
346
347 this is a helper function for 'ascii' below.
348
349 takes the following arguments:
350
351 - ui to write to
352 - graph data: list of { graph nodes/edges, text }
353
354 this function can be monkey-patched by extensions to alter graph display
355 without needing to mimic all of the edge-fixup logic in ascii()
356 """
357 for (ln, logstr) in graph:
358 ui.write((ln + logstr).rstrip() + "\n")
359
344 360 def ascii(ui, state, type, char, text, coldata):
345 361 """prints an ASCII graph of the DAG
346 362
347 363 takes the following arguments (one call per node in the graph):
348 364
349 365 - ui to write to
350 366 - Somewhere to keep the needed state in (init to asciistate())
351 367 - Column of the current node in the set of ongoing edges.
352 368 - Type indicator of node data, usually 'C' for changesets.
353 369 - Payload: (char, lines):
354 370 - Character to use as node's symbol.
355 371 - List of lines to display as the node's text.
356 372 - Edges; a list of (col, next_col) indicating the edges between
357 373 the current node and its parents.
358 374 - Number of columns (ongoing edges) in the current revision.
359 375 - The difference between the number of columns (ongoing edges)
360 376 in the next revision and the number of columns (ongoing edges)
361 377 in the current revision. That is: -1 means one column removed;
362 378 0 means no columns added or removed; 1 means one column added.
363 379 """
364 380 idx, edges, ncols, coldiff = coldata
365 381 assert -2 < coldiff < 2
366 382
367 383 edgemap, seen = state['edges'], state['seen']
368 384 # Be tolerant of history issues; make sure we have at least ncols + coldiff
369 385 # elements to work with. See test-glog.t for broken history test cases.
370 386 echars = [c for p in seen for c in (edgemap.get(p, '|'), ' ')]
371 387 echars.extend(('|', ' ') * max(ncols + coldiff - len(seen), 0))
372 388
373 389 if coldiff == -1:
374 390 # Transform
375 391 #
376 392 # | | | | | |
377 393 # o | | into o---+
378 394 # |X / |/ /
379 395 # | | | |
380 396 _fixlongrightedges(edges)
381 397
382 398 # add_padding_line says whether to rewrite
383 399 #
384 400 # | | | | | | | |
385 401 # | o---+ into | o---+
386 402 # | / / | | | # <--- padding line
387 403 # o | | | / /
388 404 # o | |
389 405 add_padding_line = (len(text) > 2 and coldiff == -1 and
390 406 [x for (x, y) in edges if x + 1 < y])
391 407
392 408 # fix_nodeline_tail says whether to rewrite
393 409 #
394 410 # | | o | | | | o | |
395 411 # | | |/ / | | |/ /
396 412 # | o | | into | o / / # <--- fixed nodeline tail
397 413 # | |/ / | |/ /
398 414 # o | | o | |
399 415 fix_nodeline_tail = len(text) <= 2 and not add_padding_line
400 416
401 417 # nodeline is the line containing the node character (typically o)
402 418 nodeline = echars[:idx * 2]
403 419 nodeline.extend([char, " "])
404 420
405 421 nodeline.extend(
406 422 _getnodelineedgestail(
407 423 echars, idx, state['lastindex'], ncols, coldiff,
408 424 state['lastcoldiff'], fix_nodeline_tail))
409 425
410 426 # shift_interline is the line containing the non-vertical
411 427 # edges between this entry and the next
412 428 shift_interline = echars[:idx * 2]
413 429 for i in xrange(2 + coldiff):
414 430 shift_interline.append(' ')
415 431 count = ncols - idx - 1
416 432 if coldiff == -1:
417 433 for i in xrange(count):
418 434 shift_interline.extend(['/', ' '])
419 435 elif coldiff == 0:
420 436 shift_interline.extend(echars[(idx + 1) * 2:ncols * 2])
421 437 else:
422 438 for i in xrange(count):
423 439 shift_interline.extend(['\\', ' '])
424 440
425 441 # draw edges from the current node to its parents
426 442 _drawedges(echars, edges, nodeline, shift_interline)
427 443
428 444 # lines is the list of all graph lines to print
429 445 lines = [nodeline]
430 446 if add_padding_line:
431 447 lines.append(_getpaddingline(echars, idx, ncols, edges))
432 448
433 449 # If 'graphshorten' config, only draw shift_interline
434 450 # when there is any non vertical flow in graph.
435 451 if state['graphshorten']:
436 452 if any(c in '\/' for c in shift_interline if c):
437 453 lines.append(shift_interline)
438 454 # Else, no 'graphshorten' config so draw shift_interline.
439 455 else:
440 456 lines.append(shift_interline)
441 457
442 458 # make sure that there are as many graph lines as there are
443 459 # log strings
444 460 extra_interline = echars[:(ncols + coldiff) * 2]
445 461 if len(lines) < len(text):
446 462 while len(lines) < len(text):
447 463 lines.append(extra_interline[:])
448 464
449 465 _drawendinglines(lines, extra_interline, edgemap, seen)
450 466
451 467 while len(text) < len(lines):
452 468 text.append("")
453 469
454 470 if any(len(char) > 1 for char in edgemap.values()):
455 471 # limit drawing an edge to the first or last N lines of the current
456 472 # section the rest of the edge is drawn like a parent line.
457 473 parent = state['styles'][PARENT][-1:]
458 474 def _drawgp(char, i):
459 475 # should a grandparent character be drawn for this line?
460 476 if len(char) < 2:
461 477 return True
462 478 num = int(char[:-1])
463 479 # either skip first num lines or take last num lines, based on sign
464 480 return -num <= i if num < 0 else (len(lines) - i) <= num
465 481 for i, line in enumerate(lines):
466 482 line[:] = [c[-1:] if _drawgp(c, i) else parent for c in line]
467 483 edgemap.update(
468 484 (e, (c if len(c) < 2 else parent)) for e, c in edgemap.items())
469 485
470 486 # print lines
471 487 indentation_level = max(ncols, ncols + coldiff)
472 for (line, logstr) in zip(lines, text):
473 ln = "%-*s %s" % (2 * indentation_level, "".join(line), logstr)
474 ui.write(ln.rstrip() + '\n')
488 lines = ["%-*s " % (2 * indentation_level, "".join(line)) for line in lines]
489 outputgraph(ui, zip(lines, text))
475 490
476 491 # ... and start over
477 492 state['lastcoldiff'] = coldiff
478 493 state['lastindex'] = idx
General Comments 0
You need to be logged in to leave comments. Login now