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