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