##// END OF EJS Templates
graphlog: refactor common grapher code...
Peter Arrenbrecht -
r7370:7bc62ebe default
parent child Browse files
Show More
@@ -1,346 +1,305 b''
1 # ASCII graph log extension for Mercurial
1 # ASCII graph log extension for Mercurial
2 #
2 #
3 # Copyright 2007 Joel Rosdahl <joel@rosdahl.net>
3 # Copyright 2007 Joel Rosdahl <joel@rosdahl.net>
4 #
4 #
5 # This software may be used and distributed according to the terms of
5 # This software may be used and distributed according to the terms of
6 # the GNU General Public License, incorporated herein by reference.
6 # the GNU General Public License, incorporated herein by reference.
7 '''show revision graphs in terminal windows'''
7 '''show revision graphs in terminal windows'''
8
8
9 import os
9 import os
10 import sys
10 import sys
11 from mercurial.cmdutil import revrange, show_changeset
11 from mercurial.cmdutil import revrange, show_changeset
12 from mercurial.commands import templateopts
12 from mercurial.commands import templateopts
13 from mercurial.i18n import _
13 from mercurial.i18n import _
14 from mercurial.node import nullrev
14 from mercurial.node import nullrev
15 from mercurial.util import Abort, canonpath
15 from mercurial.util import Abort, canonpath
16 from mercurial import util
17
16
18 def get_rev_parents(repo, rev):
17 def revisions(repo, start, stop):
19 return [x for x in repo.changelog.parentrevs(rev) if x != nullrev]
18 """cset DAG generator yielding (rev, node, [parents]) tuples
20
21 def revision_grapher(repo, start_rev, stop_rev):
22 """incremental revision grapher
23
24 This generator function walks through the revision history from
25 revision start_rev to revision stop_rev (which must be less than
26 or equal to start_rev) and for each revision emits tuples with the
27 following elements:
28
29 - Current revision.
30 - Current node.
31 - Column of the current node in the set of ongoing edges.
32 - Edges; a list of (col, next_col) indicating the edges between
33 the current node and its parents.
34 - Number of columns (ongoing edges) in the current revision.
35 - The difference between the number of columns (ongoing edges)
36 in the next revision and the number of columns (ongoing edges)
37 in the current revision. That is: -1 means one column removed;
38 0 means no columns added or removed; 1 means one column added.
39 """
40
41 assert start_rev >= stop_rev
42 curr_rev = start_rev
43 revs = []
44 while curr_rev >= stop_rev:
45 node = repo.changelog.node(curr_rev)
46
47 # Compute revs and next_revs.
48 if curr_rev not in revs:
49 # New head.
50 revs.append(curr_rev)
51 rev_index = revs.index(curr_rev)
52 next_revs = revs[:]
53
19
54 # Add parents to next_revs.
20 This generator function walks through the revision history from revision
55 parents = get_rev_parents(repo, curr_rev)
21 start to revision stop (which must be less than or equal to start).
56 parents_to_add = []
22 """
57 for parent in parents:
23 assert start >= stop
58 if parent not in next_revs:
24 cur = start
59 parents_to_add.append(parent)
25 while cur >= stop:
60 next_revs[rev_index:rev_index + 1] = util.sort(parents_to_add)
26 ctx = repo[cur]
61
27 parents = sorted(p.rev() for p in ctx.parents() if p.rev() != nullrev)
62 edges = []
28 yield (ctx, parents)
63 for parent in parents:
29 cur -= 1
64 edges.append((rev_index, next_revs.index(parent)))
65
66 n_columns_diff = len(next_revs) - len(revs)
67 yield (curr_rev, node, rev_index, edges, len(revs), n_columns_diff)
68
69 revs = next_revs
70 curr_rev -= 1
71
30
72 def filelog_grapher(repo, path, start_rev, stop_rev):
31 def filerevs(repo, path, start, stop):
73 """incremental file log grapher
32 """file cset DAG generator yielding (rev, node, [parents]) tuples
74
75 This generator function walks through the revision history of a
76 single file from revision start_rev to revision stop_rev (which must
77 be less than or equal to start_rev) and for each revision emits
78 tuples with the following elements:
79
33
80 - Current revision.
34 This generator function walks through the revision history of a single
81 - Current node.
35 file from revision start to revision stop (which must be less than or
82 - Column of the current node in the set of ongoing edges.
36 equal to start).
83 - Edges; a list of (col, next_col) indicating the edges between
84 the current node and its parents.
85 - Number of columns (ongoing edges) in the current revision.
86 - The difference between the number of columns (ongoing edges)
87 in the next revision and the number of columns (ongoing edges)
88 in the current revision. That is: -1 means one column removed;
89 0 means no columns added or removed; 1 means one column added.
90 """
37 """
91
38 assert start >= stop
92 assert start_rev >= stop_rev
93 revs = []
94 filerev = len(repo.file(path)) - 1
39 filerev = len(repo.file(path)) - 1
95 while filerev >= 0:
40 while filerev >= 0:
96 fctx = repo.filectx(path, fileid=filerev)
41 fctx = repo.filectx(path, fileid=filerev)
42 parents = [f.filerev() for f in fctx.parents() if f.path() == path]
43 parents.sort()
44 if fctx.rev() <= start:
45 yield (fctx, parents)
46 if fctx.rev() <= stop:
47 break
48 filerev -= 1
97
49
98 # Compute revs and next_revs.
50 def grapher(nodes):
99 if filerev not in revs:
51 """grapher for asciigraph on a list of nodes and their parents
100 revs.append(filerev)
52
101 rev_index = revs.index(filerev)
53 nodes must generate tuples (node, parents, char, lines) where
102 next_revs = revs[:]
103
54
104 # Add parents to next_revs.
55 - parents must generate the parents of node, in sorted order,
105 parents = [f.filerev() for f in fctx.parents() if f.path() == path]
56 and max length 2,
106 parents_to_add = []
57 - char is the char to print as the node symbol, and
58 - lines are the lines to display next to the node.
59 """
60 seen = []
61 for node, parents, char, lines in nodes:
62 if node not in seen:
63 seen.append(node)
64 nodeidx = seen.index(node)
65
66 knownparents = []
67 newparents = []
107 for parent in parents:
68 for parent in parents:
108 if parent not in next_revs:
69 if parent in seen:
109 parents_to_add.append(parent)
70 knownparents.append(parent)
110 next_revs[rev_index:rev_index + 1] = util.sort(parents_to_add)
71 else:
111
72 newparents.append(parent)
112 edges = []
113 for parent in parents:
114 edges.append((rev_index, next_revs.index(parent)))
115
73
116 changerev = fctx.linkrev()
74 ncols = len(seen)
117 if changerev <= start_rev:
75 nextseen = seen[:]
118 node = repo.changelog.node(changerev)
76 nextseen[nodeidx:nodeidx + 1] = newparents
119 n_columns_diff = len(next_revs) - len(revs)
77 edges = [(nodeidx, nextseen.index(p)) for p in knownparents]
120 yield (changerev, node, rev_index, edges, len(revs), n_columns_diff)
78
121 if changerev <= stop_rev:
79 if len(newparents) > 0:
122 break
80 edges.append((nodeidx, nodeidx))
123 revs = next_revs
81 if len(newparents) > 1:
124 filerev -= 1
82 edges.append((nodeidx, nodeidx + 1))
83 nmorecols = len(nextseen) - ncols
84 seen = nextseen
85 yield (char, lines, nodeidx, edges, ncols, nmorecols)
125
86
126 def fix_long_right_edges(edges):
87 def fix_long_right_edges(edges):
127 for (i, (start, end)) in enumerate(edges):
88 for (i, (start, end)) in enumerate(edges):
128 if end > start:
89 if end > start:
129 edges[i] = (start, end + 1)
90 edges[i] = (start, end + 1)
130
91
131 def get_nodeline_edges_tail(
92 def get_nodeline_edges_tail(
132 node_index, p_node_index, n_columns, n_columns_diff, p_diff, fix_tail):
93 node_index, p_node_index, n_columns, n_columns_diff, p_diff, fix_tail):
133 if fix_tail and n_columns_diff == p_diff and n_columns_diff != 0:
94 if fix_tail and n_columns_diff == p_diff and n_columns_diff != 0:
134 # Still going in the same non-vertical direction.
95 # Still going in the same non-vertical direction.
135 if n_columns_diff == -1:
96 if n_columns_diff == -1:
136 start = max(node_index + 1, p_node_index)
97 start = max(node_index + 1, p_node_index)
137 tail = ["|", " "] * (start - node_index - 1)
98 tail = ["|", " "] * (start - node_index - 1)
138 tail.extend(["/", " "] * (n_columns - start))
99 tail.extend(["/", " "] * (n_columns - start))
139 return tail
100 return tail
140 else:
101 else:
141 return ["\\", " "] * (n_columns - node_index - 1)
102 return ["\\", " "] * (n_columns - node_index - 1)
142 else:
103 else:
143 return ["|", " "] * (n_columns - node_index - 1)
104 return ["|", " "] * (n_columns - node_index - 1)
144
105
145 def draw_edges(edges, nodeline, interline):
106 def draw_edges(edges, nodeline, interline):
146 for (start, end) in edges:
107 for (start, end) in edges:
147 if start == end + 1:
108 if start == end + 1:
148 interline[2 * end + 1] = "/"
109 interline[2 * end + 1] = "/"
149 elif start == end - 1:
110 elif start == end - 1:
150 interline[2 * start + 1] = "\\"
111 interline[2 * start + 1] = "\\"
151 elif start == end:
112 elif start == end:
152 interline[2 * start] = "|"
113 interline[2 * start] = "|"
153 else:
114 else:
154 nodeline[2 * end] = "+"
115 nodeline[2 * end] = "+"
155 if start > end:
116 if start > end:
156 (start, end) = (end,start)
117 (start, end) = (end,start)
157 for i in range(2 * start + 1, 2 * end):
118 for i in range(2 * start + 1, 2 * end):
158 if nodeline[i] != "+":
119 if nodeline[i] != "+":
159 nodeline[i] = "-"
120 nodeline[i] = "-"
160
121
161 def get_padding_line(ni, n_columns, edges):
122 def get_padding_line(ni, n_columns, edges):
162 line = []
123 line = []
163 line.extend(["|", " "] * ni)
124 line.extend(["|", " "] * ni)
164 if (ni, ni - 1) in edges or (ni, ni) in edges:
125 if (ni, ni - 1) in edges or (ni, ni) in edges:
165 # (ni, ni - 1) (ni, ni)
126 # (ni, ni - 1) (ni, ni)
166 # | | | | | | | |
127 # | | | | | | | |
167 # +---o | | o---+
128 # +---o | | o---+
168 # | | c | | c | |
129 # | | c | | c | |
169 # | |/ / | |/ /
130 # | |/ / | |/ /
170 # | | | | | |
131 # | | | | | |
171 c = "|"
132 c = "|"
172 else:
133 else:
173 c = " "
134 c = " "
174 line.extend([c, " "])
135 line.extend([c, " "])
175 line.extend(["|", " "] * (n_columns - ni - 1))
136 line.extend(["|", " "] * (n_columns - ni - 1))
176 return line
137 return line
177
138
178 def ascii(ui, grapher):
139 def ascii(ui, grapher):
179 """prints an ASCII graph of the DAG returned by the grapher
140 """prints an ASCII graph of the DAG returned by the grapher
180
141
181 grapher is a generator that emits tuples with the following elements:
142 grapher is a generator that emits tuples with the following elements:
182
143
183 - Character to use as node's symbol.
144 - Character to use as node's symbol.
184 - List of lines to display as the node's text.
145 - List of lines to display as the node's text.
185 - Column of the current node in the set of ongoing edges.
146 - Column of the current node in the set of ongoing edges.
186 - Edges; a list of (col, next_col) indicating the edges between
147 - Edges; a list of (col, next_col) indicating the edges between
187 the current node and its parents.
148 the current node and its parents.
188 - Number of columns (ongoing edges) in the current revision.
149 - Number of columns (ongoing edges) in the current revision.
189 - The difference between the number of columns (ongoing edges)
150 - The difference between the number of columns (ongoing edges)
190 in the next revision and the number of columns (ongoing edges)
151 in the next revision and the number of columns (ongoing edges)
191 in the current revision. That is: -1 means one column removed;
152 in the current revision. That is: -1 means one column removed;
192 0 means no columns added or removed; 1 means one column added.
153 0 means no columns added or removed; 1 means one column added.
193 """
154 """
194 prev_n_columns_diff = 0
155 prev_n_columns_diff = 0
195 prev_node_index = 0
156 prev_node_index = 0
196 for (node_ch, node_lines, node_index, edges, n_columns, n_columns_diff) in grapher:
157 for (node_ch, node_lines, node_index, edges, n_columns, n_columns_diff) in grapher:
197
158
198 assert -2 < n_columns_diff < 2
159 assert -2 < n_columns_diff < 2
199 if n_columns_diff == -1:
160 if n_columns_diff == -1:
200 # Transform
161 # Transform
201 #
162 #
202 # | | | | | |
163 # | | | | | |
203 # o | | into o---+
164 # o | | into o---+
204 # |X / |/ /
165 # |X / |/ /
205 # | | | |
166 # | | | |
206 fix_long_right_edges(edges)
167 fix_long_right_edges(edges)
207
168
208 # add_padding_line says whether to rewrite
169 # add_padding_line says whether to rewrite
209 #
170 #
210 # | | | | | | | |
171 # | | | | | | | |
211 # | o---+ into | o---+
172 # | o---+ into | o---+
212 # | / / | | | # <--- padding line
173 # | / / | | | # <--- padding line
213 # o | | | / /
174 # o | | | / /
214 # o | |
175 # o | |
215 add_padding_line = (len(node_lines) > 2 and
176 add_padding_line = (len(node_lines) > 2 and
216 n_columns_diff == -1 and
177 n_columns_diff == -1 and
217 [x for (x, y) in edges if x + 1 < y])
178 [x for (x, y) in edges if x + 1 < y])
218
179
219 # fix_nodeline_tail says whether to rewrite
180 # fix_nodeline_tail says whether to rewrite
220 #
181 #
221 # | | o | | | | o | |
182 # | | o | | | | o | |
222 # | | |/ / | | |/ /
183 # | | |/ / | | |/ /
223 # | o | | into | o / / # <--- fixed nodeline tail
184 # | o | | into | o / / # <--- fixed nodeline tail
224 # | |/ / | |/ /
185 # | |/ / | |/ /
225 # o | | o | |
186 # o | | o | |
226 fix_nodeline_tail = len(node_lines) <= 2 and not add_padding_line
187 fix_nodeline_tail = len(node_lines) <= 2 and not add_padding_line
227
188
228 # nodeline is the line containing the node character (typically o)
189 # nodeline is the line containing the node character (typically o)
229 nodeline = ["|", " "] * node_index
190 nodeline = ["|", " "] * node_index
230 nodeline.extend([node_ch, " "])
191 nodeline.extend([node_ch, " "])
231
192
232 nodeline.extend(
193 nodeline.extend(
233 get_nodeline_edges_tail(
194 get_nodeline_edges_tail(
234 node_index, prev_node_index, n_columns, n_columns_diff,
195 node_index, prev_node_index, n_columns, n_columns_diff,
235 prev_n_columns_diff, fix_nodeline_tail))
196 prev_n_columns_diff, fix_nodeline_tail))
236
197
237 # shift_interline is the line containing the non-vertical
198 # shift_interline is the line containing the non-vertical
238 # edges between this entry and the next
199 # edges between this entry and the next
239 shift_interline = ["|", " "] * node_index
200 shift_interline = ["|", " "] * node_index
240 if n_columns_diff == -1:
201 if n_columns_diff == -1:
241 n_spaces = 1
202 n_spaces = 1
242 edge_ch = "/"
203 edge_ch = "/"
243 elif n_columns_diff == 0:
204 elif n_columns_diff == 0:
244 n_spaces = 2
205 n_spaces = 2
245 edge_ch = "|"
206 edge_ch = "|"
246 else:
207 else:
247 n_spaces = 3
208 n_spaces = 3
248 edge_ch = "\\"
209 edge_ch = "\\"
249 shift_interline.extend(n_spaces * [" "])
210 shift_interline.extend(n_spaces * [" "])
250 shift_interline.extend([edge_ch, " "] * (n_columns - node_index - 1))
211 shift_interline.extend([edge_ch, " "] * (n_columns - node_index - 1))
251
212
252 # draw edges from the current node to its parents
213 # draw edges from the current node to its parents
253 draw_edges(edges, nodeline, shift_interline)
214 draw_edges(edges, nodeline, shift_interline)
254
215
255 # lines is the list of all graph lines to print
216 # lines is the list of all graph lines to print
256 lines = [nodeline]
217 lines = [nodeline]
257 if add_padding_line:
218 if add_padding_line:
258 lines.append(get_padding_line(node_index, n_columns, edges))
219 lines.append(get_padding_line(node_index, n_columns, edges))
259 lines.append(shift_interline)
220 lines.append(shift_interline)
260
221
261 # make sure that there are as many graph lines as there are
222 # make sure that there are as many graph lines as there are
262 # log strings
223 # log strings
263 while len(node_lines) < len(lines):
224 while len(node_lines) < len(lines):
264 node_lines.append("")
225 node_lines.append("")
265 if len(lines) < len(node_lines):
226 if len(lines) < len(node_lines):
266 extra_interline = ["|", " "] * (n_columns + n_columns_diff)
227 extra_interline = ["|", " "] * (n_columns + n_columns_diff)
267 while len(lines) < len(node_lines):
228 while len(lines) < len(node_lines):
268 lines.append(extra_interline)
229 lines.append(extra_interline)
269
230
270 # print lines
231 # print lines
271 indentation_level = max(n_columns, n_columns + n_columns_diff)
232 indentation_level = max(n_columns, n_columns + n_columns_diff)
272 for (line, logstr) in zip(lines, node_lines):
233 for (line, logstr) in zip(lines, node_lines):
273 ln = "%-*s %s" % (2 * indentation_level, "".join(line), logstr)
234 ln = "%-*s %s" % (2 * indentation_level, "".join(line), logstr)
274 ui.write(ln.rstrip() + '\n')
235 ui.write(ln.rstrip() + '\n')
275
236
276 # ... and start over
237 # ... and start over
277 prev_node_index = node_index
238 prev_node_index = node_index
278 prev_n_columns_diff = n_columns_diff
239 prev_n_columns_diff = n_columns_diff
279
240
280 def get_limit(limit_opt):
241 def get_limit(limit_opt):
281 if limit_opt:
242 if limit_opt:
282 try:
243 try:
283 limit = int(limit_opt)
244 limit = int(limit_opt)
284 except ValueError:
245 except ValueError:
285 raise Abort(_("limit must be a positive integer"))
246 raise Abort(_("limit must be a positive integer"))
286 if limit <= 0:
247 if limit <= 0:
287 raise Abort(_("limit must be positive"))
248 raise Abort(_("limit must be positive"))
288 else:
249 else:
289 limit = sys.maxint
250 limit = sys.maxint
290 return limit
251 return limit
291
252
292 def get_revs(repo, rev_opt):
253 def get_revs(repo, rev_opt):
293 if rev_opt:
254 if rev_opt:
294 revs = revrange(repo, rev_opt)
255 revs = revrange(repo, rev_opt)
295 return (max(revs), min(revs))
256 return (max(revs), min(revs))
296 else:
257 else:
297 return (len(repo) - 1, 0)
258 return (len(repo) - 1, 0)
298
259
299 def graphlog(ui, repo, path=None, **opts):
260 def graphlog(ui, repo, path=None, **opts):
300 """show revision history alongside an ASCII revision graph
261 """show revision history alongside an ASCII revision graph
301
262
302 Print a revision history alongside a revision graph drawn with
263 Print a revision history alongside a revision graph drawn with
303 ASCII characters.
264 ASCII characters.
304
265
305 Nodes printed as an @ character are parents of the working
266 Nodes printed as an @ character are parents of the working
306 directory.
267 directory.
307 """
268 """
308
269
309 limit = get_limit(opts["limit"])
270 limit = get_limit(opts["limit"])
310 (start_rev, stop_rev) = get_revs(repo, opts["rev"])
271 start, stop = get_revs(repo, opts["rev"])
311 stop_rev = max(stop_rev, start_rev - limit + 1)
272 stop = max(stop, start - limit + 1)
312 if start_rev == nullrev:
273 if start == nullrev:
313 return
274 return
275
314 if path:
276 if path:
315 path = canonpath(repo.root, os.getcwd(), path)
277 path = canonpath(repo.root, os.getcwd(), path)
316 if path:
278 if path: # could be reset in canonpath
317 revgrapher = filelog_grapher(repo, path, start_rev, stop_rev)
279 revdag = filerevs(repo, path, start, stop)
318 else:
280 else:
319 revgrapher = revision_grapher(repo, start_rev, stop_rev)
281 revdag = revisions(repo, start, stop)
320
282
321 repo_parents = repo.dirstate.parents()
283 repo_parents = repo.dirstate.parents()
322 cs_printer = show_changeset(ui, repo, opts)
284 displayer = show_changeset(ui, repo, opts)
323 def grapher():
285 def graphabledag():
324 for (rev, node, node_index, edges, n_columns, n_columns_diff) in revgrapher:
286 for (ctx, parents) in revdag:
325 # log_strings is the list of all log strings to draw alongside
287 # log_strings is the list of all log strings to draw alongside
326 # the graph.
288 # the graph.
327 ui.pushbuffer()
289 ui.pushbuffer()
328 cs_printer.show(repo[rev])
290 displayer.show(ctx)
329 log_strings = ui.popbuffer().split("\n")[:-1]
291 log_strings = ui.popbuffer().split("\n")[:-1]
330 if node in repo_parents:
292 char = ctx.node() in repo_parents and '@' or 'o'
331 node_ch = "@"
293 yield (ctx.rev(), parents, char, log_strings)
332 else:
333 node_ch = "o"
334 yield (node_ch, log_strings, node_index, edges, n_columns, n_columns_diff)
335
294
336 ascii(ui, grapher())
295 ascii(ui, grapher(graphabledag()))
337
296
338 cmdtable = {
297 cmdtable = {
339 "glog":
298 "glog":
340 (graphlog,
299 (graphlog,
341 [('l', 'limit', '', _('limit number of changes displayed')),
300 [('l', 'limit', '', _('limit number of changes displayed')),
342 ('p', 'patch', False, _('show patch')),
301 ('p', 'patch', False, _('show patch')),
343 ('r', 'rev', [], _('show the specified revision or range')),
302 ('r', 'rev', [], _('show the specified revision or range')),
344 ] + templateopts,
303 ] + templateopts,
345 _('hg glog [OPTION]... [FILE]')),
304 _('hg glog [OPTION]... [FILE]')),
346 }
305 }
General Comments 0
You need to be logged in to leave comments. Login now