hgext/fastannotate/commands.py
changeset 39210 1ddb296e0dee
child 39406 b3572f733dbd
equal deleted inserted replaced
39209:1af95139e5ec 39210:1ddb296e0dee
       
     1 # Copyright 2016-present Facebook. All Rights Reserved.
       
     2 #
       
     3 # commands: fastannotate commands
       
     4 #
       
     5 # This software may be used and distributed according to the terms of the
       
     6 # GNU General Public License version 2 or any later version.
       
     7 
       
     8 from __future__ import absolute_import
       
     9 
       
    10 import os
       
    11 
       
    12 from mercurial.i18n import _
       
    13 from mercurial import (
       
    14     commands,
       
    15     error,
       
    16     extensions,
       
    17     patch,
       
    18     pycompat,
       
    19     registrar,
       
    20     scmutil,
       
    21     util,
       
    22 )
       
    23 
       
    24 from . import (
       
    25     context as facontext,
       
    26     error as faerror,
       
    27     formatter as faformatter,
       
    28 )
       
    29 
       
    30 cmdtable = {}
       
    31 command = registrar.command(cmdtable)
       
    32 
       
    33 def _matchpaths(repo, rev, pats, opts, aopts=facontext.defaultopts):
       
    34     """generate paths matching given patterns"""
       
    35     perfhack = repo.ui.configbool('fastannotate', 'perfhack')
       
    36 
       
    37     # disable perfhack if:
       
    38     # a) any walkopt is used
       
    39     # b) if we treat pats as plain file names, some of them do not have
       
    40     #    corresponding linelog files
       
    41     if perfhack:
       
    42         # cwd related to reporoot
       
    43         reporoot = os.path.dirname(repo.path)
       
    44         reldir = os.path.relpath(pycompat.getcwd(), reporoot)
       
    45         if reldir == '.':
       
    46             reldir = ''
       
    47         if any(opts.get(o[1]) for o in commands.walkopts): # a)
       
    48             perfhack = False
       
    49         else: # b)
       
    50             relpats = [os.path.relpath(p, reporoot) if os.path.isabs(p) else p
       
    51                        for p in pats]
       
    52             # disable perfhack on '..' since it allows escaping from the repo
       
    53             if any(('..' in f or
       
    54                     not os.path.isfile(
       
    55                         facontext.pathhelper(repo, f, aopts).linelogpath))
       
    56                    for f in relpats):
       
    57                 perfhack = False
       
    58 
       
    59     # perfhack: emit paths directory without checking with manifest
       
    60     # this can be incorrect if the rev dos not have file.
       
    61     if perfhack:
       
    62         for p in relpats:
       
    63             yield os.path.join(reldir, p)
       
    64     else:
       
    65         def bad(x, y):
       
    66             raise error.Abort("%s: %s" % (x, y))
       
    67         ctx = scmutil.revsingle(repo, rev)
       
    68         m = scmutil.match(ctx, pats, opts, badfn=bad)
       
    69         for p in ctx.walk(m):
       
    70             yield p
       
    71 
       
    72 fastannotatecommandargs = {
       
    73     'options': [
       
    74         ('r', 'rev', '.', _('annotate the specified revision'), _('REV')),
       
    75         ('u', 'user', None, _('list the author (long with -v)')),
       
    76         ('f', 'file', None, _('list the filename')),
       
    77         ('d', 'date', None, _('list the date (short with -q)')),
       
    78         ('n', 'number', None, _('list the revision number (default)')),
       
    79         ('c', 'changeset', None, _('list the changeset')),
       
    80         ('l', 'line-number', None, _('show line number at the first '
       
    81                                      'appearance')),
       
    82         ('e', 'deleted', None, _('show deleted lines (slow) (EXPERIMENTAL)')),
       
    83         ('', 'no-content', None, _('do not show file content (EXPERIMENTAL)')),
       
    84         ('', 'no-follow', None, _("don't follow copies and renames")),
       
    85         ('', 'linear', None, _('enforce linear history, ignore second parent '
       
    86                                'of merges (EXPERIMENTAL)')),
       
    87         ('', 'long-hash', None, _('show long changeset hash (EXPERIMENTAL)')),
       
    88         ('', 'rebuild', None, _('rebuild cache even if it exists '
       
    89                                 '(EXPERIMENTAL)')),
       
    90     ] + commands.diffwsopts + commands.walkopts + commands.formatteropts,
       
    91     'synopsis': _('[-r REV] [-f] [-a] [-u] [-d] [-n] [-c] [-l] FILE...'),
       
    92     'inferrepo': True,
       
    93 }
       
    94 
       
    95 def fastannotate(ui, repo, *pats, **opts):
       
    96     """show changeset information by line for each file
       
    97 
       
    98     List changes in files, showing the revision id responsible for each line.
       
    99 
       
   100     This command is useful for discovering when a change was made and by whom.
       
   101 
       
   102     By default this command prints revision numbers. If you include --file,
       
   103     --user, or --date, the revision number is suppressed unless you also
       
   104     include --number. The default format can also be customized by setting
       
   105     fastannotate.defaultformat.
       
   106 
       
   107     Returns 0 on success.
       
   108 
       
   109     .. container:: verbose
       
   110 
       
   111         This command uses an implementation different from the vanilla annotate
       
   112         command, which may produce slightly different (while still reasonable)
       
   113         outputs for some cases.
       
   114 
       
   115         Unlike the vanilla anootate, fastannotate follows rename regardless of
       
   116         the existence of --file.
       
   117 
       
   118         For the best performance when running on a full repo, use -c, -l,
       
   119         avoid -u, -d, -n. Use --linear and --no-content to make it even faster.
       
   120 
       
   121         For the best performance when running on a shallow (remotefilelog)
       
   122         repo, avoid --linear, --no-follow, or any diff options. As the server
       
   123         won't be able to populate annotate cache when non-default options
       
   124         affecting results are used.
       
   125     """
       
   126     if not pats:
       
   127         raise error.Abort(_('at least one filename or pattern is required'))
       
   128 
       
   129     # performance hack: filtered repo can be slow. unfilter by default.
       
   130     if ui.configbool('fastannotate', 'unfilteredrepo'):
       
   131         repo = repo.unfiltered()
       
   132 
       
   133     rev = opts.get('rev', '.')
       
   134     rebuild = opts.get('rebuild', False)
       
   135 
       
   136     diffopts = patch.difffeatureopts(ui, opts, section='annotate',
       
   137                                      whitespace=True)
       
   138     aopts = facontext.annotateopts(
       
   139         diffopts=diffopts,
       
   140         followmerge=not opts.get('linear', False),
       
   141         followrename=not opts.get('no_follow', False))
       
   142 
       
   143     if not any(opts.get(s)
       
   144                for s in ['user', 'date', 'file', 'number', 'changeset']):
       
   145         # default 'number' for compatibility. but fastannotate is more
       
   146         # efficient with "changeset", "line-number" and "no-content".
       
   147         for name in ui.configlist('fastannotate', 'defaultformat', ['number']):
       
   148             opts[name] = True
       
   149 
       
   150     ui.pager('fastannotate')
       
   151     template = opts.get('template')
       
   152     if template == 'json':
       
   153         formatter = faformatter.jsonformatter(ui, repo, opts)
       
   154     else:
       
   155         formatter = faformatter.defaultformatter(ui, repo, opts)
       
   156     showdeleted = opts.get('deleted', False)
       
   157     showlines = not bool(opts.get('no_content'))
       
   158     showpath = opts.get('file', False)
       
   159 
       
   160     # find the head of the main (master) branch
       
   161     master = ui.config('fastannotate', 'mainbranch') or rev
       
   162 
       
   163     # paths will be used for prefetching and the real annotating
       
   164     paths = list(_matchpaths(repo, rev, pats, opts, aopts))
       
   165 
       
   166     # for client, prefetch from the server
       
   167     if util.safehasattr(repo, 'prefetchfastannotate'):
       
   168         repo.prefetchfastannotate(paths)
       
   169 
       
   170     for path in paths:
       
   171         result = lines = existinglines = None
       
   172         while True:
       
   173             try:
       
   174                 with facontext.annotatecontext(repo, path, aopts, rebuild) as a:
       
   175                     result = a.annotate(rev, master=master, showpath=showpath,
       
   176                                         showlines=(showlines and
       
   177                                                    not showdeleted))
       
   178                     if showdeleted:
       
   179                         existinglines = set((l[0], l[1]) for l in result)
       
   180                         result = a.annotatealllines(
       
   181                             rev, showpath=showpath, showlines=showlines)
       
   182                 break
       
   183             except (faerror.CannotReuseError, faerror.CorruptedFileError):
       
   184                 # happens if master moves backwards, or the file was deleted
       
   185                 # and readded, or renamed to an existing name, or corrupted.
       
   186                 if rebuild: # give up since we have tried rebuild already
       
   187                     raise
       
   188                 else: # try a second time rebuilding the cache (slow)
       
   189                     rebuild = True
       
   190                     continue
       
   191 
       
   192         if showlines:
       
   193             result, lines = result
       
   194 
       
   195         formatter.write(result, lines, existinglines=existinglines)
       
   196     formatter.end()
       
   197 
       
   198 _newopts = set([])
       
   199 _knownopts = set([opt[1].replace('-', '_') for opt in
       
   200                   (fastannotatecommandargs['options'] + commands.globalopts)])
       
   201 
       
   202 def _annotatewrapper(orig, ui, repo, *pats, **opts):
       
   203     """used by wrapdefault"""
       
   204     # we need this hack until the obsstore has 0.0 seconds perf impact
       
   205     if ui.configbool('fastannotate', 'unfilteredrepo'):
       
   206         repo = repo.unfiltered()
       
   207 
       
   208     # treat the file as text (skip the isbinary check)
       
   209     if ui.configbool('fastannotate', 'forcetext'):
       
   210         opts['text'] = True
       
   211 
       
   212     # check if we need to do prefetch (client-side)
       
   213     rev = opts.get('rev')
       
   214     if util.safehasattr(repo, 'prefetchfastannotate') and rev is not None:
       
   215         paths = list(_matchpaths(repo, rev, pats, opts))
       
   216         repo.prefetchfastannotate(paths)
       
   217 
       
   218     return orig(ui, repo, *pats, **opts)
       
   219 
       
   220 def registercommand():
       
   221     """register the fastannotate command"""
       
   222     name = '^fastannotate|fastblame|fa'
       
   223     command(name, **fastannotatecommandargs)(fastannotate)
       
   224 
       
   225 def wrapdefault():
       
   226     """wrap the default annotate command, to be aware of the protocol"""
       
   227     extensions.wrapcommand(commands.table, 'annotate', _annotatewrapper)
       
   228 
       
   229 @command('debugbuildannotatecache',
       
   230          [('r', 'rev', '', _('build up to the specific revision'), _('REV'))
       
   231          ] + commands.walkopts,
       
   232          _('[-r REV] FILE...'))
       
   233 def debugbuildannotatecache(ui, repo, *pats, **opts):
       
   234     """incrementally build fastannotate cache up to REV for specified files
       
   235 
       
   236     If REV is not specified, use the config 'fastannotate.mainbranch'.
       
   237 
       
   238     If fastannotate.client is True, download the annotate cache from the
       
   239     server. Otherwise, build the annotate cache locally.
       
   240 
       
   241     The annotate cache will be built using the default diff and follow
       
   242     options and lives in '.hg/fastannotate/default'.
       
   243     """
       
   244     rev = opts.get('REV') or ui.config('fastannotate', 'mainbranch')
       
   245     if not rev:
       
   246         raise error.Abort(_('you need to provide a revision'),
       
   247                           hint=_('set fastannotate.mainbranch or use --rev'))
       
   248     if ui.configbool('fastannotate', 'unfilteredrepo'):
       
   249         repo = repo.unfiltered()
       
   250     ctx = scmutil.revsingle(repo, rev)
       
   251     m = scmutil.match(ctx, pats, opts)
       
   252     paths = list(ctx.walk(m))
       
   253     if util.safehasattr(repo, 'prefetchfastannotate'):
       
   254         # client
       
   255         if opts.get('REV'):
       
   256             raise error.Abort(_('--rev cannot be used for client'))
       
   257         repo.prefetchfastannotate(paths)
       
   258     else:
       
   259         # server, or full repo
       
   260         for i, path in enumerate(paths):
       
   261             ui.progress(_('building'), i, total=len(paths))
       
   262             with facontext.annotatecontext(repo, path) as actx:
       
   263                 try:
       
   264                     if actx.isuptodate(rev):
       
   265                         continue
       
   266                     actx.annotate(rev, rev)
       
   267                 except (faerror.CannotReuseError, faerror.CorruptedFileError):
       
   268                     # the cache is broken (could happen with renaming so the
       
   269                     # file history gets invalidated). rebuild and try again.
       
   270                     ui.debug('fastannotate: %s: rebuilding broken cache\n'
       
   271                              % path)
       
   272                     actx.rebuild()
       
   273                     try:
       
   274                         actx.annotate(rev, rev)
       
   275                     except Exception as ex:
       
   276                         # possibly a bug, but should not stop us from building
       
   277                         # cache for other files.
       
   278                         ui.warn(_('fastannotate: %s: failed to '
       
   279                                   'build cache: %r\n') % (path, ex))
       
   280         # clear the progress bar
       
   281         ui.write()