show.py
530 lines
| 15.9 KiB
| text/x-python
|
PythonLexer
/ hgext / show.py
|
r31765 | # show.py - Extension implementing `hg show` | ||
# | ||||
# Copyright 2017 Gregory Szorc <gregory.szorc@gmail.com> | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
"""unified command to show various repository information (EXPERIMENTAL) | ||||
This extension provides the :hg:`show` command, which provides a central | ||||
command for displaying commonly-accessed repository data and views of that | ||||
data. | ||||
|
r33132 | |||
The following config options can influence operation. | ||||
``commands`` | ||||
------------ | ||||
``show.aliasprefix`` | ||||
List of strings that will register aliases for views. e.g. ``s`` will | ||||
effectively set config options ``alias.s<view> = show <view>`` for all | ||||
views. i.e. `hg swork` would execute `hg show work`. | ||||
Aliases that would conflict with existing registrations will not be | ||||
performed. | ||||
|
r31765 | """ | ||
from __future__ import absolute_import | ||||
from mercurial.i18n import _ | ||||
|
r43346 | from mercurial.node import nullrev | ||
|
r31765 | from mercurial import ( | ||
cmdutil, | ||||
|
r33132 | commands, | ||
|
r33194 | destutil, | ||
|
r31765 | error, | ||
|
r31859 | formatter, | ||
|
r31944 | graphmod, | ||
|
r35906 | logcmdutil, | ||
|
r33194 | phases, | ||
|
r31820 | pycompat, | ||
|
r31765 | registrar, | ||
|
r31944 | revset, | ||
revsetlang, | ||||
|
r37698 | scmutil, | ||
|
r31765 | ) | ||
# Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for | ||||
# extensions which SHIP WITH MERCURIAL. Non-mainline extensions should | ||||
# be specifying the version(s) of Mercurial they are tested with, or | ||||
# leave the attribute unspecified. | ||||
|
r43347 | testedwith = b'ships-with-hg-core' | ||
|
r31765 | |||
cmdtable = {} | ||||
|
r32337 | command = registrar.command(cmdtable) | ||
|
r34519 | |||
|
r31944 | revsetpredicate = registrar.revsetpredicate() | ||
|
r31765 | |||
|
r43346 | |||
|
r31765 | class showcmdfunc(registrar._funcregistrarbase): | ||
"""Register a function to be invoked for an `hg show <thing>`.""" | ||||
# Used by _formatdoc(). | ||||
|
r43347 | _docformat = b'%s -- %s' | ||
|
r31765 | |||
|
r33046 | def _extrasetup(self, name, func, fmtopic=None, csettopic=None): | ||
|
r31765 | """Called with decorator arguments to register a show view. | ||
``name`` is the sub-command name. | ||||
``func`` is the function being decorated. | ||||
``fmtopic`` is the topic in the style that will be rendered for | ||||
this view. | ||||
|
r33046 | |||
``csettopic`` is the topic in the style to be used for a changeset | ||||
printer. | ||||
If ``fmtopic`` is specified, the view function will receive a | ||||
formatter instance. If ``csettopic`` is specified, the view | ||||
function will receive a changeset printer. | ||||
|
r31765 | """ | ||
func._fmtopic = fmtopic | ||||
|
r33046 | func._csettopic = csettopic | ||
|
r31765 | |||
|
r43346 | |||
|
r31765 | showview = showcmdfunc() | ||
|
r43346 | |||
@command( | ||||
|
r43347 | b'show', | ||
|
r43346 | [ | ||
# TODO: Switch this template flag to use cmdutil.formatteropts if | ||||
# 'hg show' becomes stable before --template/-T is stable. For now, | ||||
# we are putting it here without the '(EXPERIMENTAL)' flag because it | ||||
# is an important part of the 'hg show' user experience and the entire | ||||
# 'hg show' experience is experimental. | ||||
|
r43347 | (b'T', b'template', b'', b'display with template', _(b'TEMPLATE')), | ||
|
r43346 | ], | ||
|
r43347 | _(b'VIEW'), | ||
|
r43346 | helpcategory=command.CATEGORY_CHANGE_NAVIGATION, | ||
) | ||||
|
r31765 | def show(ui, repo, view=None, template=None): | ||
"""show various repository information | ||||
A requested view of repository data is displayed. | ||||
If no view is requested, the list of available views is shown and the | ||||
command aborts. | ||||
.. note:: | ||||
There are no backwards compatibility guarantees for the output of this | ||||
command. Output may change in any future Mercurial release. | ||||
Consumers wanting stable command output should specify a template via | ||||
``-T/--template``. | ||||
List of available views: | ||||
""" | ||||
if ui.plain() and not template: | ||||
|
r43347 | hint = _(b'invoke with -T/--template to control output format') | ||
raise error.Abort( | ||||
_(b'must specify a template in plain mode'), hint=hint | ||||
) | ||||
|
r31765 | |||
views = showview._table | ||||
if not view: | ||||
|
r43347 | ui.pager(b'show') | ||
|
r31765 | # TODO consider using formatter here so available views can be | ||
# rendered to custom format. | ||||
|
r43347 | ui.write(_(b'available views:\n')) | ||
ui.write(b'\n') | ||||
|
r31765 | |||
for name, func in sorted(views.items()): | ||||
|
r43347 | ui.write(b'%s\n' % pycompat.sysbytes(func.__doc__)) | ||
|
r31765 | |||
|
r43347 | ui.write(b'\n') | ||
|
r43346 | raise error.Abort( | ||
|
r43347 | _(b'no view requested'), | ||
hint=_(b'use "hg show VIEW" to choose a view'), | ||||
|
r43346 | ) | ||
|
r31765 | |||
# TODO use same logic as dispatch to perform prefix matching. | ||||
if view not in views: | ||||
|
r43346 | raise error.Abort( | ||
|
r43347 | _(b'unknown view: %s') % view, | ||
hint=_(b'run "hg show" to see available views'), | ||||
|
r43346 | ) | ||
|
r31765 | |||
|
r43347 | template = template or b'show' | ||
|
r31765 | |||
|
r33046 | fn = views[view] | ||
|
r43347 | ui.pager(b'show') | ||
|
r33046 | |||
if fn._fmtopic: | ||||
|
r43347 | fmtopic = b'show%s' % fn._fmtopic | ||
with ui.formatter(fmtopic, {b'template': template}) as fm: | ||||
|
r33046 | return fn(ui, repo, fm) | ||
elif fn._csettopic: | ||||
|
r43347 | ref = b'show%s' % fn._csettopic | ||
|
r33046 | spec = formatter.lookuptemplate(ui, ref, template) | ||
|
r35906 | displayer = logcmdutil.changesettemplater(ui, repo, spec, buffered=True) | ||
|
r33046 | return fn(ui, repo, displayer) | ||
else: | ||||
return fn(ui, repo) | ||||
|
r31765 | |||
|
r43346 | |||
|
r43347 | @showview(b'bookmarks', fmtopic=b'bookmarks') | ||
|
r31765 | def showbookmarks(ui, repo, fm): | ||
"""bookmarks and their associated changeset""" | ||||
marks = repo._bookmarks | ||||
if not len(marks): | ||||
|
r31859 | # This is a bit hacky. Ideally, templates would have a way to | ||
# specify an empty output, but we shouldn't corrupt JSON while | ||||
# waiting for this functionality. | ||||
if not isinstance(fm, formatter.jsonformatter): | ||||
|
r43347 | ui.write(_(b'(no bookmarks set)\n')) | ||
|
r31765 | return | ||
|
r34192 | revs = [repo[node].rev() for node in marks.values()] | ||
|
r31765 | active = repo._activebookmark | ||
longestname = max(len(b) for b in marks) | ||||
|
r34192 | nodelen = longestshortest(repo, revs) | ||
|
r31765 | |||
for bm, node in sorted(marks.items()): | ||||
fm.startitem() | ||||
fm.context(ctx=repo[node]) | ||||
|
r43347 | fm.write(b'bookmark', b'%s', bm) | ||
fm.write(b'node', fm.hexfunc(node), fm.hexfunc(node)) | ||||
|
r43346 | fm.data( | ||
active=bm == active, longestbookmarklen=longestname, nodelen=nodelen | ||||
) | ||||
|
r31765 | |||
|
r43347 | @showview(b'stack', csettopic=b'stack') | ||
|
r33194 | def showstack(ui, repo, displayer): | ||
"""current line of work""" | ||||
|
r43347 | wdirctx = repo[b'.'] | ||
|
r33194 | if wdirctx.rev() == nullrev: | ||
|
r43346 | raise error.Abort( | ||
|
r43347 | _( | ||
b'stack view only available when there is a ' | ||||
b'working directory' | ||||
) | ||||
|
r43346 | ) | ||
|
r33194 | |||
if wdirctx.phase() == phases.public: | ||||
|
r43346 | ui.write( | ||
_( | ||||
|
r43347 | b'(empty stack; working directory parent is a published ' | ||
b'changeset)\n' | ||||
|
r43346 | ) | ||
) | ||||
|
r33194 | return | ||
# TODO extract "find stack" into a function to facilitate | ||||
# customization and reuse. | ||||
baserev = destutil.stackbase(ui, repo) | ||||
basectx = None | ||||
if baserev is None: | ||||
baserev = wdirctx.rev() | ||||
stackrevs = {wdirctx.rev()} | ||||
else: | ||||
|
r43347 | stackrevs = set(repo.revs(b'%d::.', baserev)) | ||
|
r33194 | |||
ctx = repo[baserev] | ||||
if ctx.p1().rev() != nullrev: | ||||
basectx = ctx.p1() | ||||
# And relevant descendants. | ||||
branchpointattip = False | ||||
cl = repo.changelog | ||||
for rev in cl.descendants([wdirctx.rev()]): | ||||
ctx = repo[rev] | ||||
# Will only happen if . is public. | ||||
if ctx.phase() == phases.public: | ||||
break | ||||
stackrevs.add(ctx.rev()) | ||||
|
r33208 | # ctx.children() within a function iterating on descandants | ||
# potentially has severe performance concerns because revlog.children() | ||||
# iterates over all revisions after ctx's node. However, the number of | ||||
# draft changesets should be a reasonably small number. So even if | ||||
# this is quadratic, the perf impact should be minimal. | ||||
|
r33194 | if len(ctx.children()) > 1: | ||
branchpointattip = True | ||||
break | ||||
|
r33206 | stackrevs = list(sorted(stackrevs, reverse=True)) | ||
|
r33194 | |||
# Find likely target heads for the current stack. These are likely | ||||
# merge or rebase targets. | ||||
if basectx: | ||||
# TODO make this customizable? | ||||
|
r43346 | newheads = set( | ||
repo.revs( | ||||
|
r43347 | b'heads(%d::) - %ld - not public()', basectx.rev(), stackrevs | ||
|
r43346 | ) | ||
) | ||||
|
r33194 | else: | ||
newheads = set() | ||||
|
r42224 | allrevs = set(stackrevs) | newheads | {baserev} | ||
|
r34192 | nodelen = longestshortest(repo, allrevs) | ||
|
r33194 | try: | ||
|
r43347 | cmdutil.findcmd(b'rebase', commands.table) | ||
|
r33194 | haverebase = True | ||
|
r33207 | except (error.AmbiguousCommand, error.UnknownCommand): | ||
|
r33194 | haverebase = False | ||
# TODO use templating. | ||||
# TODO consider using graphmod. But it may not be necessary given | ||||
# our simplicity and the customizations required. | ||||
# TODO use proper graph symbols from graphmod | ||||
|
r35485 | tres = formatter.templateresources(ui, repo) | ||
|
r43346 | shortesttmpl = formatter.maketemplater( | ||
|
r43347 | ui, b'{shortest(node, %d)}' % nodelen, resources=tres | ||
|
r43346 | ) | ||
|
r33194 | def shortest(ctx): | ||
|
r43347 | return shortesttmpl.renderdefault({b'ctx': ctx, b'node': ctx.hex()}) | ||
|
r33194 | |||
# We write out new heads to aid in DAG awareness and to help with decision | ||||
# making on how the stack should be reconciled with commits made since the | ||||
# branch point. | ||||
if newheads: | ||||
# Calculate distance from base so we can render the count and so we can | ||||
# sort display order by commit distance. | ||||
revdistance = {} | ||||
for head in newheads: | ||||
# There is some redundancy in DAG traversal here and therefore | ||||
# room to optimize. | ||||
ancestors = cl.ancestors([head], stoprev=basectx.rev()) | ||||
revdistance[head] = len(list(ancestors)) | ||||
sourcectx = repo[stackrevs[-1]] | ||||
|
r43346 | sortedheads = sorted( | ||
newheads, key=lambda x: revdistance[x], reverse=True | ||||
) | ||||
|
r33194 | |||
for i, rev in enumerate(sortedheads): | ||||
ctx = repo[rev] | ||||
if i: | ||||
|
r43347 | ui.write(b': ') | ||
|
r33194 | else: | ||
|
r43347 | ui.write(b' ') | ||
|
r33194 | |||
|
r43347 | ui.write(b'o ') | ||
|
r34192 | displayer.show(ctx, nodelen=nodelen) | ||
|
r33194 | displayer.flush(ctx) | ||
|
r43347 | ui.write(b'\n') | ||
|
r33194 | |||
if i: | ||||
|
r43347 | ui.write(b':/') | ||
|
r33194 | else: | ||
|
r43347 | ui.write(b' /') | ||
|
r33194 | |||
|
r43347 | ui.write(b' (') | ||
|
r43346 | ui.write( | ||
|
r43347 | _(b'%d commits ahead') % revdistance[rev], | ||
label=b'stack.commitdistance', | ||||
|
r43346 | ) | ||
|
r33194 | |||
if haverebase: | ||||
# TODO may be able to omit --source in some scenarios | ||||
|
r43347 | ui.write(b'; ') | ||
|
r43346 | ui.write( | ||
( | ||||
|
r43347 | b'hg rebase --source %s --dest %s' | ||
|
r43346 | % (shortest(sourcectx), shortest(ctx)) | ||
), | ||||
|
r43347 | label=b'stack.rebasehint', | ||
|
r43346 | ) | ||
|
r33194 | |||
|
r43347 | ui.write(b')\n') | ||
|
r33194 | |||
|
r43347 | ui.write(b':\n: ') | ||
ui.write(_(b'(stack head)\n'), label=b'stack.label') | ||||
|
r33194 | |||
if branchpointattip: | ||||
|
r43347 | ui.write(b' \\ / ') | ||
ui.write(_(b'(multiple children)\n'), label=b'stack.label') | ||||
ui.write(b' |\n') | ||||
|
r33194 | |||
for rev in stackrevs: | ||||
ctx = repo[rev] | ||||
|
r43347 | symbol = b'@' if rev == wdirctx.rev() else b'o' | ||
|
r33194 | |||
if newheads: | ||||
|
r43347 | ui.write(b': ') | ||
|
r33194 | else: | ||
|
r43347 | ui.write(b' ') | ||
|
r33194 | |||
|
r43347 | ui.write(symbol, b' ') | ||
|
r34192 | displayer.show(ctx, nodelen=nodelen) | ||
|
r33194 | displayer.flush(ctx) | ||
|
r43347 | ui.write(b'\n') | ||
|
r33194 | |||
# TODO display histedit hint? | ||||
if basectx: | ||||
# Vertically and horizontally separate stack base from parent | ||||
# to reinforce stack boundary. | ||||
if newheads: | ||||
|
r43347 | ui.write(b':/ ') | ||
|
r33194 | else: | ||
|
r43347 | ui.write(b' / ') | ||
|
r33194 | |||
|
r43347 | ui.write(_(b'(stack base)'), b'\n', label=b'stack.label') | ||
ui.write(b'o ') | ||||
|
r33194 | |||
|
r34192 | displayer.show(basectx, nodelen=nodelen) | ||
|
r33194 | displayer.flush(basectx) | ||
|
r43347 | ui.write(b'\n') | ||
|
r33194 | |||
|
r43346 | |||
|
r43347 | @revsetpredicate(b'_underway([commitage[, headage]])') | ||
|
r31944 | def underwayrevset(repo, subset, x): | ||
|
r43347 | args = revset.getargsdict(x, b'underway', b'commitage headage') | ||
if b'commitage' not in args: | ||||
args[b'commitage'] = None | ||||
if b'headage' not in args: | ||||
args[b'headage'] = None | ||||
|
r31944 | |||
# We assume callers of this revset add a topographical sort on the | ||||
# result. This means there is no benefit to making the revset lazy | ||||
# since the topographical sort needs to consume all revs. | ||||
# | ||||
# With this in mind, we build up the set manually instead of constructing | ||||
# a complex revset. This enables faster execution. | ||||
# Mutable changesets (non-public) are the most important changesets | ||||
# to return. ``not public()`` will also pull in obsolete changesets if | ||||
# there is a non-obsolete changeset with obsolete ancestors. This is | ||||
# why we exclude obsolete changesets from this query. | ||||
|
r43347 | rs = b'not public() and not obsolete()' | ||
|
r31944 | rsargs = [] | ||
|
r43347 | if args[b'commitage']: | ||
rs += b' and date(%s)' | ||||
|
r43346 | rsargs.append( | ||
revsetlang.getstring( | ||||
|
r43347 | args[b'commitage'], _(b'commitage requires a string') | ||
|
r43346 | ) | ||
) | ||||
|
r31944 | |||
mutable = repo.revs(rs, *rsargs) | ||||
relevant = revset.baseset(mutable) | ||||
# Add parents of mutable changesets to provide context. | ||||
|
r43347 | relevant += repo.revs(b'parents(%ld)', mutable) | ||
|
r31944 | |||
# We also pull in (public) heads if they a) aren't closing a branch | ||||
# b) are recent. | ||||
|
r43347 | rs = b'head() and not closed()' | ||
|
r31944 | rsargs = [] | ||
|
r43347 | if args[b'headage']: | ||
rs += b' and date(%s)' | ||||
|
r43346 | rsargs.append( | ||
revsetlang.getstring( | ||||
|
r43347 | args[b'headage'], _(b'headage requires a string') | ||
|
r43346 | ) | ||
) | ||||
|
r31944 | |||
relevant += repo.revs(rs, *rsargs) | ||||
# Add working directory parent. | ||||
|
r43347 | wdirrev = repo[b'.'].rev() | ||
|
r31944 | if wdirrev != nullrev: | ||
|
r32291 | relevant += revset.baseset({wdirrev}) | ||
|
r31944 | |||
return subset & relevant | ||||
|
r43346 | |||
|
r43347 | @showview(b'work', csettopic=b'work') | ||
|
r33046 | def showwork(ui, repo, displayer): | ||
|
r31944 | """changesets that aren't finished""" | ||
# TODO support date-based limiting when calling revset. | ||||
|
r43347 | revs = repo.revs(b'sort(_underway(), topo)') | ||
|
r34192 | nodelen = longestshortest(repo, revs) | ||
|
r31944 | |||
revdag = graphmod.dagwalker(repo, revs) | ||||
|
r43347 | ui.setconfig(b'experimental', b'graphshorten', True) | ||
|
r43346 | logcmdutil.displaygraph( | ||
ui, | ||||
repo, | ||||
revdag, | ||||
displayer, | ||||
graphmod.asciiedges, | ||||
|
r43347 | props={b'nodelen': nodelen}, | ||
|
r43346 | ) | ||
|
r31944 | |||
|
r33132 | def extsetup(ui): | ||
# Alias `hg <prefix><view>` to `hg show <view>`. | ||||
|
r43347 | for prefix in ui.configlist(b'commands', b'show.aliasprefix'): | ||
|
r33132 | for view in showview._table: | ||
|
r43347 | name = b'%s%s' % (prefix, view) | ||
|
r33132 | |||
|
r43346 | choice, allcommands = cmdutil.findpossible( | ||
name, commands.table, strict=True | ||||
) | ||||
|
r33132 | |||
# This alias is already a command name. Don't set it. | ||||
if name in choice: | ||||
continue | ||||
# Same for aliases. | ||||
|
r43347 | if ui.config(b'alias', name, None): | ||
|
r33132 | continue | ||
|
r43347 | ui.setconfig(b'alias', name, b'show %s' % view, source=b'show') | ||
|
r33132 | |||
|
r43346 | |||
|
r34192 | def longestshortest(repo, revs, minlen=4): | ||
"""Return the length of the longest shortest node to identify revisions. | ||||
The result of this function can be used with the ``shortest()`` template | ||||
function to ensure that a value is unique and unambiguous for a given | ||||
set of nodes. | ||||
The number of revisions in the repo is taken into account to prevent | ||||
a numeric node prefix from conflicting with an integer revision number. | ||||
If we fail to do this, a value of e.g. ``10023`` could mean either | ||||
revision 10023 or node ``10023abc...``. | ||||
""" | ||||
|
r35514 | if not revs: | ||
return minlen | ||||
|
r37698 | cl = repo.changelog | ||
|
r43346 | return max( | ||
len(scmutil.shortesthexnodeidprefix(repo, cl.node(r), minlen)) | ||||
for r in revs | ||||
) | ||||
|
r34192 | |||
|
r31765 | # Adjust the docstring of the show command so it shows all registered views. | ||
# This is a bit hacky because it runs at the end of module load. When moved | ||||
# into core or when another extension wants to provide a view, we'll need | ||||
# to do this more robustly. | ||||
# TODO make this more robust. | ||||
|
r31943 | def _updatedocstring(): | ||
longest = max(map(len, showview._table.keys())) | ||||
entries = [] | ||||
for key in sorted(showview._table.keys()): | ||||
|
r43346 | entries.append( | ||
r' %s %s' | ||||
% ( | ||||
pycompat.sysstr(key.ljust(longest)), | ||||
showview._table[key]._origdoc, | ||||
) | ||||
) | ||||
|
r31943 | |||
|
r43347 | cmdtable[b'show'][0].__doc__ = pycompat.sysstr(b'%s\n\n%s\n ') % ( | ||
cmdtable[b'show'][0].__doc__.rstrip(), | ||||
pycompat.sysstr(b'\n\n').join(entries), | ||||
|
r43346 | ) | ||
|
r31943 | |||
_updatedocstring() | ||||