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