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