##// END OF EJS Templates
typing: add some type hints to fastannotate that have decayed in the last year...
Matt Harbison -
r52607:45d5e9a0 default
parent child Browse files
Show More
@@ -1,356 +1,359 b''
1 # Copyright 2016-present Facebook. All Rights Reserved.
1 # Copyright 2016-present Facebook. All Rights Reserved.
2 #
2 #
3 # commands: fastannotate commands
3 # commands: fastannotate commands
4 #
4 #
5 # This software may be used and distributed according to the terms of the
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.
6 # GNU General Public License version 2 or any later version.
7
7
8
8
9 import os
9 import os
10 from typing import (
11 Set,
12 )
10
13
11 from mercurial.i18n import _
14 from mercurial.i18n import _
12 from mercurial import (
15 from mercurial import (
13 commands,
16 commands,
14 encoding,
17 encoding,
15 error,
18 error,
16 extensions,
19 extensions,
17 logcmdutil,
20 logcmdutil,
18 patch,
21 patch,
19 pycompat,
22 pycompat,
20 registrar,
23 registrar,
21 scmutil,
24 scmutil,
22 )
25 )
23
26
24 from . import (
27 from . import (
25 context as facontext,
28 context as facontext,
26 error as faerror,
29 error as faerror,
27 formatter as faformatter,
30 formatter as faformatter,
28 )
31 )
29
32
30 cmdtable = {}
33 cmdtable = {}
31 command = registrar.command(cmdtable)
34 command = registrar.command(cmdtable)
32
35
33
36
34 def _matchpaths(repo, rev, pats, opts, aopts=facontext.defaultopts):
37 def _matchpaths(repo, rev, pats, opts, aopts=facontext.defaultopts):
35 """generate paths matching given patterns"""
38 """generate paths matching given patterns"""
36 perfhack = repo.ui.configbool(b'fastannotate', b'perfhack')
39 perfhack = repo.ui.configbool(b'fastannotate', b'perfhack')
37
40
38 # disable perfhack if:
41 # disable perfhack if:
39 # a) any walkopt is used
42 # a) any walkopt is used
40 # b) if we treat pats as plain file names, some of them do not have
43 # b) if we treat pats as plain file names, some of them do not have
41 # corresponding linelog files
44 # corresponding linelog files
42 if perfhack:
45 if perfhack:
43 # cwd related to reporoot
46 # cwd related to reporoot
44 reporoot = os.path.dirname(repo.path)
47 reporoot = os.path.dirname(repo.path)
45 reldir = os.path.relpath(encoding.getcwd(), reporoot)
48 reldir = os.path.relpath(encoding.getcwd(), reporoot)
46 if reldir == b'.':
49 if reldir == b'.':
47 reldir = b''
50 reldir = b''
48 if any(opts.get(o[1]) for o in commands.walkopts): # a)
51 if any(opts.get(o[1]) for o in commands.walkopts): # a)
49 perfhack = False
52 perfhack = False
50 else: # b)
53 else: # b)
51 relpats = [
54 relpats = [
52 os.path.relpath(p, reporoot) if os.path.isabs(p) else p
55 os.path.relpath(p, reporoot) if os.path.isabs(p) else p
53 for p in pats
56 for p in pats
54 ]
57 ]
55 # disable perfhack on '..' since it allows escaping from the repo
58 # disable perfhack on '..' since it allows escaping from the repo
56 if any(
59 if any(
57 (
60 (
58 b'..' in f
61 b'..' in f
59 or not os.path.isfile(
62 or not os.path.isfile(
60 facontext.pathhelper(repo, f, aopts).linelogpath
63 facontext.pathhelper(repo, f, aopts).linelogpath
61 )
64 )
62 )
65 )
63 for f in relpats
66 for f in relpats
64 ):
67 ):
65 perfhack = False
68 perfhack = False
66
69
67 # perfhack: emit paths directory without checking with manifest
70 # perfhack: emit paths directory without checking with manifest
68 # this can be incorrect if the rev dos not have file.
71 # this can be incorrect if the rev dos not have file.
69 if perfhack:
72 if perfhack:
70 for p in relpats:
73 for p in relpats:
71 yield os.path.join(reldir, p)
74 yield os.path.join(reldir, p)
72 else:
75 else:
73
76
74 def bad(x, y):
77 def bad(x, y):
75 raise error.Abort(b"%s: %s" % (x, y))
78 raise error.Abort(b"%s: %s" % (x, y))
76
79
77 ctx = logcmdutil.revsingle(repo, rev)
80 ctx = logcmdutil.revsingle(repo, rev)
78 m = scmutil.match(ctx, pats, opts, badfn=bad)
81 m = scmutil.match(ctx, pats, opts, badfn=bad)
79 for p in ctx.walk(m):
82 for p in ctx.walk(m):
80 yield p
83 yield p
81
84
82
85
83 fastannotatecommandargs = {
86 fastannotatecommandargs = {
84 'options': [
87 'options': [
85 (b'r', b'rev', b'.', _(b'annotate the specified revision'), _(b'REV')),
88 (b'r', b'rev', b'.', _(b'annotate the specified revision'), _(b'REV')),
86 (b'u', b'user', None, _(b'list the author (long with -v)')),
89 (b'u', b'user', None, _(b'list the author (long with -v)')),
87 (b'f', b'file', None, _(b'list the filename')),
90 (b'f', b'file', None, _(b'list the filename')),
88 (b'd', b'date', None, _(b'list the date (short with -q)')),
91 (b'd', b'date', None, _(b'list the date (short with -q)')),
89 (b'n', b'number', None, _(b'list the revision number (default)')),
92 (b'n', b'number', None, _(b'list the revision number (default)')),
90 (b'c', b'changeset', None, _(b'list the changeset')),
93 (b'c', b'changeset', None, _(b'list the changeset')),
91 (
94 (
92 b'l',
95 b'l',
93 b'line-number',
96 b'line-number',
94 None,
97 None,
95 _(b'show line number at the first appearance'),
98 _(b'show line number at the first appearance'),
96 ),
99 ),
97 (
100 (
98 b'e',
101 b'e',
99 b'deleted',
102 b'deleted',
100 None,
103 None,
101 _(b'show deleted lines (slow) (EXPERIMENTAL)'),
104 _(b'show deleted lines (slow) (EXPERIMENTAL)'),
102 ),
105 ),
103 (
106 (
104 b'',
107 b'',
105 b'no-content',
108 b'no-content',
106 None,
109 None,
107 _(b'do not show file content (EXPERIMENTAL)'),
110 _(b'do not show file content (EXPERIMENTAL)'),
108 ),
111 ),
109 (b'', b'no-follow', None, _(b"don't follow copies and renames")),
112 (b'', b'no-follow', None, _(b"don't follow copies and renames")),
110 (
113 (
111 b'',
114 b'',
112 b'linear',
115 b'linear',
113 None,
116 None,
114 _(
117 _(
115 b'enforce linear history, ignore second parent '
118 b'enforce linear history, ignore second parent '
116 b'of merges (EXPERIMENTAL)'
119 b'of merges (EXPERIMENTAL)'
117 ),
120 ),
118 ),
121 ),
119 (
122 (
120 b'',
123 b'',
121 b'long-hash',
124 b'long-hash',
122 None,
125 None,
123 _(b'show long changeset hash (EXPERIMENTAL)'),
126 _(b'show long changeset hash (EXPERIMENTAL)'),
124 ),
127 ),
125 (
128 (
126 b'',
129 b'',
127 b'rebuild',
130 b'rebuild',
128 None,
131 None,
129 _(b'rebuild cache even if it exists (EXPERIMENTAL)'),
132 _(b'rebuild cache even if it exists (EXPERIMENTAL)'),
130 ),
133 ),
131 ]
134 ]
132 + commands.diffwsopts
135 + commands.diffwsopts
133 + commands.walkopts
136 + commands.walkopts
134 + commands.formatteropts,
137 + commands.formatteropts,
135 'synopsis': _(b'[-r REV] [-f] [-a] [-u] [-d] [-n] [-c] [-l] FILE...'),
138 'synopsis': _(b'[-r REV] [-f] [-a] [-u] [-d] [-n] [-c] [-l] FILE...'),
136 'inferrepo': True,
139 'inferrepo': True,
137 }
140 }
138
141
139
142
140 def fastannotate(ui, repo, *pats, **opts):
143 def fastannotate(ui, repo, *pats, **opts):
141 """show changeset information by line for each file
144 """show changeset information by line for each file
142
145
143 List changes in files, showing the revision id responsible for each line.
146 List changes in files, showing the revision id responsible for each line.
144
147
145 This command is useful for discovering when a change was made and by whom.
148 This command is useful for discovering when a change was made and by whom.
146
149
147 By default this command prints revision numbers. If you include --file,
150 By default this command prints revision numbers. If you include --file,
148 --user, or --date, the revision number is suppressed unless you also
151 --user, or --date, the revision number is suppressed unless you also
149 include --number. The default format can also be customized by setting
152 include --number. The default format can also be customized by setting
150 fastannotate.defaultformat.
153 fastannotate.defaultformat.
151
154
152 Returns 0 on success.
155 Returns 0 on success.
153
156
154 .. container:: verbose
157 .. container:: verbose
155
158
156 This command uses an implementation different from the vanilla annotate
159 This command uses an implementation different from the vanilla annotate
157 command, which may produce slightly different (while still reasonable)
160 command, which may produce slightly different (while still reasonable)
158 outputs for some cases.
161 outputs for some cases.
159
162
160 Unlike the vanilla anootate, fastannotate follows rename regardless of
163 Unlike the vanilla anootate, fastannotate follows rename regardless of
161 the existence of --file.
164 the existence of --file.
162
165
163 For the best performance when running on a full repo, use -c, -l,
166 For the best performance when running on a full repo, use -c, -l,
164 avoid -u, -d, -n. Use --linear and --no-content to make it even faster.
167 avoid -u, -d, -n. Use --linear and --no-content to make it even faster.
165
168
166 For the best performance when running on a shallow (remotefilelog)
169 For the best performance when running on a shallow (remotefilelog)
167 repo, avoid --linear, --no-follow, or any diff options. As the server
170 repo, avoid --linear, --no-follow, or any diff options. As the server
168 won't be able to populate annotate cache when non-default options
171 won't be able to populate annotate cache when non-default options
169 affecting results are used.
172 affecting results are used.
170 """
173 """
171 if not pats:
174 if not pats:
172 raise error.Abort(_(b'at least one filename or pattern is required'))
175 raise error.Abort(_(b'at least one filename or pattern is required'))
173
176
174 # performance hack: filtered repo can be slow. unfilter by default.
177 # performance hack: filtered repo can be slow. unfilter by default.
175 if ui.configbool(b'fastannotate', b'unfilteredrepo'):
178 if ui.configbool(b'fastannotate', b'unfilteredrepo'):
176 repo = repo.unfiltered()
179 repo = repo.unfiltered()
177
180
178 opts = pycompat.byteskwargs(opts)
181 opts = pycompat.byteskwargs(opts)
179
182
180 rev = opts.get(b'rev', b'.')
183 rev = opts.get(b'rev', b'.')
181 rebuild = opts.get(b'rebuild', False)
184 rebuild = opts.get(b'rebuild', False)
182
185
183 diffopts = patch.difffeatureopts(
186 diffopts = patch.difffeatureopts(
184 ui, opts, section=b'annotate', whitespace=True
187 ui, opts, section=b'annotate', whitespace=True
185 )
188 )
186 aopts = facontext.annotateopts(
189 aopts = facontext.annotateopts(
187 diffopts=diffopts,
190 diffopts=diffopts,
188 followmerge=not opts.get(b'linear', False),
191 followmerge=not opts.get(b'linear', False),
189 followrename=not opts.get(b'no_follow', False),
192 followrename=not opts.get(b'no_follow', False),
190 )
193 )
191
194
192 if not any(
195 if not any(
193 opts.get(s)
196 opts.get(s)
194 for s in [b'user', b'date', b'file', b'number', b'changeset']
197 for s in [b'user', b'date', b'file', b'number', b'changeset']
195 ):
198 ):
196 # default 'number' for compatibility. but fastannotate is more
199 # default 'number' for compatibility. but fastannotate is more
197 # efficient with "changeset", "line-number" and "no-content".
200 # efficient with "changeset", "line-number" and "no-content".
198 for name in ui.configlist(
201 for name in ui.configlist(
199 b'fastannotate', b'defaultformat', [b'number']
202 b'fastannotate', b'defaultformat', [b'number']
200 ):
203 ):
201 opts[name] = True
204 opts[name] = True
202
205
203 ui.pager(b'fastannotate')
206 ui.pager(b'fastannotate')
204 template = opts.get(b'template')
207 template = opts.get(b'template')
205 if template == b'json':
208 if template == b'json':
206 formatter = faformatter.jsonformatter(ui, repo, opts)
209 formatter = faformatter.jsonformatter(ui, repo, opts)
207 else:
210 else:
208 formatter = faformatter.defaultformatter(ui, repo, opts)
211 formatter = faformatter.defaultformatter(ui, repo, opts)
209 showdeleted = opts.get(b'deleted', False)
212 showdeleted = opts.get(b'deleted', False)
210 showlines = not bool(opts.get(b'no_content'))
213 showlines = not bool(opts.get(b'no_content'))
211 showpath = opts.get(b'file', False)
214 showpath = opts.get(b'file', False)
212
215
213 # find the head of the main (master) branch
216 # find the head of the main (master) branch
214 master = ui.config(b'fastannotate', b'mainbranch') or rev
217 master = ui.config(b'fastannotate', b'mainbranch') or rev
215
218
216 # paths will be used for prefetching and the real annotating
219 # paths will be used for prefetching and the real annotating
217 paths = list(_matchpaths(repo, rev, pats, opts, aopts))
220 paths = list(_matchpaths(repo, rev, pats, opts, aopts))
218
221
219 # for client, prefetch from the server
222 # for client, prefetch from the server
220 if hasattr(repo, 'prefetchfastannotate'):
223 if hasattr(repo, 'prefetchfastannotate'):
221 repo.prefetchfastannotate(paths)
224 repo.prefetchfastannotate(paths)
222
225
223 for path in paths:
226 for path in paths:
224 result = lines = existinglines = None
227 result = lines = existinglines = None
225 while True:
228 while True:
226 try:
229 try:
227 with facontext.annotatecontext(repo, path, aopts, rebuild) as a:
230 with facontext.annotatecontext(repo, path, aopts, rebuild) as a:
228 result = a.annotate(
231 result = a.annotate(
229 rev,
232 rev,
230 master=master,
233 master=master,
231 showpath=showpath,
234 showpath=showpath,
232 showlines=(showlines and not showdeleted),
235 showlines=(showlines and not showdeleted),
233 )
236 )
234 if showdeleted:
237 if showdeleted:
235 existinglines = {(l[0], l[1]) for l in result}
238 existinglines = {(l[0], l[1]) for l in result}
236 result = a.annotatealllines(
239 result = a.annotatealllines(
237 rev, showpath=showpath, showlines=showlines
240 rev, showpath=showpath, showlines=showlines
238 )
241 )
239 break
242 break
240 except (faerror.CannotReuseError, faerror.CorruptedFileError):
243 except (faerror.CannotReuseError, faerror.CorruptedFileError):
241 # happens if master moves backwards, or the file was deleted
244 # happens if master moves backwards, or the file was deleted
242 # and readded, or renamed to an existing name, or corrupted.
245 # and readded, or renamed to an existing name, or corrupted.
243 if rebuild: # give up since we have tried rebuild already
246 if rebuild: # give up since we have tried rebuild already
244 raise
247 raise
245 else: # try a second time rebuilding the cache (slow)
248 else: # try a second time rebuilding the cache (slow)
246 rebuild = True
249 rebuild = True
247 continue
250 continue
248
251
249 if showlines:
252 if showlines:
250 result, lines = result
253 result, lines = result
251
254
252 formatter.write(result, lines, existinglines=existinglines)
255 formatter.write(result, lines, existinglines=existinglines)
253 formatter.end()
256 formatter.end()
254
257
255
258
256 _newopts = set()
259 _newopts = set()
257 _knownopts = {
260 _knownopts: Set[bytes] = {
258 opt[1].replace(b'-', b'_')
261 opt[1].replace(b'-', b'_')
259 for opt in (fastannotatecommandargs['options'] + commands.globalopts)
262 for opt in (fastannotatecommandargs['options'] + commands.globalopts)
260 }
263 }
261
264
262
265
263 def _annotatewrapper(orig, ui, repo, *pats, **opts):
266 def _annotatewrapper(orig, ui, repo, *pats, **opts):
264 """used by wrapdefault"""
267 """used by wrapdefault"""
265 # we need this hack until the obsstore has 0.0 seconds perf impact
268 # we need this hack until the obsstore has 0.0 seconds perf impact
266 if ui.configbool(b'fastannotate', b'unfilteredrepo'):
269 if ui.configbool(b'fastannotate', b'unfilteredrepo'):
267 repo = repo.unfiltered()
270 repo = repo.unfiltered()
268
271
269 # treat the file as text (skip the isbinary check)
272 # treat the file as text (skip the isbinary check)
270 if ui.configbool(b'fastannotate', b'forcetext'):
273 if ui.configbool(b'fastannotate', b'forcetext'):
271 opts['text'] = True
274 opts['text'] = True
272
275
273 # check if we need to do prefetch (client-side)
276 # check if we need to do prefetch (client-side)
274 rev = opts.get('rev')
277 rev = opts.get('rev')
275 if hasattr(repo, 'prefetchfastannotate') and rev is not None:
278 if hasattr(repo, 'prefetchfastannotate') and rev is not None:
276 paths = list(_matchpaths(repo, rev, pats, pycompat.byteskwargs(opts)))
279 paths = list(_matchpaths(repo, rev, pats, pycompat.byteskwargs(opts)))
277 repo.prefetchfastannotate(paths)
280 repo.prefetchfastannotate(paths)
278
281
279 return orig(ui, repo, *pats, **opts)
282 return orig(ui, repo, *pats, **opts)
280
283
281
284
282 def registercommand():
285 def registercommand():
283 """register the fastannotate command"""
286 """register the fastannotate command"""
284 name = b'fastannotate|fastblame|fa'
287 name = b'fastannotate|fastblame|fa'
285 command(name, helpbasic=True, **fastannotatecommandargs)(fastannotate)
288 command(name, helpbasic=True, **fastannotatecommandargs)(fastannotate)
286
289
287
290
288 def wrapdefault():
291 def wrapdefault():
289 """wrap the default annotate command, to be aware of the protocol"""
292 """wrap the default annotate command, to be aware of the protocol"""
290 extensions.wrapcommand(commands.table, b'annotate', _annotatewrapper)
293 extensions.wrapcommand(commands.table, b'annotate', _annotatewrapper)
291
294
292
295
293 @command(
296 @command(
294 b'debugbuildannotatecache',
297 b'debugbuildannotatecache',
295 [(b'r', b'rev', b'', _(b'build up to the specific revision'), _(b'REV'))]
298 [(b'r', b'rev', b'', _(b'build up to the specific revision'), _(b'REV'))]
296 + commands.walkopts,
299 + commands.walkopts,
297 _(b'[-r REV] FILE...'),
300 _(b'[-r REV] FILE...'),
298 )
301 )
299 def debugbuildannotatecache(ui, repo, *pats, **opts):
302 def debugbuildannotatecache(ui, repo, *pats, **opts):
300 """incrementally build fastannotate cache up to REV for specified files
303 """incrementally build fastannotate cache up to REV for specified files
301
304
302 If REV is not specified, use the config 'fastannotate.mainbranch'.
305 If REV is not specified, use the config 'fastannotate.mainbranch'.
303
306
304 If fastannotate.client is True, download the annotate cache from the
307 If fastannotate.client is True, download the annotate cache from the
305 server. Otherwise, build the annotate cache locally.
308 server. Otherwise, build the annotate cache locally.
306
309
307 The annotate cache will be built using the default diff and follow
310 The annotate cache will be built using the default diff and follow
308 options and lives in '.hg/fastannotate/default'.
311 options and lives in '.hg/fastannotate/default'.
309 """
312 """
310 opts = pycompat.byteskwargs(opts)
313 opts = pycompat.byteskwargs(opts)
311 rev = opts.get(b'REV') or ui.config(b'fastannotate', b'mainbranch')
314 rev = opts.get(b'REV') or ui.config(b'fastannotate', b'mainbranch')
312 if not rev:
315 if not rev:
313 raise error.Abort(
316 raise error.Abort(
314 _(b'you need to provide a revision'),
317 _(b'you need to provide a revision'),
315 hint=_(b'set fastannotate.mainbranch or use --rev'),
318 hint=_(b'set fastannotate.mainbranch or use --rev'),
316 )
319 )
317 if ui.configbool(b'fastannotate', b'unfilteredrepo'):
320 if ui.configbool(b'fastannotate', b'unfilteredrepo'):
318 repo = repo.unfiltered()
321 repo = repo.unfiltered()
319 ctx = logcmdutil.revsingle(repo, rev)
322 ctx = logcmdutil.revsingle(repo, rev)
320 m = scmutil.match(ctx, pats, opts)
323 m = scmutil.match(ctx, pats, opts)
321 paths = list(ctx.walk(m))
324 paths = list(ctx.walk(m))
322 if hasattr(repo, 'prefetchfastannotate'):
325 if hasattr(repo, 'prefetchfastannotate'):
323 # client
326 # client
324 if opts.get(b'REV'):
327 if opts.get(b'REV'):
325 raise error.Abort(_(b'--rev cannot be used for client'))
328 raise error.Abort(_(b'--rev cannot be used for client'))
326 repo.prefetchfastannotate(paths)
329 repo.prefetchfastannotate(paths)
327 else:
330 else:
328 # server, or full repo
331 # server, or full repo
329 progress = ui.makeprogress(_(b'building'), total=len(paths))
332 progress = ui.makeprogress(_(b'building'), total=len(paths))
330 for i, path in enumerate(paths):
333 for i, path in enumerate(paths):
331 progress.update(i)
334 progress.update(i)
332 with facontext.annotatecontext(repo, path) as actx:
335 with facontext.annotatecontext(repo, path) as actx:
333 try:
336 try:
334 if actx.isuptodate(rev):
337 if actx.isuptodate(rev):
335 continue
338 continue
336 actx.annotate(rev, rev)
339 actx.annotate(rev, rev)
337 except (faerror.CannotReuseError, faerror.CorruptedFileError):
340 except (faerror.CannotReuseError, faerror.CorruptedFileError):
338 # the cache is broken (could happen with renaming so the
341 # the cache is broken (could happen with renaming so the
339 # file history gets invalidated). rebuild and try again.
342 # file history gets invalidated). rebuild and try again.
340 ui.debug(
343 ui.debug(
341 b'fastannotate: %s: rebuilding broken cache\n' % path
344 b'fastannotate: %s: rebuilding broken cache\n' % path
342 )
345 )
343 actx.rebuild()
346 actx.rebuild()
344 try:
347 try:
345 actx.annotate(rev, rev)
348 actx.annotate(rev, rev)
346 except Exception as ex:
349 except Exception as ex:
347 # possibly a bug, but should not stop us from building
350 # possibly a bug, but should not stop us from building
348 # cache for other files.
351 # cache for other files.
349 ui.warn(
352 ui.warn(
350 _(
353 _(
351 b'fastannotate: %s: failed to '
354 b'fastannotate: %s: failed to '
352 b'build cache: %r\n'
355 b'build cache: %r\n'
353 )
356 )
354 % (path, ex)
357 % (path, ex)
355 )
358 )
356 progress.complete()
359 progress.complete()
@@ -1,859 +1,863 b''
1 # Copyright 2016-present Facebook. All Rights Reserved.
1 # Copyright 2016-present Facebook. All Rights Reserved.
2 #
2 #
3 # context: context needed to annotate a file
3 # context: context needed to annotate a file
4 #
4 #
5 # This software may be used and distributed according to the terms of the
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.
6 # GNU General Public License version 2 or any later version.
7
7
8
8
9 import collections
9 import collections
10 import contextlib
10 import contextlib
11 import os
11 import os
12
12
13 from mercurial.i18n import _
13 from mercurial.i18n import _
14 from mercurial.pycompat import (
14 from mercurial.pycompat import (
15 open,
15 open,
16 )
16 )
17 from mercurial.node import (
17 from mercurial.node import (
18 bin,
18 bin,
19 hex,
19 hex,
20 short,
20 short,
21 )
21 )
22 from mercurial import (
22 from mercurial import (
23 error,
23 error,
24 linelog as linelogmod,
24 linelog as linelogmod,
25 lock as lockmod,
25 lock as lockmod,
26 mdiff,
26 mdiff,
27 pycompat,
27 pycompat,
28 scmutil,
28 scmutil,
29 util,
29 util,
30 )
30 )
31 from mercurial.utils import (
31 from mercurial.utils import (
32 hashutil,
32 hashutil,
33 stringutil,
33 stringutil,
34 )
34 )
35
35
36 from . import (
36 from . import (
37 error as faerror,
37 error as faerror,
38 revmap as revmapmod,
38 revmap as revmapmod,
39 )
39 )
40
40
41
41
42 # given path, get filelog, cached
42 # given path, get filelog, cached
43 @util.lrucachefunc
43 @util.lrucachefunc
44 def _getflog(repo, path):
44 def _getflog(repo, path):
45 return repo.file(path)
45 return repo.file(path)
46
46
47
47
48 # extracted from mercurial.context.basefilectx.annotate
48 # extracted from mercurial.context.basefilectx.annotate
49 def _parents(f, follow=True):
49 def _parents(f, follow=True):
50 # Cut _descendantrev here to mitigate the penalty of lazy linkrev
50 # Cut _descendantrev here to mitigate the penalty of lazy linkrev
51 # adjustment. Otherwise, p._adjustlinkrev() would walk changelog
51 # adjustment. Otherwise, p._adjustlinkrev() would walk changelog
52 # from the topmost introrev (= srcrev) down to p.linkrev() if it
52 # from the topmost introrev (= srcrev) down to p.linkrev() if it
53 # isn't an ancestor of the srcrev.
53 # isn't an ancestor of the srcrev.
54 f._changeid
54 f._changeid
55 pl = f.parents()
55 pl = f.parents()
56
56
57 # Don't return renamed parents if we aren't following.
57 # Don't return renamed parents if we aren't following.
58 if not follow:
58 if not follow:
59 pl = [p for p in pl if p.path() == f.path()]
59 pl = [p for p in pl if p.path() == f.path()]
60
60
61 # renamed filectx won't have a filelog yet, so set it
61 # renamed filectx won't have a filelog yet, so set it
62 # from the cache to save time
62 # from the cache to save time
63 for p in pl:
63 for p in pl:
64 if not '_filelog' in p.__dict__:
64 if not '_filelog' in p.__dict__:
65 p._filelog = _getflog(f._repo, p.path())
65 p._filelog = _getflog(f._repo, p.path())
66
66
67 return pl
67 return pl
68
68
69
69
70 # extracted from mercurial.context.basefilectx.annotate. slightly modified
70 # extracted from mercurial.context.basefilectx.annotate. slightly modified
71 # so it takes a fctx instead of a pair of text and fctx.
71 # so it takes a fctx instead of a pair of text and fctx.
72 def _decorate(fctx):
72 def _decorate(fctx):
73 text = fctx.data()
73 text = fctx.data()
74 linecount = text.count(b'\n')
74 linecount = text.count(b'\n')
75 if text and not text.endswith(b'\n'):
75 if text and not text.endswith(b'\n'):
76 linecount += 1
76 linecount += 1
77 return ([(fctx, i) for i in range(linecount)], text)
77 return ([(fctx, i) for i in range(linecount)], text)
78
78
79
79
80 # extracted from mercurial.context.basefilectx.annotate. slightly modified
80 # extracted from mercurial.context.basefilectx.annotate. slightly modified
81 # so it takes an extra "blocks" parameter calculated elsewhere, instead of
81 # so it takes an extra "blocks" parameter calculated elsewhere, instead of
82 # calculating diff here.
82 # calculating diff here.
83 def _pair(parent, child, blocks):
83 def _pair(parent, child, blocks):
84 for (a1, a2, b1, b2), t in blocks:
84 for (a1, a2, b1, b2), t in blocks:
85 # Changed blocks ('!') or blocks made only of blank lines ('~')
85 # Changed blocks ('!') or blocks made only of blank lines ('~')
86 # belong to the child.
86 # belong to the child.
87 if t == b'=':
87 if t == b'=':
88 child[0][b1:b2] = parent[0][a1:a2]
88 child[0][b1:b2] = parent[0][a1:a2]
89 return child
89 return child
90
90
91
91
92 # like scmutil.revsingle, but with lru cache, so their states (like manifests)
92 # like scmutil.revsingle, but with lru cache, so their states (like manifests)
93 # could be reused
93 # could be reused
94 _revsingle = util.lrucachefunc(scmutil.revsingle)
94 _revsingle = util.lrucachefunc(scmutil.revsingle)
95
95
96
96
97 def resolvefctx(repo, rev, path, resolverev=False, adjustctx=None):
97 def resolvefctx(repo, rev, path, resolverev=False, adjustctx=None):
98 """(repo, str, str) -> fctx
98 """(repo, str, str) -> fctx
99
99
100 get the filectx object from repo, rev, path, in an efficient way.
100 get the filectx object from repo, rev, path, in an efficient way.
101
101
102 if resolverev is True, "rev" is a revision specified by the revset
102 if resolverev is True, "rev" is a revision specified by the revset
103 language, otherwise "rev" is a nodeid, or a revision number that can
103 language, otherwise "rev" is a nodeid, or a revision number that can
104 be consumed by repo.__getitem__.
104 be consumed by repo.__getitem__.
105
105
106 if adjustctx is not None, the returned fctx will point to a changeset
106 if adjustctx is not None, the returned fctx will point to a changeset
107 that introduces the change (last modified the file). if adjustctx
107 that introduces the change (last modified the file). if adjustctx
108 is 'linkrev', trust the linkrev and do not adjust it. this is noticeably
108 is 'linkrev', trust the linkrev and do not adjust it. this is noticeably
109 faster for big repos but is incorrect for some cases.
109 faster for big repos but is incorrect for some cases.
110 """
110 """
111 if resolverev and not isinstance(rev, int) and rev is not None:
111 if resolverev and not isinstance(rev, int) and rev is not None:
112 ctx = _revsingle(repo, rev)
112 ctx = _revsingle(repo, rev)
113 else:
113 else:
114 ctx = repo[rev]
114 ctx = repo[rev]
115
115
116 # If we don't need to adjust the linkrev, create the filectx using the
116 # If we don't need to adjust the linkrev, create the filectx using the
117 # changectx instead of using ctx[path]. This means it already has the
117 # changectx instead of using ctx[path]. This means it already has the
118 # changectx information, so blame -u will be able to look directly at the
118 # changectx information, so blame -u will be able to look directly at the
119 # commitctx object instead of having to resolve it by going through the
119 # commitctx object instead of having to resolve it by going through the
120 # manifest. In a lazy-manifest world this can prevent us from downloading a
120 # manifest. In a lazy-manifest world this can prevent us from downloading a
121 # lot of data.
121 # lot of data.
122 if adjustctx is None:
122 if adjustctx is None:
123 # ctx.rev() is None means it's the working copy, which is a special
123 # ctx.rev() is None means it's the working copy, which is a special
124 # case.
124 # case.
125 if ctx.rev() is None:
125 if ctx.rev() is None:
126 fctx = ctx[path]
126 fctx = ctx[path]
127 else:
127 else:
128 fctx = repo.filectx(path, changeid=ctx.rev())
128 fctx = repo.filectx(path, changeid=ctx.rev())
129 else:
129 else:
130 fctx = ctx[path]
130 fctx = ctx[path]
131 if adjustctx == b'linkrev':
131 if adjustctx == b'linkrev':
132 introrev = fctx.linkrev()
132 introrev = fctx.linkrev()
133 else:
133 else:
134 introrev = fctx.introrev()
134 introrev = fctx.introrev()
135 if introrev != ctx.rev():
135 if introrev != ctx.rev():
136 fctx._changeid = introrev
136 fctx._changeid = introrev
137 fctx._changectx = repo[introrev]
137 fctx._changectx = repo[introrev]
138 return fctx
138 return fctx
139
139
140
140
141 # like mercurial.store.encodedir, but use linelog suffixes: .m, .l, .lock
141 # like mercurial.store.encodedir, but use linelog suffixes: .m, .l, .lock
142 def encodedir(path):
142 def encodedir(path):
143 return (
143 return (
144 path.replace(b'.hg/', b'.hg.hg/')
144 path.replace(b'.hg/', b'.hg.hg/')
145 .replace(b'.l/', b'.l.hg/')
145 .replace(b'.l/', b'.l.hg/')
146 .replace(b'.m/', b'.m.hg/')
146 .replace(b'.m/', b'.m.hg/')
147 .replace(b'.lock/', b'.lock.hg/')
147 .replace(b'.lock/', b'.lock.hg/')
148 )
148 )
149
149
150
150
151 def hashdiffopts(diffopts):
151 def hashdiffopts(diffopts):
152 diffoptstr = stringutil.pprint(
152 diffoptstr = stringutil.pprint(
153 sorted(
153 sorted(
154 (k, getattr(diffopts, pycompat.sysstr(k)))
154 (k, getattr(diffopts, pycompat.sysstr(k)))
155 for k in mdiff.diffopts.defaults
155 for k in mdiff.diffopts.defaults
156 )
156 )
157 )
157 )
158 return hex(hashutil.sha1(diffoptstr).digest())[:6]
158 return hex(hashutil.sha1(diffoptstr).digest())[:6]
159
159
160
160
161 _defaultdiffopthash = hashdiffopts(mdiff.defaultopts)
161 _defaultdiffopthash = hashdiffopts(mdiff.defaultopts)
162
162
163
163
164 class annotateopts:
164 class annotateopts:
165 """like mercurial.mdiff.diffopts, but is for annotate
165 """like mercurial.mdiff.diffopts, but is for annotate
166
166
167 followrename: follow renames, like "hg annotate -f"
167 followrename: follow renames, like "hg annotate -f"
168 followmerge: follow p2 of a merge changeset, otherwise p2 is ignored
168 followmerge: follow p2 of a merge changeset, otherwise p2 is ignored
169 """
169 """
170
170
171 defaults = {
171 defaults = {
172 'diffopts': None,
172 'diffopts': None,
173 'followrename': True,
173 'followrename': True,
174 'followmerge': True,
174 'followmerge': True,
175 }
175 }
176
176
177 diffopts: mdiff.diffopts
178 followrename: bool
179 followmerge: bool
180
177 def __init__(self, **opts):
181 def __init__(self, **opts):
178 for k, v in self.defaults.items():
182 for k, v in self.defaults.items():
179 setattr(self, k, opts.get(k, v))
183 setattr(self, k, opts.get(k, v))
180
184
181 @util.propertycache
185 @util.propertycache
182 def shortstr(self):
186 def shortstr(self) -> bytes:
183 """represent opts in a short string, suitable for a directory name"""
187 """represent opts in a short string, suitable for a directory name"""
184 result = b''
188 result = b''
185 if not self.followrename:
189 if not self.followrename:
186 result += b'r0'
190 result += b'r0'
187 if not self.followmerge:
191 if not self.followmerge:
188 result += b'm0'
192 result += b'm0'
189 if self.diffopts is not None:
193 if self.diffopts is not None:
190 assert isinstance(self.diffopts, mdiff.diffopts)
194 assert isinstance(self.diffopts, mdiff.diffopts)
191 diffopthash = hashdiffopts(self.diffopts)
195 diffopthash = hashdiffopts(self.diffopts)
192 if diffopthash != _defaultdiffopthash:
196 if diffopthash != _defaultdiffopthash:
193 result += b'i' + diffopthash
197 result += b'i' + diffopthash
194 return result or b'default'
198 return result or b'default'
195
199
196
200
197 defaultopts = annotateopts()
201 defaultopts = annotateopts()
198
202
199
203
200 class _annotatecontext:
204 class _annotatecontext:
201 """do not use this class directly as it does not use lock to protect
205 """do not use this class directly as it does not use lock to protect
202 writes. use "with annotatecontext(...)" instead.
206 writes. use "with annotatecontext(...)" instead.
203 """
207 """
204
208
205 def __init__(self, repo, path, linelogpath, revmappath, opts):
209 def __init__(self, repo, path, linelogpath, revmappath, opts):
206 self.repo = repo
210 self.repo = repo
207 self.ui = repo.ui
211 self.ui = repo.ui
208 self.path = path
212 self.path = path
209 self.opts = opts
213 self.opts = opts
210 self.linelogpath = linelogpath
214 self.linelogpath = linelogpath
211 self.revmappath = revmappath
215 self.revmappath = revmappath
212 self._linelog = None
216 self._linelog = None
213 self._revmap = None
217 self._revmap = None
214 self._node2path = {} # {str: str}
218 self._node2path = {} # {str: str}
215
219
216 @property
220 @property
217 def linelog(self):
221 def linelog(self):
218 if self._linelog is None:
222 if self._linelog is None:
219 if os.path.exists(self.linelogpath):
223 if os.path.exists(self.linelogpath):
220 with open(self.linelogpath, b'rb') as f:
224 with open(self.linelogpath, b'rb') as f:
221 try:
225 try:
222 self._linelog = linelogmod.linelog.fromdata(f.read())
226 self._linelog = linelogmod.linelog.fromdata(f.read())
223 except linelogmod.LineLogError:
227 except linelogmod.LineLogError:
224 self._linelog = linelogmod.linelog()
228 self._linelog = linelogmod.linelog()
225 else:
229 else:
226 self._linelog = linelogmod.linelog()
230 self._linelog = linelogmod.linelog()
227 return self._linelog
231 return self._linelog
228
232
229 @property
233 @property
230 def revmap(self):
234 def revmap(self):
231 if self._revmap is None:
235 if self._revmap is None:
232 self._revmap = revmapmod.revmap(self.revmappath)
236 self._revmap = revmapmod.revmap(self.revmappath)
233 return self._revmap
237 return self._revmap
234
238
235 def close(self):
239 def close(self):
236 if self._revmap is not None:
240 if self._revmap is not None:
237 self._revmap.flush()
241 self._revmap.flush()
238 self._revmap = None
242 self._revmap = None
239 if self._linelog is not None:
243 if self._linelog is not None:
240 with open(self.linelogpath, b'wb') as f:
244 with open(self.linelogpath, b'wb') as f:
241 f.write(self._linelog.encode())
245 f.write(self._linelog.encode())
242 self._linelog = None
246 self._linelog = None
243
247
244 __del__ = close
248 __del__ = close
245
249
246 def rebuild(self):
250 def rebuild(self):
247 """delete linelog and revmap, useful for rebuilding"""
251 """delete linelog and revmap, useful for rebuilding"""
248 self.close()
252 self.close()
249 self._node2path.clear()
253 self._node2path.clear()
250 _unlinkpaths([self.revmappath, self.linelogpath])
254 _unlinkpaths([self.revmappath, self.linelogpath])
251
255
252 @property
256 @property
253 def lastnode(self):
257 def lastnode(self):
254 """return last node in revmap, or None if revmap is empty"""
258 """return last node in revmap, or None if revmap is empty"""
255 if self._revmap is None:
259 if self._revmap is None:
256 # fast path, read revmap without loading its full content
260 # fast path, read revmap without loading its full content
257 return revmapmod.getlastnode(self.revmappath)
261 return revmapmod.getlastnode(self.revmappath)
258 else:
262 else:
259 return self._revmap.rev2hsh(self._revmap.maxrev)
263 return self._revmap.rev2hsh(self._revmap.maxrev)
260
264
261 def isuptodate(self, master, strict=True):
265 def isuptodate(self, master, strict=True):
262 """return True if the revmap / linelog is up-to-date, or the file
266 """return True if the revmap / linelog is up-to-date, or the file
263 does not exist in the master revision. False otherwise.
267 does not exist in the master revision. False otherwise.
264
268
265 it tries to be fast and could return false negatives, because of the
269 it tries to be fast and could return false negatives, because of the
266 use of linkrev instead of introrev.
270 use of linkrev instead of introrev.
267
271
268 useful for both server and client to decide whether to update
272 useful for both server and client to decide whether to update
269 fastannotate cache or not.
273 fastannotate cache or not.
270
274
271 if strict is True, even if fctx exists in the revmap, but is not the
275 if strict is True, even if fctx exists in the revmap, but is not the
272 last node, isuptodate will return False. it's good for performance - no
276 last node, isuptodate will return False. it's good for performance - no
273 expensive check was done.
277 expensive check was done.
274
278
275 if strict is False, if fctx exists in the revmap, this function may
279 if strict is False, if fctx exists in the revmap, this function may
276 return True. this is useful for the client to skip downloading the
280 return True. this is useful for the client to skip downloading the
277 cache if the client's master is behind the server's.
281 cache if the client's master is behind the server's.
278 """
282 """
279 lastnode = self.lastnode
283 lastnode = self.lastnode
280 try:
284 try:
281 f = self._resolvefctx(master, resolverev=True)
285 f = self._resolvefctx(master, resolverev=True)
282 # choose linkrev instead of introrev as the check is meant to be
286 # choose linkrev instead of introrev as the check is meant to be
283 # *fast*.
287 # *fast*.
284 linknode = self.repo.changelog.node(f.linkrev())
288 linknode = self.repo.changelog.node(f.linkrev())
285 if not strict and lastnode and linknode != lastnode:
289 if not strict and lastnode and linknode != lastnode:
286 # check if f.node() is in the revmap. note: this loads the
290 # check if f.node() is in the revmap. note: this loads the
287 # revmap and can be slow.
291 # revmap and can be slow.
288 return self.revmap.hsh2rev(linknode) is not None
292 return self.revmap.hsh2rev(linknode) is not None
289 # avoid resolving old manifest, or slow adjustlinkrev to be fast,
293 # avoid resolving old manifest, or slow adjustlinkrev to be fast,
290 # false negatives are acceptable in this case.
294 # false negatives are acceptable in this case.
291 return linknode == lastnode
295 return linknode == lastnode
292 except LookupError:
296 except LookupError:
293 # master does not have the file, or the revmap is ahead
297 # master does not have the file, or the revmap is ahead
294 return True
298 return True
295
299
296 def annotate(self, rev, master=None, showpath=False, showlines=False):
300 def annotate(self, rev, master=None, showpath=False, showlines=False):
297 """incrementally update the cache so it includes revisions in the main
301 """incrementally update the cache so it includes revisions in the main
298 branch till 'master'. and run annotate on 'rev', which may or may not be
302 branch till 'master'. and run annotate on 'rev', which may or may not be
299 included in the main branch.
303 included in the main branch.
300
304
301 if master is None, do not update linelog.
305 if master is None, do not update linelog.
302
306
303 the first value returned is the annotate result, it is [(node, linenum)]
307 the first value returned is the annotate result, it is [(node, linenum)]
304 by default. [(node, linenum, path)] if showpath is True.
308 by default. [(node, linenum, path)] if showpath is True.
305
309
306 if showlines is True, a second value will be returned, it is a list of
310 if showlines is True, a second value will be returned, it is a list of
307 corresponding line contents.
311 corresponding line contents.
308 """
312 """
309
313
310 # the fast path test requires commit hash, convert rev number to hash,
314 # the fast path test requires commit hash, convert rev number to hash,
311 # so it may hit the fast path. note: in the "fctx" mode, the "annotate"
315 # so it may hit the fast path. note: in the "fctx" mode, the "annotate"
312 # command could give us a revision number even if the user passes a
316 # command could give us a revision number even if the user passes a
313 # commit hash.
317 # commit hash.
314 if isinstance(rev, int):
318 if isinstance(rev, int):
315 rev = hex(self.repo.changelog.node(rev))
319 rev = hex(self.repo.changelog.node(rev))
316
320
317 # fast path: if rev is in the main branch already
321 # fast path: if rev is in the main branch already
318 directly, revfctx = self.canannotatedirectly(rev)
322 directly, revfctx = self.canannotatedirectly(rev)
319 if directly:
323 if directly:
320 if self.ui.debugflag:
324 if self.ui.debugflag:
321 self.ui.debug(
325 self.ui.debug(
322 b'fastannotate: %s: using fast path '
326 b'fastannotate: %s: using fast path '
323 b'(resolved fctx: %s)\n'
327 b'(resolved fctx: %s)\n'
324 % (
328 % (
325 self.path,
329 self.path,
326 stringutil.pprint(hasattr(revfctx, 'node')),
330 stringutil.pprint(hasattr(revfctx, 'node')),
327 )
331 )
328 )
332 )
329 return self.annotatedirectly(revfctx, showpath, showlines)
333 return self.annotatedirectly(revfctx, showpath, showlines)
330
334
331 # resolve master
335 # resolve master
332 masterfctx = None
336 masterfctx = None
333 if master:
337 if master:
334 try:
338 try:
335 masterfctx = self._resolvefctx(
339 masterfctx = self._resolvefctx(
336 master, resolverev=True, adjustctx=True
340 master, resolverev=True, adjustctx=True
337 )
341 )
338 except LookupError: # master does not have the file
342 except LookupError: # master does not have the file
339 pass
343 pass
340 else:
344 else:
341 if masterfctx in self.revmap: # no need to update linelog
345 if masterfctx in self.revmap: # no need to update linelog
342 masterfctx = None
346 masterfctx = None
343
347
344 # ... - @ <- rev (can be an arbitrary changeset,
348 # ... - @ <- rev (can be an arbitrary changeset,
345 # / not necessarily a descendant
349 # / not necessarily a descendant
346 # master -> o of master)
350 # master -> o of master)
347 # |
351 # |
348 # a merge -> o 'o': new changesets in the main branch
352 # a merge -> o 'o': new changesets in the main branch
349 # |\ '#': revisions in the main branch that
353 # |\ '#': revisions in the main branch that
350 # o * exist in linelog / revmap
354 # o * exist in linelog / revmap
351 # | . '*': changesets in side branches, or
355 # | . '*': changesets in side branches, or
352 # last master -> # . descendants of master
356 # last master -> # . descendants of master
353 # | .
357 # | .
354 # # * joint: '#', and is a parent of a '*'
358 # # * joint: '#', and is a parent of a '*'
355 # |/
359 # |/
356 # a joint -> # ^^^^ --- side branches
360 # a joint -> # ^^^^ --- side branches
357 # |
361 # |
358 # ^ --- main branch (in linelog)
362 # ^ --- main branch (in linelog)
359
363
360 # these DFSes are similar to the traditional annotate algorithm.
364 # these DFSes are similar to the traditional annotate algorithm.
361 # we cannot really reuse the code for perf reason.
365 # we cannot really reuse the code for perf reason.
362
366
363 # 1st DFS calculates merges, joint points, and needed.
367 # 1st DFS calculates merges, joint points, and needed.
364 # "needed" is a simple reference counting dict to free items in
368 # "needed" is a simple reference counting dict to free items in
365 # "hist", reducing its memory usage otherwise could be huge.
369 # "hist", reducing its memory usage otherwise could be huge.
366 initvisit = [revfctx]
370 initvisit = [revfctx]
367 if masterfctx:
371 if masterfctx:
368 if masterfctx.rev() is None:
372 if masterfctx.rev() is None:
369 raise error.Abort(
373 raise error.Abort(
370 _(b'cannot update linelog to wdir()'),
374 _(b'cannot update linelog to wdir()'),
371 hint=_(b'set fastannotate.mainbranch'),
375 hint=_(b'set fastannotate.mainbranch'),
372 )
376 )
373 initvisit.append(masterfctx)
377 initvisit.append(masterfctx)
374 visit = initvisit[:]
378 visit = initvisit[:]
375 pcache = {}
379 pcache = {}
376 needed = {revfctx: 1}
380 needed = {revfctx: 1}
377 hist = {} # {fctx: ([(llrev or fctx, linenum)], text)}
381 hist = {} # {fctx: ([(llrev or fctx, linenum)], text)}
378 while visit:
382 while visit:
379 f = visit.pop()
383 f = visit.pop()
380 if f in pcache or f in hist:
384 if f in pcache or f in hist:
381 continue
385 continue
382 if f in self.revmap: # in the old main branch, it's a joint
386 if f in self.revmap: # in the old main branch, it's a joint
383 llrev = self.revmap.hsh2rev(f.node())
387 llrev = self.revmap.hsh2rev(f.node())
384 self.linelog.annotate(llrev)
388 self.linelog.annotate(llrev)
385 result = self.linelog.annotateresult
389 result = self.linelog.annotateresult
386 hist[f] = (result, f.data())
390 hist[f] = (result, f.data())
387 continue
391 continue
388 pl = self._parentfunc(f)
392 pl = self._parentfunc(f)
389 pcache[f] = pl
393 pcache[f] = pl
390 for p in pl:
394 for p in pl:
391 needed[p] = needed.get(p, 0) + 1
395 needed[p] = needed.get(p, 0) + 1
392 if p not in pcache:
396 if p not in pcache:
393 visit.append(p)
397 visit.append(p)
394
398
395 # 2nd (simple) DFS calculates new changesets in the main branch
399 # 2nd (simple) DFS calculates new changesets in the main branch
396 # ('o' nodes in # the above graph), so we know when to update linelog.
400 # ('o' nodes in # the above graph), so we know when to update linelog.
397 newmainbranch = set()
401 newmainbranch = set()
398 f = masterfctx
402 f = masterfctx
399 while f and f not in self.revmap:
403 while f and f not in self.revmap:
400 newmainbranch.add(f)
404 newmainbranch.add(f)
401 pl = pcache[f]
405 pl = pcache[f]
402 if pl:
406 if pl:
403 f = pl[0]
407 f = pl[0]
404 else:
408 else:
405 f = None
409 f = None
406 break
410 break
407
411
408 # f, if present, is the position where the last build stopped at, and
412 # f, if present, is the position where the last build stopped at, and
409 # should be the "master" last time. check to see if we can continue
413 # should be the "master" last time. check to see if we can continue
410 # building the linelog incrementally. (we cannot if diverged)
414 # building the linelog incrementally. (we cannot if diverged)
411 if masterfctx is not None:
415 if masterfctx is not None:
412 self._checklastmasterhead(f)
416 self._checklastmasterhead(f)
413
417
414 if self.ui.debugflag:
418 if self.ui.debugflag:
415 if newmainbranch:
419 if newmainbranch:
416 self.ui.debug(
420 self.ui.debug(
417 b'fastannotate: %s: %d new changesets in the main'
421 b'fastannotate: %s: %d new changesets in the main'
418 b' branch\n' % (self.path, len(newmainbranch))
422 b' branch\n' % (self.path, len(newmainbranch))
419 )
423 )
420 elif not hist: # no joints, no updates
424 elif not hist: # no joints, no updates
421 self.ui.debug(
425 self.ui.debug(
422 b'fastannotate: %s: linelog cannot help in '
426 b'fastannotate: %s: linelog cannot help in '
423 b'annotating this revision\n' % self.path
427 b'annotating this revision\n' % self.path
424 )
428 )
425
429
426 # prepare annotateresult so we can update linelog incrementally
430 # prepare annotateresult so we can update linelog incrementally
427 self.linelog.annotate(self.linelog.maxrev)
431 self.linelog.annotate(self.linelog.maxrev)
428
432
429 # 3rd DFS does the actual annotate
433 # 3rd DFS does the actual annotate
430 visit = initvisit[:]
434 visit = initvisit[:]
431 progress = self.ui.makeprogress(
435 progress = self.ui.makeprogress(
432 b'building cache', total=len(newmainbranch)
436 b'building cache', total=len(newmainbranch)
433 )
437 )
434 while visit:
438 while visit:
435 f = visit[-1]
439 f = visit[-1]
436 if f in hist:
440 if f in hist:
437 visit.pop()
441 visit.pop()
438 continue
442 continue
439
443
440 ready = True
444 ready = True
441 pl = pcache[f]
445 pl = pcache[f]
442 for p in pl:
446 for p in pl:
443 if p not in hist:
447 if p not in hist:
444 ready = False
448 ready = False
445 visit.append(p)
449 visit.append(p)
446 if not ready:
450 if not ready:
447 continue
451 continue
448
452
449 visit.pop()
453 visit.pop()
450 blocks = None # mdiff blocks, used for appending linelog
454 blocks = None # mdiff blocks, used for appending linelog
451 ismainbranch = f in newmainbranch
455 ismainbranch = f in newmainbranch
452 # curr is the same as the traditional annotate algorithm,
456 # curr is the same as the traditional annotate algorithm,
453 # if we only care about linear history (do not follow merge),
457 # if we only care about linear history (do not follow merge),
454 # then curr is not actually used.
458 # then curr is not actually used.
455 assert f not in hist
459 assert f not in hist
456 curr = _decorate(f)
460 curr = _decorate(f)
457 for i, p in enumerate(pl):
461 for i, p in enumerate(pl):
458 bs = list(self._diffblocks(hist[p][1], curr[1]))
462 bs = list(self._diffblocks(hist[p][1], curr[1]))
459 if i == 0 and ismainbranch:
463 if i == 0 and ismainbranch:
460 blocks = bs
464 blocks = bs
461 curr = _pair(hist[p], curr, bs)
465 curr = _pair(hist[p], curr, bs)
462 if needed[p] == 1:
466 if needed[p] == 1:
463 del hist[p]
467 del hist[p]
464 del needed[p]
468 del needed[p]
465 else:
469 else:
466 needed[p] -= 1
470 needed[p] -= 1
467
471
468 hist[f] = curr
472 hist[f] = curr
469 del pcache[f]
473 del pcache[f]
470
474
471 if ismainbranch: # need to write to linelog
475 if ismainbranch: # need to write to linelog
472 progress.increment()
476 progress.increment()
473 bannotated = None
477 bannotated = None
474 if len(pl) == 2 and self.opts.followmerge: # merge
478 if len(pl) == 2 and self.opts.followmerge: # merge
475 bannotated = curr[0]
479 bannotated = curr[0]
476 if blocks is None: # no parents, add an empty one
480 if blocks is None: # no parents, add an empty one
477 blocks = list(self._diffblocks(b'', curr[1]))
481 blocks = list(self._diffblocks(b'', curr[1]))
478 self._appendrev(f, blocks, bannotated)
482 self._appendrev(f, blocks, bannotated)
479 elif showpath: # not append linelog, but we need to record path
483 elif showpath: # not append linelog, but we need to record path
480 self._node2path[f.node()] = f.path()
484 self._node2path[f.node()] = f.path()
481
485
482 progress.complete()
486 progress.complete()
483
487
484 result = [
488 result = [
485 ((self.revmap.rev2hsh(fr) if isinstance(fr, int) else fr.node()), l)
489 ((self.revmap.rev2hsh(fr) if isinstance(fr, int) else fr.node()), l)
486 for fr, l in hist[revfctx][0]
490 for fr, l in hist[revfctx][0]
487 ] # [(node, linenumber)]
491 ] # [(node, linenumber)]
488 return self._refineannotateresult(result, revfctx, showpath, showlines)
492 return self._refineannotateresult(result, revfctx, showpath, showlines)
489
493
490 def canannotatedirectly(self, rev):
494 def canannotatedirectly(self, rev):
491 """(str) -> bool, fctx or node.
495 """(str) -> bool, fctx or node.
492 return (True, f) if we can annotate without updating the linelog, pass
496 return (True, f) if we can annotate without updating the linelog, pass
493 f to annotatedirectly.
497 f to annotatedirectly.
494 return (False, f) if we need extra calculation. f is the fctx resolved
498 return (False, f) if we need extra calculation. f is the fctx resolved
495 from rev.
499 from rev.
496 """
500 """
497 result = True
501 result = True
498 f = None
502 f = None
499 if not isinstance(rev, int) and rev is not None:
503 if not isinstance(rev, int) and rev is not None:
500 hsh = {20: bytes, 40: bin}.get(len(rev), lambda x: None)(rev)
504 hsh = {20: bytes, 40: bin}.get(len(rev), lambda x: None)(rev)
501 if hsh is not None and (hsh, self.path) in self.revmap:
505 if hsh is not None and (hsh, self.path) in self.revmap:
502 f = hsh
506 f = hsh
503 if f is None:
507 if f is None:
504 adjustctx = b'linkrev' if self._perfhack else True
508 adjustctx = b'linkrev' if self._perfhack else True
505 f = self._resolvefctx(rev, adjustctx=adjustctx, resolverev=True)
509 f = self._resolvefctx(rev, adjustctx=adjustctx, resolverev=True)
506 result = f in self.revmap
510 result = f in self.revmap
507 if not result and self._perfhack:
511 if not result and self._perfhack:
508 # redo the resolution without perfhack - as we are going to
512 # redo the resolution without perfhack - as we are going to
509 # do write operations, we need a correct fctx.
513 # do write operations, we need a correct fctx.
510 f = self._resolvefctx(rev, adjustctx=True, resolverev=True)
514 f = self._resolvefctx(rev, adjustctx=True, resolverev=True)
511 return result, f
515 return result, f
512
516
513 def annotatealllines(self, rev, showpath=False, showlines=False):
517 def annotatealllines(self, rev, showpath=False, showlines=False):
514 """(rev : str) -> [(node : str, linenum : int, path : str)]
518 """(rev : str) -> [(node : str, linenum : int, path : str)]
515
519
516 the result has the same format with annotate, but include all (including
520 the result has the same format with annotate, but include all (including
517 deleted) lines up to rev. call this after calling annotate(rev, ...) for
521 deleted) lines up to rev. call this after calling annotate(rev, ...) for
518 better performance and accuracy.
522 better performance and accuracy.
519 """
523 """
520 revfctx = self._resolvefctx(rev, resolverev=True, adjustctx=True)
524 revfctx = self._resolvefctx(rev, resolverev=True, adjustctx=True)
521
525
522 # find a chain from rev to anything in the mainbranch
526 # find a chain from rev to anything in the mainbranch
523 if revfctx not in self.revmap:
527 if revfctx not in self.revmap:
524 chain = [revfctx]
528 chain = [revfctx]
525 a = b''
529 a = b''
526 while True:
530 while True:
527 f = chain[-1]
531 f = chain[-1]
528 pl = self._parentfunc(f)
532 pl = self._parentfunc(f)
529 if not pl:
533 if not pl:
530 break
534 break
531 if pl[0] in self.revmap:
535 if pl[0] in self.revmap:
532 a = pl[0].data()
536 a = pl[0].data()
533 break
537 break
534 chain.append(pl[0])
538 chain.append(pl[0])
535
539
536 # both self.linelog and self.revmap is backed by filesystem. now
540 # both self.linelog and self.revmap is backed by filesystem. now
537 # we want to modify them but do not want to write changes back to
541 # we want to modify them but do not want to write changes back to
538 # files. so we create in-memory objects and copy them. it's like
542 # files. so we create in-memory objects and copy them. it's like
539 # a "fork".
543 # a "fork".
540 linelog = linelogmod.linelog()
544 linelog = linelogmod.linelog()
541 linelog.copyfrom(self.linelog)
545 linelog.copyfrom(self.linelog)
542 linelog.annotate(linelog.maxrev)
546 linelog.annotate(linelog.maxrev)
543 revmap = revmapmod.revmap()
547 revmap = revmapmod.revmap()
544 revmap.copyfrom(self.revmap)
548 revmap.copyfrom(self.revmap)
545
549
546 for f in reversed(chain):
550 for f in reversed(chain):
547 b = f.data()
551 b = f.data()
548 blocks = list(self._diffblocks(a, b))
552 blocks = list(self._diffblocks(a, b))
549 self._doappendrev(linelog, revmap, f, blocks)
553 self._doappendrev(linelog, revmap, f, blocks)
550 a = b
554 a = b
551 else:
555 else:
552 # fastpath: use existing linelog, revmap as we don't write to them
556 # fastpath: use existing linelog, revmap as we don't write to them
553 linelog = self.linelog
557 linelog = self.linelog
554 revmap = self.revmap
558 revmap = self.revmap
555
559
556 lines = linelog.getalllines()
560 lines = linelog.getalllines()
557 hsh = revfctx.node()
561 hsh = revfctx.node()
558 llrev = revmap.hsh2rev(hsh)
562 llrev = revmap.hsh2rev(hsh)
559 result = [(revmap.rev2hsh(r), l) for r, l in lines if r <= llrev]
563 result = [(revmap.rev2hsh(r), l) for r, l in lines if r <= llrev]
560 # cannot use _refineannotateresult since we need custom logic for
564 # cannot use _refineannotateresult since we need custom logic for
561 # resolving line contents
565 # resolving line contents
562 if showpath:
566 if showpath:
563 result = self._addpathtoresult(result, revmap)
567 result = self._addpathtoresult(result, revmap)
564 if showlines:
568 if showlines:
565 linecontents = self._resolvelines(result, revmap, linelog)
569 linecontents = self._resolvelines(result, revmap, linelog)
566 result = (result, linecontents)
570 result = (result, linecontents)
567 return result
571 return result
568
572
569 def _resolvelines(self, annotateresult, revmap, linelog):
573 def _resolvelines(self, annotateresult, revmap, linelog):
570 """(annotateresult) -> [line]. designed for annotatealllines.
574 """(annotateresult) -> [line]. designed for annotatealllines.
571 this is probably the most inefficient code in the whole fastannotate
575 this is probably the most inefficient code in the whole fastannotate
572 directory. but we have made a decision that the linelog does not
576 directory. but we have made a decision that the linelog does not
573 store line contents. so getting them requires random accesses to
577 store line contents. so getting them requires random accesses to
574 the revlog data, since they can be many, it can be very slow.
578 the revlog data, since they can be many, it can be very slow.
575 """
579 """
576 # [llrev]
580 # [llrev]
577 revs = [revmap.hsh2rev(l[0]) for l in annotateresult]
581 revs = [revmap.hsh2rev(l[0]) for l in annotateresult]
578 result = [None] * len(annotateresult)
582 result = [None] * len(annotateresult)
579 # {(rev, linenum): [lineindex]}
583 # {(rev, linenum): [lineindex]}
580 key2idxs = collections.defaultdict(list)
584 key2idxs = collections.defaultdict(list)
581 for i in range(len(result)):
585 for i in range(len(result)):
582 key2idxs[(revs[i], annotateresult[i][1])].append(i)
586 key2idxs[(revs[i], annotateresult[i][1])].append(i)
583 while key2idxs:
587 while key2idxs:
584 # find an unresolved line and its linelog rev to annotate
588 # find an unresolved line and its linelog rev to annotate
585 hsh = None
589 hsh = None
586 try:
590 try:
587 for (rev, _linenum), idxs in key2idxs.items():
591 for (rev, _linenum), idxs in key2idxs.items():
588 if revmap.rev2flag(rev) & revmapmod.sidebranchflag:
592 if revmap.rev2flag(rev) & revmapmod.sidebranchflag:
589 continue
593 continue
590 hsh = annotateresult[idxs[0]][0]
594 hsh = annotateresult[idxs[0]][0]
591 break
595 break
592 except StopIteration: # no more unresolved lines
596 except StopIteration: # no more unresolved lines
593 return result
597 return result
594 if hsh is None:
598 if hsh is None:
595 # the remaining key2idxs are not in main branch, resolving them
599 # the remaining key2idxs are not in main branch, resolving them
596 # using the hard way...
600 # using the hard way...
597 revlines = {}
601 revlines = {}
598 for (rev, linenum), idxs in key2idxs.items():
602 for (rev, linenum), idxs in key2idxs.items():
599 if rev not in revlines:
603 if rev not in revlines:
600 hsh = annotateresult[idxs[0]][0]
604 hsh = annotateresult[idxs[0]][0]
601 if self.ui.debugflag:
605 if self.ui.debugflag:
602 self.ui.debug(
606 self.ui.debug(
603 b'fastannotate: reading %s line #%d '
607 b'fastannotate: reading %s line #%d '
604 b'to resolve lines %r\n'
608 b'to resolve lines %r\n'
605 % (short(hsh), linenum, idxs)
609 % (short(hsh), linenum, idxs)
606 )
610 )
607 fctx = self._resolvefctx(hsh, revmap.rev2path(rev))
611 fctx = self._resolvefctx(hsh, revmap.rev2path(rev))
608 lines = mdiff.splitnewlines(fctx.data())
612 lines = mdiff.splitnewlines(fctx.data())
609 revlines[rev] = lines
613 revlines[rev] = lines
610 for idx in idxs:
614 for idx in idxs:
611 result[idx] = revlines[rev][linenum]
615 result[idx] = revlines[rev][linenum]
612 assert all(x is not None for x in result)
616 assert all(x is not None for x in result)
613 return result
617 return result
614
618
615 # run the annotate and the lines should match to the file content
619 # run the annotate and the lines should match to the file content
616 self.ui.debug(
620 self.ui.debug(
617 b'fastannotate: annotate %s to resolve lines\n' % short(hsh)
621 b'fastannotate: annotate %s to resolve lines\n' % short(hsh)
618 )
622 )
619 linelog.annotate(rev)
623 linelog.annotate(rev)
620 fctx = self._resolvefctx(hsh, revmap.rev2path(rev))
624 fctx = self._resolvefctx(hsh, revmap.rev2path(rev))
621 annotated = linelog.annotateresult
625 annotated = linelog.annotateresult
622 lines = mdiff.splitnewlines(fctx.data())
626 lines = mdiff.splitnewlines(fctx.data())
623 if len(lines) != len(annotated):
627 if len(lines) != len(annotated):
624 raise faerror.CorruptedFileError(b'unexpected annotated lines')
628 raise faerror.CorruptedFileError(b'unexpected annotated lines')
625 # resolve lines from the annotate result
629 # resolve lines from the annotate result
626 for i, line in enumerate(lines):
630 for i, line in enumerate(lines):
627 k = annotated[i]
631 k = annotated[i]
628 if k in key2idxs:
632 if k in key2idxs:
629 for idx in key2idxs[k]:
633 for idx in key2idxs[k]:
630 result[idx] = line
634 result[idx] = line
631 del key2idxs[k]
635 del key2idxs[k]
632 return result
636 return result
633
637
634 def annotatedirectly(self, f, showpath, showlines):
638 def annotatedirectly(self, f, showpath, showlines):
635 """like annotate, but when we know that f is in linelog.
639 """like annotate, but when we know that f is in linelog.
636 f can be either a 20-char str (node) or a fctx. this is for perf - in
640 f can be either a 20-char str (node) or a fctx. this is for perf - in
637 the best case, the user provides a node and we don't need to read the
641 the best case, the user provides a node and we don't need to read the
638 filelog or construct any filecontext.
642 filelog or construct any filecontext.
639 """
643 """
640 if isinstance(f, bytes):
644 if isinstance(f, bytes):
641 hsh = f
645 hsh = f
642 else:
646 else:
643 hsh = f.node()
647 hsh = f.node()
644 llrev = self.revmap.hsh2rev(hsh)
648 llrev = self.revmap.hsh2rev(hsh)
645 if not llrev:
649 if not llrev:
646 raise faerror.CorruptedFileError(b'%s is not in revmap' % hex(hsh))
650 raise faerror.CorruptedFileError(b'%s is not in revmap' % hex(hsh))
647 if (self.revmap.rev2flag(llrev) & revmapmod.sidebranchflag) != 0:
651 if (self.revmap.rev2flag(llrev) & revmapmod.sidebranchflag) != 0:
648 raise faerror.CorruptedFileError(
652 raise faerror.CorruptedFileError(
649 b'%s is not in revmap mainbranch' % hex(hsh)
653 b'%s is not in revmap mainbranch' % hex(hsh)
650 )
654 )
651 self.linelog.annotate(llrev)
655 self.linelog.annotate(llrev)
652 result = [
656 result = [
653 (self.revmap.rev2hsh(r), l) for r, l in self.linelog.annotateresult
657 (self.revmap.rev2hsh(r), l) for r, l in self.linelog.annotateresult
654 ]
658 ]
655 return self._refineannotateresult(result, f, showpath, showlines)
659 return self._refineannotateresult(result, f, showpath, showlines)
656
660
657 def _refineannotateresult(self, result, f, showpath, showlines):
661 def _refineannotateresult(self, result, f, showpath, showlines):
658 """add the missing path or line contents, they can be expensive.
662 """add the missing path or line contents, they can be expensive.
659 f could be either node or fctx.
663 f could be either node or fctx.
660 """
664 """
661 if showpath:
665 if showpath:
662 result = self._addpathtoresult(result)
666 result = self._addpathtoresult(result)
663 if showlines:
667 if showlines:
664 if isinstance(f, bytes): # f: node or fctx
668 if isinstance(f, bytes): # f: node or fctx
665 llrev = self.revmap.hsh2rev(f)
669 llrev = self.revmap.hsh2rev(f)
666 fctx = self._resolvefctx(f, self.revmap.rev2path(llrev))
670 fctx = self._resolvefctx(f, self.revmap.rev2path(llrev))
667 else:
671 else:
668 fctx = f
672 fctx = f
669 lines = mdiff.splitnewlines(fctx.data())
673 lines = mdiff.splitnewlines(fctx.data())
670 if len(lines) != len(result): # linelog is probably corrupted
674 if len(lines) != len(result): # linelog is probably corrupted
671 raise faerror.CorruptedFileError()
675 raise faerror.CorruptedFileError()
672 result = (result, lines)
676 result = (result, lines)
673 return result
677 return result
674
678
675 def _appendrev(self, fctx, blocks, bannotated=None):
679 def _appendrev(self, fctx, blocks, bannotated=None):
676 self._doappendrev(self.linelog, self.revmap, fctx, blocks, bannotated)
680 self._doappendrev(self.linelog, self.revmap, fctx, blocks, bannotated)
677
681
678 def _diffblocks(self, a, b):
682 def _diffblocks(self, a, b):
679 return mdiff.allblocks(a, b, self.opts.diffopts)
683 return mdiff.allblocks(a, b, self.opts.diffopts)
680
684
681 @staticmethod
685 @staticmethod
682 def _doappendrev(linelog, revmap, fctx, blocks, bannotated=None):
686 def _doappendrev(linelog, revmap, fctx, blocks, bannotated=None):
683 """append a revision to linelog and revmap"""
687 """append a revision to linelog and revmap"""
684
688
685 def getllrev(f):
689 def getllrev(f):
686 """(fctx) -> int"""
690 """(fctx) -> int"""
687 # f should not be a linelog revision
691 # f should not be a linelog revision
688 if isinstance(f, int):
692 if isinstance(f, int):
689 raise error.ProgrammingError(b'f should not be an int')
693 raise error.ProgrammingError(b'f should not be an int')
690 # f is a fctx, allocate linelog rev on demand
694 # f is a fctx, allocate linelog rev on demand
691 hsh = f.node()
695 hsh = f.node()
692 rev = revmap.hsh2rev(hsh)
696 rev = revmap.hsh2rev(hsh)
693 if rev is None:
697 if rev is None:
694 rev = revmap.append(hsh, sidebranch=True, path=f.path())
698 rev = revmap.append(hsh, sidebranch=True, path=f.path())
695 return rev
699 return rev
696
700
697 # append sidebranch revisions to revmap
701 # append sidebranch revisions to revmap
698 siderevs = []
702 siderevs = []
699 siderevmap = {} # node: int
703 siderevmap = {} # node: int
700 if bannotated is not None:
704 if bannotated is not None:
701 for (a1, a2, b1, b2), op in blocks:
705 for (a1, a2, b1, b2), op in blocks:
702 if op != b'=':
706 if op != b'=':
703 # f could be either linelong rev, or fctx.
707 # f could be either linelong rev, or fctx.
704 siderevs += [
708 siderevs += [
705 f
709 f
706 for f, l in bannotated[b1:b2]
710 for f, l in bannotated[b1:b2]
707 if not isinstance(f, int)
711 if not isinstance(f, int)
708 ]
712 ]
709 siderevs = set(siderevs)
713 siderevs = set(siderevs)
710 if fctx in siderevs: # mainnode must be appended seperately
714 if fctx in siderevs: # mainnode must be appended seperately
711 siderevs.remove(fctx)
715 siderevs.remove(fctx)
712 for f in siderevs:
716 for f in siderevs:
713 siderevmap[f] = getllrev(f)
717 siderevmap[f] = getllrev(f)
714
718
715 # the changeset in the main branch, could be a merge
719 # the changeset in the main branch, could be a merge
716 llrev = revmap.append(fctx.node(), path=fctx.path())
720 llrev = revmap.append(fctx.node(), path=fctx.path())
717 siderevmap[fctx] = llrev
721 siderevmap[fctx] = llrev
718
722
719 for (a1, a2, b1, b2), op in reversed(blocks):
723 for (a1, a2, b1, b2), op in reversed(blocks):
720 if op == b'=':
724 if op == b'=':
721 continue
725 continue
722 if bannotated is None:
726 if bannotated is None:
723 linelog.replacelines(llrev, a1, a2, b1, b2)
727 linelog.replacelines(llrev, a1, a2, b1, b2)
724 else:
728 else:
725 blines = [
729 blines = [
726 ((r if isinstance(r, int) else siderevmap[r]), l)
730 ((r if isinstance(r, int) else siderevmap[r]), l)
727 for r, l in bannotated[b1:b2]
731 for r, l in bannotated[b1:b2]
728 ]
732 ]
729 linelog.replacelines_vec(llrev, a1, a2, blines)
733 linelog.replacelines_vec(llrev, a1, a2, blines)
730
734
731 def _addpathtoresult(self, annotateresult, revmap=None):
735 def _addpathtoresult(self, annotateresult, revmap=None):
732 """(revmap, [(node, linenum)]) -> [(node, linenum, path)]"""
736 """(revmap, [(node, linenum)]) -> [(node, linenum, path)]"""
733 if revmap is None:
737 if revmap is None:
734 revmap = self.revmap
738 revmap = self.revmap
735
739
736 def _getpath(nodeid):
740 def _getpath(nodeid):
737 path = self._node2path.get(nodeid)
741 path = self._node2path.get(nodeid)
738 if path is None:
742 if path is None:
739 path = revmap.rev2path(revmap.hsh2rev(nodeid))
743 path = revmap.rev2path(revmap.hsh2rev(nodeid))
740 self._node2path[nodeid] = path
744 self._node2path[nodeid] = path
741 return path
745 return path
742
746
743 return [(n, l, _getpath(n)) for n, l in annotateresult]
747 return [(n, l, _getpath(n)) for n, l in annotateresult]
744
748
745 def _checklastmasterhead(self, fctx):
749 def _checklastmasterhead(self, fctx):
746 """check if fctx is the master's head last time, raise if not"""
750 """check if fctx is the master's head last time, raise if not"""
747 if fctx is None:
751 if fctx is None:
748 llrev = 0
752 llrev = 0
749 else:
753 else:
750 llrev = self.revmap.hsh2rev(fctx.node())
754 llrev = self.revmap.hsh2rev(fctx.node())
751 if not llrev:
755 if not llrev:
752 raise faerror.CannotReuseError()
756 raise faerror.CannotReuseError()
753 if self.linelog.maxrev != llrev:
757 if self.linelog.maxrev != llrev:
754 raise faerror.CannotReuseError()
758 raise faerror.CannotReuseError()
755
759
756 @util.propertycache
760 @util.propertycache
757 def _parentfunc(self):
761 def _parentfunc(self):
758 """-> (fctx) -> [fctx]"""
762 """-> (fctx) -> [fctx]"""
759 followrename = self.opts.followrename
763 followrename = self.opts.followrename
760 followmerge = self.opts.followmerge
764 followmerge = self.opts.followmerge
761
765
762 def parents(f):
766 def parents(f):
763 pl = _parents(f, follow=followrename)
767 pl = _parents(f, follow=followrename)
764 if not followmerge:
768 if not followmerge:
765 pl = pl[:1]
769 pl = pl[:1]
766 return pl
770 return pl
767
771
768 return parents
772 return parents
769
773
770 @util.propertycache
774 @util.propertycache
771 def _perfhack(self):
775 def _perfhack(self):
772 return self.ui.configbool(b'fastannotate', b'perfhack')
776 return self.ui.configbool(b'fastannotate', b'perfhack')
773
777
774 def _resolvefctx(self, rev, path=None, **kwds):
778 def _resolvefctx(self, rev, path=None, **kwds):
775 return resolvefctx(self.repo, rev, (path or self.path), **kwds)
779 return resolvefctx(self.repo, rev, (path or self.path), **kwds)
776
780
777
781
778 def _unlinkpaths(paths):
782 def _unlinkpaths(paths):
779 """silent, best-effort unlink"""
783 """silent, best-effort unlink"""
780 for path in paths:
784 for path in paths:
781 try:
785 try:
782 util.unlink(path)
786 util.unlink(path)
783 except OSError:
787 except OSError:
784 pass
788 pass
785
789
786
790
787 class pathhelper:
791 class pathhelper:
788 """helper for getting paths for lockfile, linelog and revmap"""
792 """helper for getting paths for lockfile, linelog and revmap"""
789
793
790 def __init__(self, repo, path, opts=defaultopts):
794 def __init__(self, repo, path, opts=defaultopts):
791 # different options use different directories
795 # different options use different directories
792 self._vfspath = os.path.join(
796 self._vfspath = os.path.join(
793 b'fastannotate', opts.shortstr, encodedir(path)
797 b'fastannotate', opts.shortstr, encodedir(path)
794 )
798 )
795 self._repo = repo
799 self._repo = repo
796
800
797 @property
801 @property
798 def dirname(self):
802 def dirname(self):
799 return os.path.dirname(self._repo.vfs.join(self._vfspath))
803 return os.path.dirname(self._repo.vfs.join(self._vfspath))
800
804
801 @property
805 @property
802 def linelogpath(self):
806 def linelogpath(self):
803 return self._repo.vfs.join(self._vfspath + b'.l')
807 return self._repo.vfs.join(self._vfspath + b'.l')
804
808
805 def lock(self):
809 def lock(self):
806 return lockmod.lock(self._repo.vfs, self._vfspath + b'.lock')
810 return lockmod.lock(self._repo.vfs, self._vfspath + b'.lock')
807
811
808 @property
812 @property
809 def revmappath(self):
813 def revmappath(self):
810 return self._repo.vfs.join(self._vfspath + b'.m')
814 return self._repo.vfs.join(self._vfspath + b'.m')
811
815
812
816
813 @contextlib.contextmanager
817 @contextlib.contextmanager
814 def annotatecontext(repo, path, opts=defaultopts, rebuild=False):
818 def annotatecontext(repo, path, opts=defaultopts, rebuild=False):
815 """context needed to perform (fast) annotate on a file
819 """context needed to perform (fast) annotate on a file
816
820
817 an annotatecontext of a single file consists of two structures: the
821 an annotatecontext of a single file consists of two structures: the
818 linelog and the revmap. this function takes care of locking. only 1
822 linelog and the revmap. this function takes care of locking. only 1
819 process is allowed to write that file's linelog and revmap at a time.
823 process is allowed to write that file's linelog and revmap at a time.
820
824
821 when something goes wrong, this function will assume the linelog and the
825 when something goes wrong, this function will assume the linelog and the
822 revmap are in a bad state, and remove them from disk.
826 revmap are in a bad state, and remove them from disk.
823
827
824 use this function in the following way:
828 use this function in the following way:
825
829
826 with annotatecontext(...) as actx:
830 with annotatecontext(...) as actx:
827 actx. ....
831 actx. ....
828 """
832 """
829 helper = pathhelper(repo, path, opts)
833 helper = pathhelper(repo, path, opts)
830 util.makedirs(helper.dirname)
834 util.makedirs(helper.dirname)
831 revmappath = helper.revmappath
835 revmappath = helper.revmappath
832 linelogpath = helper.linelogpath
836 linelogpath = helper.linelogpath
833 actx = None
837 actx = None
834 try:
838 try:
835 with helper.lock():
839 with helper.lock():
836 actx = _annotatecontext(repo, path, linelogpath, revmappath, opts)
840 actx = _annotatecontext(repo, path, linelogpath, revmappath, opts)
837 if rebuild:
841 if rebuild:
838 actx.rebuild()
842 actx.rebuild()
839 yield actx
843 yield actx
840 except Exception:
844 except Exception:
841 if actx is not None:
845 if actx is not None:
842 actx.rebuild()
846 actx.rebuild()
843 repo.ui.debug(b'fastannotate: %s: cache broken and deleted\n' % path)
847 repo.ui.debug(b'fastannotate: %s: cache broken and deleted\n' % path)
844 raise
848 raise
845 finally:
849 finally:
846 if actx is not None:
850 if actx is not None:
847 actx.close()
851 actx.close()
848
852
849
853
850 def fctxannotatecontext(fctx, follow=True, diffopts=None, rebuild=False):
854 def fctxannotatecontext(fctx, follow=True, diffopts=None, rebuild=False):
851 """like annotatecontext but get the context from a fctx. convenient when
855 """like annotatecontext but get the context from a fctx. convenient when
852 used in fctx.annotate
856 used in fctx.annotate
853 """
857 """
854 repo = fctx._repo
858 repo = fctx._repo
855 path = fctx._path
859 path = fctx._path
856 if repo.ui.configbool(b'fastannotate', b'forcefollow', True):
860 if repo.ui.configbool(b'fastannotate', b'forcefollow', True):
857 follow = True
861 follow = True
858 aopts = annotateopts(diffopts=diffopts, followrename=follow)
862 aopts = annotateopts(diffopts=diffopts, followrename=follow)
859 return annotatecontext(repo, path, aopts, rebuild)
863 return annotatecontext(repo, path, aopts, rebuild)
General Comments 0
You need to be logged in to leave comments. Login now