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