##// END OF EJS Templates
graphlog: unify log -G revset translation
Patrick Mezard -
r14085:4852753d default
parent child Browse files
Show More
@@ -1,366 +1,370
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 the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''command to view revision graphs from a shell
9 9
10 10 This extension adds a --graph option to the incoming, outgoing and log
11 11 commands. When this options is given, an ASCII representation of the
12 12 revision graph is also shown.
13 13 '''
14 14
15 15 from mercurial.cmdutil import revrange, show_changeset
16 16 from mercurial.commands import templateopts
17 17 from mercurial.i18n import _
18 18 from mercurial.node import nullrev
19 19 from mercurial import cmdutil, commands, extensions
20 20 from mercurial import hg, scmutil, util, graphmod
21 21
22 22 ASCIIDATA = 'ASC'
23 23
24 24 def asciiedges(seen, rev, parents):
25 25 """adds edge info to changelog DAG walk suitable for ascii()"""
26 26 if rev not in seen:
27 27 seen.append(rev)
28 28 nodeidx = seen.index(rev)
29 29
30 30 knownparents = []
31 31 newparents = []
32 32 for parent in parents:
33 33 if parent in seen:
34 34 knownparents.append(parent)
35 35 else:
36 36 newparents.append(parent)
37 37
38 38 ncols = len(seen)
39 39 seen[nodeidx:nodeidx + 1] = newparents
40 40 edges = [(nodeidx, seen.index(p)) for p in knownparents]
41 41
42 42 if len(newparents) > 0:
43 43 edges.append((nodeidx, nodeidx))
44 44 if len(newparents) > 1:
45 45 edges.append((nodeidx, nodeidx + 1))
46 46
47 47 nmorecols = len(seen) - ncols
48 48 return nodeidx, edges, ncols, nmorecols
49 49
50 50 def fix_long_right_edges(edges):
51 51 for (i, (start, end)) in enumerate(edges):
52 52 if end > start:
53 53 edges[i] = (start, end + 1)
54 54
55 55 def get_nodeline_edges_tail(
56 56 node_index, p_node_index, n_columns, n_columns_diff, p_diff, fix_tail):
57 57 if fix_tail and n_columns_diff == p_diff and n_columns_diff != 0:
58 58 # Still going in the same non-vertical direction.
59 59 if n_columns_diff == -1:
60 60 start = max(node_index + 1, p_node_index)
61 61 tail = ["|", " "] * (start - node_index - 1)
62 62 tail.extend(["/", " "] * (n_columns - start))
63 63 return tail
64 64 else:
65 65 return ["\\", " "] * (n_columns - node_index - 1)
66 66 else:
67 67 return ["|", " "] * (n_columns - node_index - 1)
68 68
69 69 def draw_edges(edges, nodeline, interline):
70 70 for (start, end) in edges:
71 71 if start == end + 1:
72 72 interline[2 * end + 1] = "/"
73 73 elif start == end - 1:
74 74 interline[2 * start + 1] = "\\"
75 75 elif start == end:
76 76 interline[2 * start] = "|"
77 77 else:
78 78 nodeline[2 * end] = "+"
79 79 if start > end:
80 80 (start, end) = (end, start)
81 81 for i in range(2 * start + 1, 2 * end):
82 82 if nodeline[i] != "+":
83 83 nodeline[i] = "-"
84 84
85 85 def get_padding_line(ni, n_columns, edges):
86 86 line = []
87 87 line.extend(["|", " "] * ni)
88 88 if (ni, ni - 1) in edges or (ni, ni) in edges:
89 89 # (ni, ni - 1) (ni, ni)
90 90 # | | | | | | | |
91 91 # +---o | | o---+
92 92 # | | c | | c | |
93 93 # | |/ / | |/ /
94 94 # | | | | | |
95 95 c = "|"
96 96 else:
97 97 c = " "
98 98 line.extend([c, " "])
99 99 line.extend(["|", " "] * (n_columns - ni - 1))
100 100 return line
101 101
102 102 def asciistate():
103 103 """returns the initial value for the "state" argument to ascii()"""
104 104 return [0, 0]
105 105
106 106 def ascii(ui, state, type, char, text, coldata):
107 107 """prints an ASCII graph of the DAG
108 108
109 109 takes the following arguments (one call per node in the graph):
110 110
111 111 - ui to write to
112 112 - Somewhere to keep the needed state in (init to asciistate())
113 113 - Column of the current node in the set of ongoing edges.
114 114 - Type indicator of node data == ASCIIDATA.
115 115 - Payload: (char, lines):
116 116 - Character to use as node's symbol.
117 117 - List of lines to display as the node's text.
118 118 - Edges; a list of (col, next_col) indicating the edges between
119 119 the current node and its parents.
120 120 - Number of columns (ongoing edges) in the current revision.
121 121 - The difference between the number of columns (ongoing edges)
122 122 in the next revision and the number of columns (ongoing edges)
123 123 in the current revision. That is: -1 means one column removed;
124 124 0 means no columns added or removed; 1 means one column added.
125 125 """
126 126
127 127 idx, edges, ncols, coldiff = coldata
128 128 assert -2 < coldiff < 2
129 129 if coldiff == -1:
130 130 # Transform
131 131 #
132 132 # | | | | | |
133 133 # o | | into o---+
134 134 # |X / |/ /
135 135 # | | | |
136 136 fix_long_right_edges(edges)
137 137
138 138 # add_padding_line says whether to rewrite
139 139 #
140 140 # | | | | | | | |
141 141 # | o---+ into | o---+
142 142 # | / / | | | # <--- padding line
143 143 # o | | | / /
144 144 # o | |
145 145 add_padding_line = (len(text) > 2 and coldiff == -1 and
146 146 [x for (x, y) in edges if x + 1 < y])
147 147
148 148 # fix_nodeline_tail says whether to rewrite
149 149 #
150 150 # | | o | | | | o | |
151 151 # | | |/ / | | |/ /
152 152 # | o | | into | o / / # <--- fixed nodeline tail
153 153 # | |/ / | |/ /
154 154 # o | | o | |
155 155 fix_nodeline_tail = len(text) <= 2 and not add_padding_line
156 156
157 157 # nodeline is the line containing the node character (typically o)
158 158 nodeline = ["|", " "] * idx
159 159 nodeline.extend([char, " "])
160 160
161 161 nodeline.extend(
162 162 get_nodeline_edges_tail(idx, state[1], ncols, coldiff,
163 163 state[0], fix_nodeline_tail))
164 164
165 165 # shift_interline is the line containing the non-vertical
166 166 # edges between this entry and the next
167 167 shift_interline = ["|", " "] * idx
168 168 if coldiff == -1:
169 169 n_spaces = 1
170 170 edge_ch = "/"
171 171 elif coldiff == 0:
172 172 n_spaces = 2
173 173 edge_ch = "|"
174 174 else:
175 175 n_spaces = 3
176 176 edge_ch = "\\"
177 177 shift_interline.extend(n_spaces * [" "])
178 178 shift_interline.extend([edge_ch, " "] * (ncols - idx - 1))
179 179
180 180 # draw edges from the current node to its parents
181 181 draw_edges(edges, nodeline, shift_interline)
182 182
183 183 # lines is the list of all graph lines to print
184 184 lines = [nodeline]
185 185 if add_padding_line:
186 186 lines.append(get_padding_line(idx, ncols, edges))
187 187 lines.append(shift_interline)
188 188
189 189 # make sure that there are as many graph lines as there are
190 190 # log strings
191 191 while len(text) < len(lines):
192 192 text.append("")
193 193 if len(lines) < len(text):
194 194 extra_interline = ["|", " "] * (ncols + coldiff)
195 195 while len(lines) < len(text):
196 196 lines.append(extra_interline)
197 197
198 198 # print lines
199 199 indentation_level = max(ncols, ncols + coldiff)
200 200 for (line, logstr) in zip(lines, text):
201 201 ln = "%-*s %s" % (2 * indentation_level, "".join(line), logstr)
202 202 ui.write(ln.rstrip() + '\n')
203 203
204 204 # ... and start over
205 205 state[0] = coldiff
206 206 state[1] = idx
207 207
208 208 def get_revs(repo, rev_opt):
209 209 if rev_opt:
210 210 revs = revrange(repo, rev_opt)
211 211 if len(revs) == 0:
212 212 return (nullrev, nullrev)
213 213 return (max(revs), min(revs))
214 214 else:
215 215 return (len(repo) - 1, 0)
216 216
217 217 def check_unsupported_flags(opts):
218 218 for op in ["follow_first", "copies", "newest_first"]:
219 219 if op in opts and opts[op]:
220 220 raise util.Abort(_("-G/--graph option is incompatible with --%s")
221 221 % op.replace("_", "-"))
222 222
223 223 def revset(pats, opts):
224 224 """Return revset str built of revisions, log options and file patterns.
225 225 """
226 opt2revset = dict(only_merges='merge()',
227 only_branch='branch($)',
228 no_merges='not merge()',
229 include='file($)',
230 exclude='not file($)',
231 prune='not ($ or ancestors($))',
232 user='user($)',
233 branch='branch($)',
234 keyword='keyword($)',
235 follow='follow()',
236 removed='removes("*")')
237 opt2revset = dict((k, v.replace('$', '%(val)r'))
238 for k,v in opt2revset.iteritems())
226 opt2revset = {
227 'follow': (0, 'follow()'),
228 'no_merges': (0, 'not merge()'),
229 'only_merges': (0, 'merge()'),
230 'removed': (0, 'removes("*")'),
231 'date': (1, 'date($)'),
232 'branch': (2, 'branch($)'),
233 'exclude': (2, 'not file($)'),
234 'include': (2, 'file($)'),
235 'keyword': (2, 'keyword($)'),
236 'only_branch': (2, 'branch($)'),
237 'prune': (2, 'not ($ or ancestors($))'),
238 'user': (2, 'user($)'),
239 }
239 240 revset = []
240 241 for op, val in opts.iteritems():
241 242 if not val:
242 243 continue
243 revop = opt2revset.get(op, op)
244 if op in ('follow', 'only_merges', 'no_merges', 'removed'):
245 revset.append('%s' % revop)
246 elif op in ('date',):
247 revset.append('%s(%r)' % (revop, val))
248 elif op in ('include', 'exclude', 'user', 'branch', 'keyword',
249 'prune', 'only_branch'):
244 if op == 'rev':
245 # Already a revset
246 revset.extend(val)
247 if op not in opt2revset:
248 continue
249 arity, revop = opt2revset[op]
250 revop = revop.replace('$', '%(val)r')
251 if arity == 0:
252 revset.append(revop)
253 elif arity == 1:
254 revset.append(revop % {'val': val})
255 else:
250 256 for f in val:
251 257 revset.append(revop % {'val': f})
252 elif op == 'rev':
253 revset.extend(val)
254 258
255 259 for path in pats:
256 260 revset.append('file(%r)' % path)
257 261
258 262 revset = ' and '.join(revset) or 'all()'
259 263 # we want reverted revset to build graph
260 264 revset = 'reverse(%s)' % revset
261 265 if opts['limit']:
262 266 revset = 'limit(%s, %s)' % (revset, opts['limit'])
263 267 return revset
264 268
265 269 def generate(ui, dag, displayer, showparents, edgefn):
266 270 seen, state = [], asciistate()
267 271 for rev, type, ctx, parents in dag:
268 272 char = ctx.node() in showparents and '@' or 'o'
269 273 displayer.show(ctx)
270 274 lines = displayer.hunk.pop(rev).split('\n')[:-1]
271 275 displayer.flush(rev)
272 276 ascii(ui, state, type, char, lines, edgefn(seen, rev, parents))
273 277 displayer.close()
274 278
275 279 def graphlog(ui, repo, *pats, **opts):
276 280 """show revision history alongside an ASCII revision graph
277 281
278 282 Print a revision history alongside a revision graph drawn with
279 283 ASCII characters.
280 284
281 285 Nodes printed as an @ character are parents of the working
282 286 directory.
283 287 """
284 288
285 289 check_unsupported_flags(opts)
286 290
287 291 revs = revrange(repo, [revset(pats, opts)])
288 292 revdag = graphmod.dagwalker(repo, revs)
289 293
290 294 displayer = show_changeset(ui, repo, opts, buffered=True)
291 295 showparents = [ctx.node() for ctx in repo[None].parents()]
292 296 generate(ui, revdag, displayer, showparents, asciiedges)
293 297
294 298 def graphrevs(repo, nodes, opts):
295 299 limit = cmdutil.loglimit(opts)
296 300 nodes.reverse()
297 301 if limit is not None:
298 302 nodes = nodes[:limit]
299 303 return graphmod.nodes(repo, nodes)
300 304
301 305 def goutgoing(ui, repo, dest=None, **opts):
302 306 """show the outgoing changesets alongside an ASCII revision graph
303 307
304 308 Print the outgoing changesets alongside a revision graph drawn with
305 309 ASCII characters.
306 310
307 311 Nodes printed as an @ character are parents of the working
308 312 directory.
309 313 """
310 314
311 315 check_unsupported_flags(opts)
312 316 o = hg._outgoing(ui, repo, dest, opts)
313 317 if o is None:
314 318 return
315 319
316 320 revdag = graphrevs(repo, o, opts)
317 321 displayer = show_changeset(ui, repo, opts, buffered=True)
318 322 showparents = [ctx.node() for ctx in repo[None].parents()]
319 323 generate(ui, revdag, displayer, showparents, asciiedges)
320 324
321 325 def gincoming(ui, repo, source="default", **opts):
322 326 """show the incoming changesets alongside an ASCII revision graph
323 327
324 328 Print the incoming changesets alongside a revision graph drawn with
325 329 ASCII characters.
326 330
327 331 Nodes printed as an @ character are parents of the working
328 332 directory.
329 333 """
330 334 def subreporecurse():
331 335 return 1
332 336
333 337 check_unsupported_flags(opts)
334 338 def display(other, chlist, displayer):
335 339 revdag = graphrevs(other, chlist, opts)
336 340 showparents = [ctx.node() for ctx in repo[None].parents()]
337 341 generate(ui, revdag, displayer, showparents, asciiedges)
338 342
339 343 hg._incoming(display, subreporecurse, ui, repo, source, opts, buffered=True)
340 344
341 345 def uisetup(ui):
342 346 '''Initialize the extension.'''
343 347 _wrapcmd(ui, 'log', commands.table, graphlog)
344 348 _wrapcmd(ui, 'incoming', commands.table, gincoming)
345 349 _wrapcmd(ui, 'outgoing', commands.table, goutgoing)
346 350
347 351 def _wrapcmd(ui, cmd, table, wrapfn):
348 352 '''wrap the command'''
349 353 def graph(orig, *args, **kwargs):
350 354 if kwargs['graph']:
351 355 return wrapfn(*args, **kwargs)
352 356 return orig(*args, **kwargs)
353 357 entry = extensions.wrapcommand(table, cmd, graph)
354 358 entry[1].append(('G', 'graph', None, _("show the revision DAG")))
355 359
356 360 cmdtable = {
357 361 "glog":
358 362 (graphlog,
359 363 [('l', 'limit', '',
360 364 _('limit number of changes displayed'), _('NUM')),
361 365 ('p', 'patch', False, _('show patch')),
362 366 ('r', 'rev', [],
363 367 _('show the specified revision or range'), _('REV')),
364 368 ] + templateopts,
365 369 _('hg glog [OPTION]... [FILE]')),
366 370 }
General Comments 0
You need to be logged in to leave comments. Login now