##// END OF EJS Templates
configitems: register the 'commands.show.aliasprefix' config
Boris Feld -
r34519:592a3cc1 default
parent child Browse files
Show More
@@ -1,465 +1,472
1 1 # show.py - Extension implementing `hg show`
2 2 #
3 3 # Copyright 2017 Gregory Szorc <gregory.szorc@gmail.com>
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 """unified command to show various repository information (EXPERIMENTAL)
9 9
10 10 This extension provides the :hg:`show` command, which provides a central
11 11 command for displaying commonly-accessed repository data and views of that
12 12 data.
13 13
14 14 The following config options can influence operation.
15 15
16 16 ``commands``
17 17 ------------
18 18
19 19 ``show.aliasprefix``
20 20 List of strings that will register aliases for views. e.g. ``s`` will
21 21 effectively set config options ``alias.s<view> = show <view>`` for all
22 22 views. i.e. `hg swork` would execute `hg show work`.
23 23
24 24 Aliases that would conflict with existing registrations will not be
25 25 performed.
26 26 """
27 27
28 28 from __future__ import absolute_import
29 29
30 30 from mercurial.i18n import _
31 31 from mercurial.node import nullrev
32 32 from mercurial import (
33 33 cmdutil,
34 34 commands,
35 35 destutil,
36 36 error,
37 37 formatter,
38 38 graphmod,
39 39 phases,
40 40 pycompat,
41 41 registrar,
42 42 revset,
43 43 revsetlang,
44 44 )
45 45
46 46 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
47 47 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
48 48 # be specifying the version(s) of Mercurial they are tested with, or
49 49 # leave the attribute unspecified.
50 50 testedwith = 'ships-with-hg-core'
51 51
52 52 cmdtable = {}
53 53 command = registrar.command(cmdtable)
54
55 configtable = {}
56 configitem = registrar.configitem(configtable)
57 configitem('commands', 'show.aliasprefix',
58 default=list,
59 )
60
54 61 revsetpredicate = registrar.revsetpredicate()
55 62
56 63 class showcmdfunc(registrar._funcregistrarbase):
57 64 """Register a function to be invoked for an `hg show <thing>`."""
58 65
59 66 # Used by _formatdoc().
60 67 _docformat = '%s -- %s'
61 68
62 69 def _extrasetup(self, name, func, fmtopic=None, csettopic=None):
63 70 """Called with decorator arguments to register a show view.
64 71
65 72 ``name`` is the sub-command name.
66 73
67 74 ``func`` is the function being decorated.
68 75
69 76 ``fmtopic`` is the topic in the style that will be rendered for
70 77 this view.
71 78
72 79 ``csettopic`` is the topic in the style to be used for a changeset
73 80 printer.
74 81
75 82 If ``fmtopic`` is specified, the view function will receive a
76 83 formatter instance. If ``csettopic`` is specified, the view
77 84 function will receive a changeset printer.
78 85 """
79 86 func._fmtopic = fmtopic
80 87 func._csettopic = csettopic
81 88
82 89 showview = showcmdfunc()
83 90
84 91 @command('show', [
85 92 # TODO: Switch this template flag to use cmdutil.formatteropts if
86 93 # 'hg show' becomes stable before --template/-T is stable. For now,
87 94 # we are putting it here without the '(EXPERIMENTAL)' flag because it
88 95 # is an important part of the 'hg show' user experience and the entire
89 96 # 'hg show' experience is experimental.
90 97 ('T', 'template', '', ('display with template'), _('TEMPLATE')),
91 98 ], _('VIEW'))
92 99 def show(ui, repo, view=None, template=None):
93 100 """show various repository information
94 101
95 102 A requested view of repository data is displayed.
96 103
97 104 If no view is requested, the list of available views is shown and the
98 105 command aborts.
99 106
100 107 .. note::
101 108
102 109 There are no backwards compatibility guarantees for the output of this
103 110 command. Output may change in any future Mercurial release.
104 111
105 112 Consumers wanting stable command output should specify a template via
106 113 ``-T/--template``.
107 114
108 115 List of available views:
109 116 """
110 117 if ui.plain() and not template:
111 118 hint = _('invoke with -T/--template to control output format')
112 119 raise error.Abort(_('must specify a template in plain mode'), hint=hint)
113 120
114 121 views = showview._table
115 122
116 123 if not view:
117 124 ui.pager('show')
118 125 # TODO consider using formatter here so available views can be
119 126 # rendered to custom format.
120 127 ui.write(_('available views:\n'))
121 128 ui.write('\n')
122 129
123 130 for name, func in sorted(views.items()):
124 131 ui.write(('%s\n') % func.__doc__)
125 132
126 133 ui.write('\n')
127 134 raise error.Abort(_('no view requested'),
128 135 hint=_('use "hg show VIEW" to choose a view'))
129 136
130 137 # TODO use same logic as dispatch to perform prefix matching.
131 138 if view not in views:
132 139 raise error.Abort(_('unknown view: %s') % view,
133 140 hint=_('run "hg show" to see available views'))
134 141
135 142 template = template or 'show'
136 143
137 144 fn = views[view]
138 145 ui.pager('show')
139 146
140 147 if fn._fmtopic:
141 148 fmtopic = 'show%s' % fn._fmtopic
142 149 with ui.formatter(fmtopic, {'template': template}) as fm:
143 150 return fn(ui, repo, fm)
144 151 elif fn._csettopic:
145 152 ref = 'show%s' % fn._csettopic
146 153 spec = formatter.lookuptemplate(ui, ref, template)
147 154 displayer = cmdutil.changeset_templater(ui, repo, spec, buffered=True)
148 155 return fn(ui, repo, displayer)
149 156 else:
150 157 return fn(ui, repo)
151 158
152 159 @showview('bookmarks', fmtopic='bookmarks')
153 160 def showbookmarks(ui, repo, fm):
154 161 """bookmarks and their associated changeset"""
155 162 marks = repo._bookmarks
156 163 if not len(marks):
157 164 # This is a bit hacky. Ideally, templates would have a way to
158 165 # specify an empty output, but we shouldn't corrupt JSON while
159 166 # waiting for this functionality.
160 167 if not isinstance(fm, formatter.jsonformatter):
161 168 ui.write(_('(no bookmarks set)\n'))
162 169 return
163 170
164 171 revs = [repo[node].rev() for node in marks.values()]
165 172 active = repo._activebookmark
166 173 longestname = max(len(b) for b in marks)
167 174 nodelen = longestshortest(repo, revs)
168 175
169 176 for bm, node in sorted(marks.items()):
170 177 fm.startitem()
171 178 fm.context(ctx=repo[node])
172 179 fm.write('bookmark', '%s', bm)
173 180 fm.write('node', fm.hexfunc(node), fm.hexfunc(node))
174 181 fm.data(active=bm == active,
175 182 longestbookmarklen=longestname,
176 183 nodelen=nodelen)
177 184
178 185 @showview('stack', csettopic='stack')
179 186 def showstack(ui, repo, displayer):
180 187 """current line of work"""
181 188 wdirctx = repo['.']
182 189 if wdirctx.rev() == nullrev:
183 190 raise error.Abort(_('stack view only available when there is a '
184 191 'working directory'))
185 192
186 193 if wdirctx.phase() == phases.public:
187 194 ui.write(_('(empty stack; working directory parent is a published '
188 195 'changeset)\n'))
189 196 return
190 197
191 198 # TODO extract "find stack" into a function to facilitate
192 199 # customization and reuse.
193 200
194 201 baserev = destutil.stackbase(ui, repo)
195 202 basectx = None
196 203
197 204 if baserev is None:
198 205 baserev = wdirctx.rev()
199 206 stackrevs = {wdirctx.rev()}
200 207 else:
201 208 stackrevs = set(repo.revs('%d::.', baserev))
202 209
203 210 ctx = repo[baserev]
204 211 if ctx.p1().rev() != nullrev:
205 212 basectx = ctx.p1()
206 213
207 214 # And relevant descendants.
208 215 branchpointattip = False
209 216 cl = repo.changelog
210 217
211 218 for rev in cl.descendants([wdirctx.rev()]):
212 219 ctx = repo[rev]
213 220
214 221 # Will only happen if . is public.
215 222 if ctx.phase() == phases.public:
216 223 break
217 224
218 225 stackrevs.add(ctx.rev())
219 226
220 227 # ctx.children() within a function iterating on descandants
221 228 # potentially has severe performance concerns because revlog.children()
222 229 # iterates over all revisions after ctx's node. However, the number of
223 230 # draft changesets should be a reasonably small number. So even if
224 231 # this is quadratic, the perf impact should be minimal.
225 232 if len(ctx.children()) > 1:
226 233 branchpointattip = True
227 234 break
228 235
229 236 stackrevs = list(sorted(stackrevs, reverse=True))
230 237
231 238 # Find likely target heads for the current stack. These are likely
232 239 # merge or rebase targets.
233 240 if basectx:
234 241 # TODO make this customizable?
235 242 newheads = set(repo.revs('heads(%d::) - %ld - not public()',
236 243 basectx.rev(), stackrevs))
237 244 else:
238 245 newheads = set()
239 246
240 247 allrevs = set(stackrevs) | newheads | set([baserev])
241 248 nodelen = longestshortest(repo, allrevs)
242 249
243 250 try:
244 251 cmdutil.findcmd('rebase', commands.table)
245 252 haverebase = True
246 253 except (error.AmbiguousCommand, error.UnknownCommand):
247 254 haverebase = False
248 255
249 256 # TODO use templating.
250 257 # TODO consider using graphmod. But it may not be necessary given
251 258 # our simplicity and the customizations required.
252 259 # TODO use proper graph symbols from graphmod
253 260
254 261 shortesttmpl = formatter.maketemplater(ui, '{shortest(node, %d)}' % nodelen)
255 262 def shortest(ctx):
256 263 return shortesttmpl.render({'ctx': ctx, 'node': ctx.hex()})
257 264
258 265 # We write out new heads to aid in DAG awareness and to help with decision
259 266 # making on how the stack should be reconciled with commits made since the
260 267 # branch point.
261 268 if newheads:
262 269 # Calculate distance from base so we can render the count and so we can
263 270 # sort display order by commit distance.
264 271 revdistance = {}
265 272 for head in newheads:
266 273 # There is some redundancy in DAG traversal here and therefore
267 274 # room to optimize.
268 275 ancestors = cl.ancestors([head], stoprev=basectx.rev())
269 276 revdistance[head] = len(list(ancestors))
270 277
271 278 sourcectx = repo[stackrevs[-1]]
272 279
273 280 sortedheads = sorted(newheads, key=lambda x: revdistance[x],
274 281 reverse=True)
275 282
276 283 for i, rev in enumerate(sortedheads):
277 284 ctx = repo[rev]
278 285
279 286 if i:
280 287 ui.write(': ')
281 288 else:
282 289 ui.write(' ')
283 290
284 291 ui.write(('o '))
285 292 displayer.show(ctx, nodelen=nodelen)
286 293 displayer.flush(ctx)
287 294 ui.write('\n')
288 295
289 296 if i:
290 297 ui.write(':/')
291 298 else:
292 299 ui.write(' /')
293 300
294 301 ui.write(' (')
295 302 ui.write(_('%d commits ahead') % revdistance[rev],
296 303 label='stack.commitdistance')
297 304
298 305 if haverebase:
299 306 # TODO may be able to omit --source in some scenarios
300 307 ui.write('; ')
301 308 ui.write(('hg rebase --source %s --dest %s' % (
302 309 shortest(sourcectx), shortest(ctx))),
303 310 label='stack.rebasehint')
304 311
305 312 ui.write(')\n')
306 313
307 314 ui.write(':\n: ')
308 315 ui.write(_('(stack head)\n'), label='stack.label')
309 316
310 317 if branchpointattip:
311 318 ui.write(' \\ / ')
312 319 ui.write(_('(multiple children)\n'), label='stack.label')
313 320 ui.write(' |\n')
314 321
315 322 for rev in stackrevs:
316 323 ctx = repo[rev]
317 324 symbol = '@' if rev == wdirctx.rev() else 'o'
318 325
319 326 if newheads:
320 327 ui.write(': ')
321 328 else:
322 329 ui.write(' ')
323 330
324 331 ui.write(symbol, ' ')
325 332 displayer.show(ctx, nodelen=nodelen)
326 333 displayer.flush(ctx)
327 334 ui.write('\n')
328 335
329 336 # TODO display histedit hint?
330 337
331 338 if basectx:
332 339 # Vertically and horizontally separate stack base from parent
333 340 # to reinforce stack boundary.
334 341 if newheads:
335 342 ui.write(':/ ')
336 343 else:
337 344 ui.write(' / ')
338 345
339 346 ui.write(_('(stack base)'), '\n', label='stack.label')
340 347 ui.write(('o '))
341 348
342 349 displayer.show(basectx, nodelen=nodelen)
343 350 displayer.flush(basectx)
344 351 ui.write('\n')
345 352
346 353 @revsetpredicate('_underway([commitage[, headage]])')
347 354 def underwayrevset(repo, subset, x):
348 355 args = revset.getargsdict(x, 'underway', 'commitage headage')
349 356 if 'commitage' not in args:
350 357 args['commitage'] = None
351 358 if 'headage' not in args:
352 359 args['headage'] = None
353 360
354 361 # We assume callers of this revset add a topographical sort on the
355 362 # result. This means there is no benefit to making the revset lazy
356 363 # since the topographical sort needs to consume all revs.
357 364 #
358 365 # With this in mind, we build up the set manually instead of constructing
359 366 # a complex revset. This enables faster execution.
360 367
361 368 # Mutable changesets (non-public) are the most important changesets
362 369 # to return. ``not public()`` will also pull in obsolete changesets if
363 370 # there is a non-obsolete changeset with obsolete ancestors. This is
364 371 # why we exclude obsolete changesets from this query.
365 372 rs = 'not public() and not obsolete()'
366 373 rsargs = []
367 374 if args['commitage']:
368 375 rs += ' and date(%s)'
369 376 rsargs.append(revsetlang.getstring(args['commitage'],
370 377 _('commitage requires a string')))
371 378
372 379 mutable = repo.revs(rs, *rsargs)
373 380 relevant = revset.baseset(mutable)
374 381
375 382 # Add parents of mutable changesets to provide context.
376 383 relevant += repo.revs('parents(%ld)', mutable)
377 384
378 385 # We also pull in (public) heads if they a) aren't closing a branch
379 386 # b) are recent.
380 387 rs = 'head() and not closed()'
381 388 rsargs = []
382 389 if args['headage']:
383 390 rs += ' and date(%s)'
384 391 rsargs.append(revsetlang.getstring(args['headage'],
385 392 _('headage requires a string')))
386 393
387 394 relevant += repo.revs(rs, *rsargs)
388 395
389 396 # Add working directory parent.
390 397 wdirrev = repo['.'].rev()
391 398 if wdirrev != nullrev:
392 399 relevant += revset.baseset({wdirrev})
393 400
394 401 return subset & relevant
395 402
396 403 @showview('work', csettopic='work')
397 404 def showwork(ui, repo, displayer):
398 405 """changesets that aren't finished"""
399 406 # TODO support date-based limiting when calling revset.
400 407 revs = repo.revs('sort(_underway(), topo)')
401 408 nodelen = longestshortest(repo, revs)
402 409
403 410 revdag = graphmod.dagwalker(repo, revs)
404 411
405 412 ui.setconfig('experimental', 'graphshorten', True)
406 413 cmdutil.displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges,
407 414 props={'nodelen': nodelen})
408 415
409 416 def extsetup(ui):
410 417 # Alias `hg <prefix><view>` to `hg show <view>`.
411 418 for prefix in ui.configlist('commands', 'show.aliasprefix'):
412 419 for view in showview._table:
413 420 name = '%s%s' % (prefix, view)
414 421
415 422 choice, allcommands = cmdutil.findpossible(name, commands.table,
416 423 strict=True)
417 424
418 425 # This alias is already a command name. Don't set it.
419 426 if name in choice:
420 427 continue
421 428
422 429 # Same for aliases.
423 430 if ui.config('alias', name):
424 431 continue
425 432
426 433 ui.setconfig('alias', name, 'show %s' % view, source='show')
427 434
428 435 def longestshortest(repo, revs, minlen=4):
429 436 """Return the length of the longest shortest node to identify revisions.
430 437
431 438 The result of this function can be used with the ``shortest()`` template
432 439 function to ensure that a value is unique and unambiguous for a given
433 440 set of nodes.
434 441
435 442 The number of revisions in the repo is taken into account to prevent
436 443 a numeric node prefix from conflicting with an integer revision number.
437 444 If we fail to do this, a value of e.g. ``10023`` could mean either
438 445 revision 10023 or node ``10023abc...``.
439 446 """
440 447 tmpl = formatter.maketemplater(repo.ui, '{shortest(node, %d)}' % minlen)
441 448 lens = [minlen]
442 449 for rev in revs:
443 450 ctx = repo[rev]
444 451 shortest = tmpl.render({'ctx': ctx, 'node': ctx.hex()})
445 452 lens.append(len(shortest))
446 453
447 454 return max(lens)
448 455
449 456 # Adjust the docstring of the show command so it shows all registered views.
450 457 # This is a bit hacky because it runs at the end of module load. When moved
451 458 # into core or when another extension wants to provide a view, we'll need
452 459 # to do this more robustly.
453 460 # TODO make this more robust.
454 461 def _updatedocstring():
455 462 longest = max(map(len, showview._table.keys()))
456 463 entries = []
457 464 for key in sorted(showview._table.keys()):
458 465 entries.append(pycompat.sysstr(' %s %s' % (
459 466 key.ljust(longest), showview._table[key]._origdoc)))
460 467
461 468 cmdtable['show'][0].__doc__ = pycompat.sysstr('%s\n\n%s\n ') % (
462 469 cmdtable['show'][0].__doc__.rstrip(),
463 470 pycompat.sysstr('\n\n').join(entries))
464 471
465 472 _updatedocstring()
General Comments 0
You need to be logged in to leave comments. Login now