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