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