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