Mercurial > public > mercurial-scm > hg
comparison hgext/fastannotate/commands.py @ 39210:1ddb296e0dee
fastannotate: initial import from Facebook's hg-experimental
I made as few changes as I could to get the tests to pass, but this
was a bit involved due to some churn in the blame code since someone
last gave fastannotate any TLC.
There's still follow-up work here to rip out support for old versions
of hg and to integrate the protocol with modern standards.
Some performance numbers (all on my 2016 MacBook Pro with a 2.6Ghz i7):
Mercurial mercurial/manifest.py
traditional blame
time: real 1.050 secs (user 0.990+0.000 sys 0.060+0.000)
build cache
time: real 5.900 secs (user 5.720+0.000 sys 0.110+0.000)
fastannotate
time: real 0.120 secs (user 0.100+0.000 sys 0.020+0.000)
Mercurial mercurial/localrepo.py
traditional blame
time: real 3.330 secs (user 3.220+0.000 sys 0.070+0.000)
build cache
time: real 30.610 secs (user 30.190+0.000 sys 0.230+0.000)
fastannotate
time: real 0.180 secs (user 0.160+0.000 sys 0.020+0.000)
mozilla-central dom/ipc/ContentParent.cpp
traditional blame
time: real 7.640 secs (user 7.210+0.000 sys 0.380+0.000)
build cache
time: real 98.650 secs (user 97.000+0.000 sys 0.950+0.000)
fastannotate
time: real 1.580 secs (user 1.340+0.000 sys 0.240+0.000)
mozilla-central dom/base/nsDocument.cpp
traditional blame
time: real 17.110 secs (user 16.490+0.000 sys 0.500+0.000)
build cache
time: real 399.750 secs (user 394.520+0.000 sys 2.610+0.000)
fastannotate
time: real 1.780 secs (user 1.530+0.000 sys 0.240+0.000)
So building the cache is expensive (but might be faster with xdiff
enabled), but the blame results are *way* faster.
Differential Revision: https://phab.mercurial-scm.org/D3994
author | Augie Fackler <augie@google.com> |
---|---|
date | Mon, 30 Jul 2018 22:50:00 -0400 |
parents | |
children | b3572f733dbd |
comparison
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() |