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