25 pycompat, |
25 pycompat, |
26 smartset, |
26 smartset, |
27 util, |
27 util, |
28 ) |
28 ) |
29 |
29 |
30 CHANGESET = 'C' |
30 CHANGESET = b'C' |
31 PARENT = 'P' |
31 PARENT = b'P' |
32 GRANDPARENT = 'G' |
32 GRANDPARENT = b'G' |
33 MISSINGPARENT = 'M' |
33 MISSINGPARENT = b'M' |
34 # Style of line to draw. None signals a line that ends and is removed at this |
34 # Style of line to draw. None signals a line that ends and is removed at this |
35 # point. A number prefix means only the last N characters of the current block |
35 # point. A number prefix means only the last N characters of the current block |
36 # will use that style, the rest will use the PARENT style. Add a - sign |
36 # will use that style, the rest will use the PARENT style. Add a - sign |
37 # (so making N negative) and all but the first N characters use that style. |
37 # (so making N negative) and all but the first N characters use that style. |
38 EDGES = {PARENT: '|', GRANDPARENT: ':', MISSINGPARENT: None} |
38 EDGES = {PARENT: b'|', GRANDPARENT: b':', MISSINGPARENT: None} |
39 |
39 |
40 |
40 |
41 def dagwalker(repo, revs): |
41 def dagwalker(repo, revs): |
42 """cset DAG generator yielding (id, CHANGESET, ctx, [parentinfo]) tuples |
42 """cset DAG generator yielding (id, CHANGESET, ctx, [parentinfo]) tuples |
43 |
43 |
116 seen = [] |
116 seen = [] |
117 colors = {} |
117 colors = {} |
118 newcolor = 1 |
118 newcolor = 1 |
119 config = {} |
119 config = {} |
120 |
120 |
121 for key, val in repo.ui.configitems('graph'): |
121 for key, val in repo.ui.configitems(b'graph'): |
122 if '.' in key: |
122 if b'.' in key: |
123 branch, setting = key.rsplit('.', 1) |
123 branch, setting = key.rsplit(b'.', 1) |
124 # Validation |
124 # Validation |
125 if setting == "width" and val.isdigit(): |
125 if setting == b"width" and val.isdigit(): |
126 config.setdefault(branch, {})[setting] = int(val) |
126 config.setdefault(branch, {})[setting] = int(val) |
127 elif setting == "color" and val.isalnum(): |
127 elif setting == b"color" and val.isalnum(): |
128 config.setdefault(branch, {})[setting] = val |
128 config.setdefault(branch, {})[setting] = val |
129 |
129 |
130 if config: |
130 if config: |
131 getconf = util.lrucachefunc( |
131 getconf = util.lrucachefunc( |
132 lambda rev: config.get(repo[rev].branch(), {}) |
132 lambda rev: config.get(repo[rev].branch(), {}) |
166 edges.append( |
166 edges.append( |
167 ( |
167 ( |
168 ecol, |
168 ecol, |
169 next.index(eid), |
169 next.index(eid), |
170 colors[eid], |
170 colors[eid], |
171 bconf.get('width', -1), |
171 bconf.get(b'width', -1), |
172 bconf.get('color', ''), |
172 bconf.get(b'color', b''), |
173 ) |
173 ) |
174 ) |
174 ) |
175 elif eid == cur: |
175 elif eid == cur: |
176 for ptype, p in parents: |
176 for ptype, p in parents: |
177 bconf = getconf(p) |
177 bconf = getconf(p) |
178 edges.append( |
178 edges.append( |
179 ( |
179 ( |
180 ecol, |
180 ecol, |
181 next.index(p), |
181 next.index(p), |
182 color, |
182 color, |
183 bconf.get('width', -1), |
183 bconf.get(b'width', -1), |
184 bconf.get('color', ''), |
184 bconf.get(b'color', b''), |
185 ) |
185 ) |
186 ) |
186 ) |
187 |
187 |
188 # Yield and move on |
188 # Yield and move on |
189 yield (cur, type, data, (col, color), edges) |
189 yield (cur, type, data, (col, color), edges) |
190 seen = next |
190 seen = next |
191 |
191 |
192 |
192 |
193 def asciiedges(type, char, state, rev, parents): |
193 def asciiedges(type, char, state, rev, parents): |
194 """adds edge info to changelog DAG walk suitable for ascii()""" |
194 """adds edge info to changelog DAG walk suitable for ascii()""" |
195 seen = state['seen'] |
195 seen = state[b'seen'] |
196 if rev not in seen: |
196 if rev not in seen: |
197 seen.append(rev) |
197 seen.append(rev) |
198 nodeidx = seen.index(rev) |
198 nodeidx = seen.index(rev) |
199 |
199 |
200 knownparents = [] |
200 knownparents = [] |
224 edges.append((nodeidx, nodeidx)) |
224 edges.append((nodeidx, nodeidx)) |
225 edges.append((nodeidx, nodeidx + 1)) |
225 edges.append((nodeidx, nodeidx + 1)) |
226 nmorecols = 1 |
226 nmorecols = 1 |
227 width += 2 |
227 width += 2 |
228 yield (type, char, width, (nodeidx, edges, ncols, nmorecols)) |
228 yield (type, char, width, (nodeidx, edges, ncols, nmorecols)) |
229 char = '\\' |
229 char = b'\\' |
230 nodeidx += 1 |
230 nodeidx += 1 |
231 ncols += 1 |
231 ncols += 1 |
232 edges = [] |
232 edges = [] |
233 del newparents[0] |
233 del newparents[0] |
234 |
234 |
238 edges.append((nodeidx, nodeidx + 1)) |
238 edges.append((nodeidx, nodeidx + 1)) |
239 nmorecols = len(nextseen) - ncols |
239 nmorecols = len(nextseen) - ncols |
240 if nmorecols > 0: |
240 if nmorecols > 0: |
241 width += 2 |
241 width += 2 |
242 # remove current node from edge characters, no longer needed |
242 # remove current node from edge characters, no longer needed |
243 state['edges'].pop(rev, None) |
243 state[b'edges'].pop(rev, None) |
244 yield (type, char, width, (nodeidx, edges, ncols, nmorecols)) |
244 yield (type, char, width, (nodeidx, edges, ncols, nmorecols)) |
245 |
245 |
246 |
246 |
247 def _fixlongrightedges(edges): |
247 def _fixlongrightedges(edges): |
248 for (i, (start, end)) in enumerate(edges): |
248 for (i, (start, end)) in enumerate(edges): |
254 if fix_tail and coldiff == pdiff and coldiff != 0: |
254 if fix_tail and coldiff == pdiff and coldiff != 0: |
255 # Still going in the same non-vertical direction. |
255 # Still going in the same non-vertical direction. |
256 if coldiff == -1: |
256 if coldiff == -1: |
257 start = max(idx + 1, pidx) |
257 start = max(idx + 1, pidx) |
258 tail = echars[idx * 2 : (start - 1) * 2] |
258 tail = echars[idx * 2 : (start - 1) * 2] |
259 tail.extend(["/", " "] * (ncols - start)) |
259 tail.extend([b"/", b" "] * (ncols - start)) |
260 return tail |
260 return tail |
261 else: |
261 else: |
262 return ["\\", " "] * (ncols - idx - 1) |
262 return [b"\\", b" "] * (ncols - idx - 1) |
263 else: |
263 else: |
264 remainder = ncols - idx - 1 |
264 remainder = ncols - idx - 1 |
265 return echars[-(remainder * 2) :] if remainder > 0 else [] |
265 return echars[-(remainder * 2) :] if remainder > 0 else [] |
266 |
266 |
267 |
267 |
268 def _drawedges(echars, edges, nodeline, interline): |
268 def _drawedges(echars, edges, nodeline, interline): |
269 for (start, end) in edges: |
269 for (start, end) in edges: |
270 if start == end + 1: |
270 if start == end + 1: |
271 interline[2 * end + 1] = "/" |
271 interline[2 * end + 1] = b"/" |
272 elif start == end - 1: |
272 elif start == end - 1: |
273 interline[2 * start + 1] = "\\" |
273 interline[2 * start + 1] = b"\\" |
274 elif start == end: |
274 elif start == end: |
275 interline[2 * start] = echars[2 * start] |
275 interline[2 * start] = echars[2 * start] |
276 else: |
276 else: |
277 if 2 * end >= len(nodeline): |
277 if 2 * end >= len(nodeline): |
278 continue |
278 continue |
279 nodeline[2 * end] = "+" |
279 nodeline[2 * end] = b"+" |
280 if start > end: |
280 if start > end: |
281 (start, end) = (end, start) |
281 (start, end) = (end, start) |
282 for i in range(2 * start + 1, 2 * end): |
282 for i in range(2 * start + 1, 2 * end): |
283 if nodeline[i] != "+": |
283 if nodeline[i] != b"+": |
284 nodeline[i] = "-" |
284 nodeline[i] = b"-" |
285 |
285 |
286 |
286 |
287 def _getpaddingline(echars, idx, ncols, edges): |
287 def _getpaddingline(echars, idx, ncols, edges): |
288 # all edges up to the current node |
288 # all edges up to the current node |
289 line = echars[: idx * 2] |
289 line = echars[: idx * 2] |
320 # We need enough space to draw adjustment lines for these. |
320 # We need enough space to draw adjustment lines for these. |
321 edgechars = extra[::2] |
321 edgechars = extra[::2] |
322 while edgechars and edgechars[-1] is None: |
322 while edgechars and edgechars[-1] is None: |
323 edgechars.pop() |
323 edgechars.pop() |
324 shift_size = max((edgechars.count(None) * 2) - 1, 0) |
324 shift_size = max((edgechars.count(None) * 2) - 1, 0) |
325 minlines = 3 if not state['graphshorten'] else 2 |
325 minlines = 3 if not state[b'graphshorten'] else 2 |
326 while len(lines) < minlines + shift_size: |
326 while len(lines) < minlines + shift_size: |
327 lines.append(extra[:]) |
327 lines.append(extra[:]) |
328 |
328 |
329 if shift_size: |
329 if shift_size: |
330 empties = [] |
330 empties = [] |
336 else: |
336 else: |
337 toshift.append(i * 2) |
337 toshift.append(i * 2) |
338 targets = list(range(first_empty, first_empty + len(toshift) * 2, 2)) |
338 targets = list(range(first_empty, first_empty + len(toshift) * 2, 2)) |
339 positions = toshift[:] |
339 positions = toshift[:] |
340 for line in lines[-shift_size:]: |
340 for line in lines[-shift_size:]: |
341 line[first_empty:] = [' '] * (len(line) - first_empty) |
341 line[first_empty:] = [b' '] * (len(line) - first_empty) |
342 for i in range(len(positions)): |
342 for i in range(len(positions)): |
343 pos = positions[i] - 1 |
343 pos = positions[i] - 1 |
344 positions[i] = max(pos, targets[i]) |
344 positions[i] = max(pos, targets[i]) |
345 line[pos] = '/' if pos > targets[i] else extra[toshift[i]] |
345 line[pos] = b'/' if pos > targets[i] else extra[toshift[i]] |
346 |
346 |
347 map = {1: '|', 2: '~'} if not state['graphshorten'] else {1: '~'} |
347 map = {1: b'|', 2: b'~'} if not state[b'graphshorten'] else {1: b'~'} |
348 for i, line in enumerate(lines): |
348 for i, line in enumerate(lines): |
349 if None not in line: |
349 if None not in line: |
350 continue |
350 continue |
351 line[:] = [c or map.get(i, ' ') for c in line] |
351 line[:] = [c or map.get(i, b' ') for c in line] |
352 |
352 |
353 # remove edges that ended |
353 # remove edges that ended |
354 remove = [p for p, c in edgemap.items() if c is None] |
354 remove = [p for p, c in edgemap.items() if c is None] |
355 for parent in remove: |
355 for parent in remove: |
356 del edgemap[parent] |
356 del edgemap[parent] |
358 |
358 |
359 |
359 |
360 def asciistate(): |
360 def asciistate(): |
361 """returns the initial value for the "state" argument to ascii()""" |
361 """returns the initial value for the "state" argument to ascii()""" |
362 return { |
362 return { |
363 'seen': [], |
363 b'seen': [], |
364 'edges': {}, |
364 b'edges': {}, |
365 'lastcoldiff': 0, |
365 b'lastcoldiff': 0, |
366 'lastindex': 0, |
366 b'lastindex': 0, |
367 'styles': EDGES.copy(), |
367 b'styles': EDGES.copy(), |
368 'graphshorten': False, |
368 b'graphshorten': False, |
369 } |
369 } |
370 |
370 |
371 |
371 |
372 def outputgraph(ui, graph): |
372 def outputgraph(ui, graph): |
373 """outputs an ASCII graph of a DAG |
373 """outputs an ASCII graph of a DAG |
381 |
381 |
382 this function can be monkey-patched by extensions to alter graph display |
382 this function can be monkey-patched by extensions to alter graph display |
383 without needing to mimic all of the edge-fixup logic in ascii() |
383 without needing to mimic all of the edge-fixup logic in ascii() |
384 """ |
384 """ |
385 for (ln, logstr) in graph: |
385 for (ln, logstr) in graph: |
386 ui.write((ln + logstr).rstrip() + "\n") |
386 ui.write((ln + logstr).rstrip() + b"\n") |
387 |
387 |
388 |
388 |
389 def ascii(ui, state, type, char, text, coldata): |
389 def ascii(ui, state, type, char, text, coldata): |
390 """prints an ASCII graph of the DAG |
390 """prints an ASCII graph of the DAG |
391 |
391 |
407 0 means no columns added or removed; 1 means one column added. |
407 0 means no columns added or removed; 1 means one column added. |
408 """ |
408 """ |
409 idx, edges, ncols, coldiff = coldata |
409 idx, edges, ncols, coldiff = coldata |
410 assert -2 < coldiff < 2 |
410 assert -2 < coldiff < 2 |
411 |
411 |
412 edgemap, seen = state['edges'], state['seen'] |
412 edgemap, seen = state[b'edges'], state[b'seen'] |
413 # Be tolerant of history issues; make sure we have at least ncols + coldiff |
413 # Be tolerant of history issues; make sure we have at least ncols + coldiff |
414 # elements to work with. See test-glog.t for broken history test cases. |
414 # elements to work with. See test-glog.t for broken history test cases. |
415 echars = [c for p in seen for c in (edgemap.get(p, '|'), ' ')] |
415 echars = [c for p in seen for c in (edgemap.get(p, b'|'), b' ')] |
416 echars.extend(('|', ' ') * max(ncols + coldiff - len(seen), 0)) |
416 echars.extend((b'|', b' ') * max(ncols + coldiff - len(seen), 0)) |
417 |
417 |
418 if coldiff == -1: |
418 if coldiff == -1: |
419 # Transform |
419 # Transform |
420 # |
420 # |
421 # | | | | | | |
421 # | | | | | | |
444 # o | | o | | |
444 # o | | o | | |
445 fix_nodeline_tail = len(text) <= 2 and not add_padding_line |
445 fix_nodeline_tail = len(text) <= 2 and not add_padding_line |
446 |
446 |
447 # nodeline is the line containing the node character (typically o) |
447 # nodeline is the line containing the node character (typically o) |
448 nodeline = echars[: idx * 2] |
448 nodeline = echars[: idx * 2] |
449 nodeline.extend([char, " "]) |
449 nodeline.extend([char, b" "]) |
450 |
450 |
451 nodeline.extend( |
451 nodeline.extend( |
452 _getnodelineedgestail( |
452 _getnodelineedgestail( |
453 echars, |
453 echars, |
454 idx, |
454 idx, |
455 state['lastindex'], |
455 state[b'lastindex'], |
456 ncols, |
456 ncols, |
457 coldiff, |
457 coldiff, |
458 state['lastcoldiff'], |
458 state[b'lastcoldiff'], |
459 fix_nodeline_tail, |
459 fix_nodeline_tail, |
460 ) |
460 ) |
461 ) |
461 ) |
462 |
462 |
463 # shift_interline is the line containing the non-vertical |
463 # shift_interline is the line containing the non-vertical |
464 # edges between this entry and the next |
464 # edges between this entry and the next |
465 shift_interline = echars[: idx * 2] |
465 shift_interline = echars[: idx * 2] |
466 for i in pycompat.xrange(2 + coldiff): |
466 for i in pycompat.xrange(2 + coldiff): |
467 shift_interline.append(' ') |
467 shift_interline.append(b' ') |
468 count = ncols - idx - 1 |
468 count = ncols - idx - 1 |
469 if coldiff == -1: |
469 if coldiff == -1: |
470 for i in pycompat.xrange(count): |
470 for i in pycompat.xrange(count): |
471 shift_interline.extend(['/', ' ']) |
471 shift_interline.extend([b'/', b' ']) |
472 elif coldiff == 0: |
472 elif coldiff == 0: |
473 shift_interline.extend(echars[(idx + 1) * 2 : ncols * 2]) |
473 shift_interline.extend(echars[(idx + 1) * 2 : ncols * 2]) |
474 else: |
474 else: |
475 for i in pycompat.xrange(count): |
475 for i in pycompat.xrange(count): |
476 shift_interline.extend(['\\', ' ']) |
476 shift_interline.extend([b'\\', b' ']) |
477 |
477 |
478 # draw edges from the current node to its parents |
478 # draw edges from the current node to its parents |
479 _drawedges(echars, edges, nodeline, shift_interline) |
479 _drawedges(echars, edges, nodeline, shift_interline) |
480 |
480 |
481 # lines is the list of all graph lines to print |
481 # lines is the list of all graph lines to print |
483 if add_padding_line: |
483 if add_padding_line: |
484 lines.append(_getpaddingline(echars, idx, ncols, edges)) |
484 lines.append(_getpaddingline(echars, idx, ncols, edges)) |
485 |
485 |
486 # If 'graphshorten' config, only draw shift_interline |
486 # If 'graphshorten' config, only draw shift_interline |
487 # when there is any non vertical flow in graph. |
487 # when there is any non vertical flow in graph. |
488 if state['graphshorten']: |
488 if state[b'graphshorten']: |
489 if any(c in br'\/' for c in shift_interline if c): |
489 if any(c in br'\/' for c in shift_interline if c): |
490 lines.append(shift_interline) |
490 lines.append(shift_interline) |
491 # Else, no 'graphshorten' config so draw shift_interline. |
491 # Else, no 'graphshorten' config so draw shift_interline. |
492 else: |
492 else: |
493 lines.append(shift_interline) |
493 lines.append(shift_interline) |
500 lines.append(extra_interline[:]) |
500 lines.append(extra_interline[:]) |
501 |
501 |
502 _drawendinglines(lines, extra_interline, edgemap, seen, state) |
502 _drawendinglines(lines, extra_interline, edgemap, seen, state) |
503 |
503 |
504 while len(text) < len(lines): |
504 while len(text) < len(lines): |
505 text.append("") |
505 text.append(b"") |
506 |
506 |
507 # print lines |
507 # print lines |
508 indentation_level = max(ncols, ncols + coldiff) |
508 indentation_level = max(ncols, ncols + coldiff) |
509 lines = ["%-*s " % (2 * indentation_level, "".join(line)) for line in lines] |
509 lines = [ |
|
510 b"%-*s " % (2 * indentation_level, b"".join(line)) for line in lines |
|
511 ] |
510 outputgraph(ui, zip(lines, text)) |
512 outputgraph(ui, zip(lines, text)) |
511 |
513 |
512 # ... and start over |
514 # ... and start over |
513 state['lastcoldiff'] = coldiff |
515 state[b'lastcoldiff'] = coldiff |
514 state['lastindex'] = idx |
516 state[b'lastindex'] = idx |