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