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