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