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