commands.py
358 lines
| 11.9 KiB
| text/x-python
|
PythonLexer
|
r39243 | # Copyright 2016-present Facebook. All Rights Reserved. | ||
# | ||||
# commands: fastannotate commands | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
from __future__ import absolute_import | ||||
import os | ||||
from mercurial.i18n import _ | ||||
from mercurial import ( | ||||
commands, | ||||
|
r39843 | encoding, | ||
|
r39243 | error, | ||
extensions, | ||||
|
r48930 | logcmdutil, | ||
|
r39243 | patch, | ||
pycompat, | ||||
registrar, | ||||
scmutil, | ||||
util, | ||||
) | ||||
from . import ( | ||||
context as facontext, | ||||
error as faerror, | ||||
formatter as faformatter, | ||||
) | ||||
cmdtable = {} | ||||
command = registrar.command(cmdtable) | ||||
|
r43346 | |||
|
r39243 | def _matchpaths(repo, rev, pats, opts, aopts=facontext.defaultopts): | ||
"""generate paths matching given patterns""" | ||||
|
r43347 | perfhack = repo.ui.configbool(b'fastannotate', b'perfhack') | ||
|
r39243 | |||
# disable perfhack if: | ||||
# a) any walkopt is used | ||||
# b) if we treat pats as plain file names, some of them do not have | ||||
# corresponding linelog files | ||||
if perfhack: | ||||
# cwd related to reporoot | ||||
reporoot = os.path.dirname(repo.path) | ||||
|
r39843 | reldir = os.path.relpath(encoding.getcwd(), reporoot) | ||
|
r43347 | if reldir == b'.': | ||
reldir = b'' | ||||
|
r43346 | if any(opts.get(o[1]) for o in commands.walkopts): # a) | ||
|
r39243 | perfhack = False | ||
|
r43346 | else: # b) | ||
relpats = [ | ||||
os.path.relpath(p, reporoot) if os.path.isabs(p) else p | ||||
for p in pats | ||||
] | ||||
|
r39243 | # disable perfhack on '..' since it allows escaping from the repo | ||
|
r43346 | if any( | ||
( | ||||
|
r43347 | b'..' in f | ||
|
r43346 | or not os.path.isfile( | ||
facontext.pathhelper(repo, f, aopts).linelogpath | ||||
) | ||||
) | ||||
for f in relpats | ||||
): | ||||
|
r39243 | perfhack = False | ||
# perfhack: emit paths directory without checking with manifest | ||||
# this can be incorrect if the rev dos not have file. | ||||
if perfhack: | ||||
for p in relpats: | ||||
yield os.path.join(reldir, p) | ||||
else: | ||||
|
r43346 | |||
|
r39243 | def bad(x, y): | ||
|
r43347 | raise error.Abort(b"%s: %s" % (x, y)) | ||
|
r43346 | |||
|
r48930 | ctx = logcmdutil.revsingle(repo, rev) | ||
|
r39243 | m = scmutil.match(ctx, pats, opts, badfn=bad) | ||
for p in ctx.walk(m): | ||||
yield p | ||||
|
r43346 | |||
|
r39243 | fastannotatecommandargs = { | ||
|
r43906 | 'options': [ | ||
|
r43347 | (b'r', b'rev', b'.', _(b'annotate the specified revision'), _(b'REV')), | ||
(b'u', b'user', None, _(b'list the author (long with -v)')), | ||||
(b'f', b'file', None, _(b'list the filename')), | ||||
(b'd', b'date', None, _(b'list the date (short with -q)')), | ||||
(b'n', b'number', None, _(b'list the revision number (default)')), | ||||
(b'c', b'changeset', None, _(b'list the changeset')), | ||||
( | ||||
b'l', | ||||
b'line-number', | ||||
None, | ||||
|
r43387 | _(b'show line number at the first appearance'), | ||
|
r43347 | ), | ||
|
r43346 | ( | ||
|
r43347 | b'e', | ||
b'deleted', | ||||
|
r43346 | None, | ||
|
r43347 | _(b'show deleted lines (slow) (EXPERIMENTAL)'), | ||
|
r43346 | ), | ||
( | ||||
|
r43347 | b'', | ||
b'no-content', | ||||
None, | ||||
_(b'do not show file content (EXPERIMENTAL)'), | ||||
), | ||||
(b'', b'no-follow', None, _(b"don't follow copies and renames")), | ||||
( | ||||
b'', | ||||
b'linear', | ||||
|
r43346 | None, | ||
_( | ||||
|
r43347 | b'enforce linear history, ignore second parent ' | ||
b'of merges (EXPERIMENTAL)' | ||||
|
r43346 | ), | ||
), | ||||
( | ||||
|
r43347 | b'', | ||
b'long-hash', | ||||
|
r43346 | None, | ||
|
r43347 | _(b'show long changeset hash (EXPERIMENTAL)'), | ||
), | ||||
( | ||||
b'', | ||||
b'rebuild', | ||||
None, | ||||
|
r43387 | _(b'rebuild cache even if it exists (EXPERIMENTAL)'), | ||
|
r43346 | ), | ||
] | ||||
+ commands.diffwsopts | ||||
+ commands.walkopts | ||||
+ commands.formatteropts, | ||||
|
r43906 | 'synopsis': _(b'[-r REV] [-f] [-a] [-u] [-d] [-n] [-c] [-l] FILE...'), | ||
'inferrepo': True, | ||||
|
r39243 | } | ||
|
r43346 | |||
|
r39243 | def fastannotate(ui, repo, *pats, **opts): | ||
"""show changeset information by line for each file | ||||
List changes in files, showing the revision id responsible for each line. | ||||
This command is useful for discovering when a change was made and by whom. | ||||
By default this command prints revision numbers. If you include --file, | ||||
--user, or --date, the revision number is suppressed unless you also | ||||
include --number. The default format can also be customized by setting | ||||
fastannotate.defaultformat. | ||||
Returns 0 on success. | ||||
.. container:: verbose | ||||
This command uses an implementation different from the vanilla annotate | ||||
command, which may produce slightly different (while still reasonable) | ||||
outputs for some cases. | ||||
Unlike the vanilla anootate, fastannotate follows rename regardless of | ||||
the existence of --file. | ||||
For the best performance when running on a full repo, use -c, -l, | ||||
avoid -u, -d, -n. Use --linear and --no-content to make it even faster. | ||||
For the best performance when running on a shallow (remotefilelog) | ||||
repo, avoid --linear, --no-follow, or any diff options. As the server | ||||
won't be able to populate annotate cache when non-default options | ||||
affecting results are used. | ||||
""" | ||||
if not pats: | ||||
|
r43347 | raise error.Abort(_(b'at least one filename or pattern is required')) | ||
|
r39243 | |||
# performance hack: filtered repo can be slow. unfilter by default. | ||||
|
r43347 | if ui.configbool(b'fastannotate', b'unfilteredrepo'): | ||
|
r39243 | repo = repo.unfiltered() | ||
|
r39702 | opts = pycompat.byteskwargs(opts) | ||
|
r43347 | rev = opts.get(b'rev', b'.') | ||
rebuild = opts.get(b'rebuild', False) | ||||
|
r39243 | |||
|
r43346 | diffopts = patch.difffeatureopts( | ||
|
r43347 | ui, opts, section=b'annotate', whitespace=True | ||
|
r43346 | ) | ||
|
r39243 | aopts = facontext.annotateopts( | ||
diffopts=diffopts, | ||||
|
r43347 | followmerge=not opts.get(b'linear', False), | ||
followrename=not opts.get(b'no_follow', False), | ||||
|
r43346 | ) | ||
|
r39243 | |||
|
r43346 | if not any( | ||
|
r43347 | opts.get(s) | ||
for s in [b'user', b'date', b'file', b'number', b'changeset'] | ||||
|
r43346 | ): | ||
|
r39243 | # default 'number' for compatibility. but fastannotate is more | ||
# efficient with "changeset", "line-number" and "no-content". | ||||
|
r43347 | for name in ui.configlist( | ||
b'fastannotate', b'defaultformat', [b'number'] | ||||
): | ||||
|
r39243 | opts[name] = True | ||
|
r43347 | ui.pager(b'fastannotate') | ||
template = opts.get(b'template') | ||||
if template == b'json': | ||||
|
r39243 | formatter = faformatter.jsonformatter(ui, repo, opts) | ||
else: | ||||
formatter = faformatter.defaultformatter(ui, repo, opts) | ||||
|
r43347 | showdeleted = opts.get(b'deleted', False) | ||
showlines = not bool(opts.get(b'no_content')) | ||||
showpath = opts.get(b'file', False) | ||||
|
r39243 | |||
# find the head of the main (master) branch | ||||
|
r43347 | master = ui.config(b'fastannotate', b'mainbranch') or rev | ||
|
r39243 | |||
# paths will be used for prefetching and the real annotating | ||||
paths = list(_matchpaths(repo, rev, pats, opts, aopts)) | ||||
# for client, prefetch from the server | ||||
|
r43385 | if util.safehasattr(repo, 'prefetchfastannotate'): | ||
|
r39243 | repo.prefetchfastannotate(paths) | ||
for path in paths: | ||||
result = lines = existinglines = None | ||||
while True: | ||||
try: | ||||
with facontext.annotatecontext(repo, path, aopts, rebuild) as a: | ||||
|
r43346 | result = a.annotate( | ||
rev, | ||||
master=master, | ||||
showpath=showpath, | ||||
showlines=(showlines and not showdeleted), | ||||
) | ||||
|
r39243 | if showdeleted: | ||
|
r44937 | existinglines = {(l[0], l[1]) for l in result} | ||
|
r39243 | result = a.annotatealllines( | ||
|
r43346 | rev, showpath=showpath, showlines=showlines | ||
) | ||||
|
r39243 | break | ||
except (faerror.CannotReuseError, faerror.CorruptedFileError): | ||||
# happens if master moves backwards, or the file was deleted | ||||
# and readded, or renamed to an existing name, or corrupted. | ||||
|
r43346 | if rebuild: # give up since we have tried rebuild already | ||
|
r39243 | raise | ||
|
r43346 | else: # try a second time rebuilding the cache (slow) | ||
|
r39243 | rebuild = True | ||
continue | ||||
if showlines: | ||||
result, lines = result | ||||
formatter.write(result, lines, existinglines=existinglines) | ||||
formatter.end() | ||||
|
r43346 | |||
|
r42224 | _newopts = set() | ||
|
r43346 | _knownopts = { | ||
|
r43347 | opt[1].replace(b'-', b'_') | ||
|
r43906 | for opt in (fastannotatecommandargs['options'] + commands.globalopts) | ||
|
r43346 | } | ||
|
r39243 | |||
def _annotatewrapper(orig, ui, repo, *pats, **opts): | ||||
"""used by wrapdefault""" | ||||
# we need this hack until the obsstore has 0.0 seconds perf impact | ||||
|
r43347 | if ui.configbool(b'fastannotate', b'unfilteredrepo'): | ||
|
r39243 | repo = repo.unfiltered() | ||
# treat the file as text (skip the isbinary check) | ||||
|
r43347 | if ui.configbool(b'fastannotate', b'forcetext'): | ||
|
r43906 | opts['text'] = True | ||
|
r39243 | |||
# check if we need to do prefetch (client-side) | ||||
|
r43906 | rev = opts.get('rev') | ||
|
r43385 | if util.safehasattr(repo, 'prefetchfastannotate') and rev is not None: | ||
|
r39702 | paths = list(_matchpaths(repo, rev, pats, pycompat.byteskwargs(opts))) | ||
|
r39243 | repo.prefetchfastannotate(paths) | ||
return orig(ui, repo, *pats, **opts) | ||||
|
r43346 | |||
|
r39243 | def registercommand(): | ||
"""register the fastannotate command""" | ||||
|
r43347 | name = b'fastannotate|fastblame|fa' | ||
|
r40331 | command(name, helpbasic=True, **fastannotatecommandargs)(fastannotate) | ||
|
r39243 | |||
|
r43346 | |||
|
r39243 | def wrapdefault(): | ||
"""wrap the default annotate command, to be aware of the protocol""" | ||||
|
r43347 | extensions.wrapcommand(commands.table, b'annotate', _annotatewrapper) | ||
|
r39243 | |||
|
r43346 | |||
@command( | ||||
|
r43347 | b'debugbuildannotatecache', | ||
[(b'r', b'rev', b'', _(b'build up to the specific revision'), _(b'REV'))] | ||||
|
r43346 | + commands.walkopts, | ||
|
r43347 | _(b'[-r REV] FILE...'), | ||
|
r43346 | ) | ||
|
r39243 | def debugbuildannotatecache(ui, repo, *pats, **opts): | ||
"""incrementally build fastannotate cache up to REV for specified files | ||||
If REV is not specified, use the config 'fastannotate.mainbranch'. | ||||
If fastannotate.client is True, download the annotate cache from the | ||||
server. Otherwise, build the annotate cache locally. | ||||
The annotate cache will be built using the default diff and follow | ||||
options and lives in '.hg/fastannotate/default'. | ||||
""" | ||||
|
r39702 | opts = pycompat.byteskwargs(opts) | ||
|
r43347 | rev = opts.get(b'REV') or ui.config(b'fastannotate', b'mainbranch') | ||
|
r39243 | if not rev: | ||
|
r43346 | raise error.Abort( | ||
|
r43347 | _(b'you need to provide a revision'), | ||
hint=_(b'set fastannotate.mainbranch or use --rev'), | ||||
|
r43346 | ) | ||
|
r43347 | if ui.configbool(b'fastannotate', b'unfilteredrepo'): | ||
|
r39243 | repo = repo.unfiltered() | ||
|
r48930 | ctx = logcmdutil.revsingle(repo, rev) | ||
|
r39243 | m = scmutil.match(ctx, pats, opts) | ||
paths = list(ctx.walk(m)) | ||||
|
r43385 | if util.safehasattr(repo, 'prefetchfastannotate'): | ||
|
r39243 | # client | ||
|
r43347 | if opts.get(b'REV'): | ||
raise error.Abort(_(b'--rev cannot be used for client')) | ||||
|
r39243 | repo.prefetchfastannotate(paths) | ||
else: | ||||
# server, or full repo | ||||
|
r43347 | progress = ui.makeprogress(_(b'building'), total=len(paths)) | ||
|
r39243 | for i, path in enumerate(paths): | ||
|
r40873 | progress.update(i) | ||
|
r39243 | with facontext.annotatecontext(repo, path) as actx: | ||
try: | ||||
if actx.isuptodate(rev): | ||||
continue | ||||
actx.annotate(rev, rev) | ||||
except (faerror.CannotReuseError, faerror.CorruptedFileError): | ||||
# the cache is broken (could happen with renaming so the | ||||
# file history gets invalidated). rebuild and try again. | ||||
|
r43346 | ui.debug( | ||
|
r43347 | b'fastannotate: %s: rebuilding broken cache\n' % path | ||
|
r43346 | ) | ||
|
r39243 | actx.rebuild() | ||
try: | ||||
actx.annotate(rev, rev) | ||||
except Exception as ex: | ||||
# possibly a bug, but should not stop us from building | ||||
# cache for other files. | ||||
|
r43346 | ui.warn( | ||
_( | ||||
|
r43347 | b'fastannotate: %s: failed to ' | ||
b'build cache: %r\n' | ||||
|
r43346 | ) | ||
% (path, ex) | ||||
) | ||||
|
r40873 | progress.complete() | ||