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