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