context.py
856 lines
| 30.8 KiB
| text/x-python
|
PythonLexer
|
r39243 | # Copyright 2016-present Facebook. All Rights Reserved. | ||
# | ||||
# context: context needed to annotate a file | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
from __future__ import absolute_import | ||||
import collections | ||||
import contextlib | ||||
import hashlib | ||||
import os | ||||
from mercurial.i18n import _ | ||||
|
r43357 | from mercurial.pycompat import ( | ||
|
r43359 | getattr, | ||
|
r43357 | open, | ||
setattr, | ||||
) | ||||
|
r39243 | from mercurial import ( | ||
error, | ||||
linelog as linelogmod, | ||||
lock as lockmod, | ||||
mdiff, | ||||
node, | ||||
pycompat, | ||||
scmutil, | ||||
util, | ||||
) | ||||
|
r43346 | from mercurial.utils import stringutil | ||
|
r39243 | |||
from . import ( | ||||
error as faerror, | ||||
revmap as revmapmod, | ||||
) | ||||
# given path, get filelog, cached | ||||
@util.lrucachefunc | ||||
def _getflog(repo, path): | ||||
return repo.file(path) | ||||
|
r43346 | |||
|
r39243 | # extracted from mercurial.context.basefilectx.annotate | ||
def _parents(f, follow=True): | ||||
# Cut _descendantrev here to mitigate the penalty of lazy linkrev | ||||
# adjustment. Otherwise, p._adjustlinkrev() would walk changelog | ||||
# from the topmost introrev (= srcrev) down to p.linkrev() if it | ||||
# isn't an ancestor of the srcrev. | ||||
f._changeid | ||||
pl = f.parents() | ||||
# Don't return renamed parents if we aren't following. | ||||
if not follow: | ||||
pl = [p for p in pl if p.path() == f.path()] | ||||
# renamed filectx won't have a filelog yet, so set it | ||||
# from the cache to save time | ||||
for p in pl: | ||||
|
r43347 | if not b'_filelog' in p.__dict__: | ||
|
r39243 | p._filelog = _getflog(f._repo, p.path()) | ||
return pl | ||||
|
r43346 | |||
|
r39243 | # extracted from mercurial.context.basefilectx.annotate. slightly modified | ||
# so it takes a fctx instead of a pair of text and fctx. | ||||
def _decorate(fctx): | ||||
text = fctx.data() | ||||
|
r43347 | linecount = text.count(b'\n') | ||
if text and not text.endswith(b'\n'): | ||||
|
r39243 | linecount += 1 | ||
return ([(fctx, i) for i in pycompat.xrange(linecount)], text) | ||||
|
r43346 | |||
|
r39243 | # extracted from mercurial.context.basefilectx.annotate. slightly modified | ||
# so it takes an extra "blocks" parameter calculated elsewhere, instead of | ||||
# calculating diff here. | ||||
def _pair(parent, child, blocks): | ||||
for (a1, a2, b1, b2), t in blocks: | ||||
# Changed blocks ('!') or blocks made only of blank lines ('~') | ||||
# belong to the child. | ||||
|
r43347 | if t == b'=': | ||
|
r39243 | child[0][b1:b2] = parent[0][a1:a2] | ||
return child | ||||
|
r43346 | |||
|
r39243 | # like scmutil.revsingle, but with lru cache, so their states (like manifests) | ||
# could be reused | ||||
_revsingle = util.lrucachefunc(scmutil.revsingle) | ||||
|
r43346 | |||
|
r39243 | def resolvefctx(repo, rev, path, resolverev=False, adjustctx=None): | ||
"""(repo, str, str) -> fctx | ||||
get the filectx object from repo, rev, path, in an efficient way. | ||||
if resolverev is True, "rev" is a revision specified by the revset | ||||
language, otherwise "rev" is a nodeid, or a revision number that can | ||||
be consumed by repo.__getitem__. | ||||
if adjustctx is not None, the returned fctx will point to a changeset | ||||
that introduces the change (last modified the file). if adjustctx | ||||
is 'linkrev', trust the linkrev and do not adjust it. this is noticeably | ||||
faster for big repos but is incorrect for some cases. | ||||
""" | ||||
if resolverev and not isinstance(rev, int) and rev is not None: | ||||
ctx = _revsingle(repo, rev) | ||||
else: | ||||
ctx = repo[rev] | ||||
# If we don't need to adjust the linkrev, create the filectx using the | ||||
# changectx instead of using ctx[path]. This means it already has the | ||||
# changectx information, so blame -u will be able to look directly at the | ||||
# commitctx object instead of having to resolve it by going through the | ||||
# manifest. In a lazy-manifest world this can prevent us from downloading a | ||||
# lot of data. | ||||
if adjustctx is None: | ||||
# ctx.rev() is None means it's the working copy, which is a special | ||||
# case. | ||||
if ctx.rev() is None: | ||||
fctx = ctx[path] | ||||
else: | ||||
fctx = repo.filectx(path, changeid=ctx.rev()) | ||||
else: | ||||
fctx = ctx[path] | ||||
|
r43347 | if adjustctx == b'linkrev': | ||
|
r39243 | introrev = fctx.linkrev() | ||
else: | ||||
introrev = fctx.introrev() | ||||
if introrev != ctx.rev(): | ||||
fctx._changeid = introrev | ||||
fctx._changectx = repo[introrev] | ||||
return fctx | ||||
|
r43346 | |||
|
r39243 | # like mercurial.store.encodedir, but use linelog suffixes: .m, .l, .lock | ||
def encodedir(path): | ||||
|
r43346 | return ( | ||
|
r43347 | path.replace(b'.hg/', b'.hg.hg/') | ||
.replace(b'.l/', b'.l.hg/') | ||||
.replace(b'.m/', b'.m.hg/') | ||||
.replace(b'.lock/', b'.lock.hg/') | ||||
|
r43346 | ) | ||
|
r39243 | |||
def hashdiffopts(diffopts): | ||||
|
r43346 | diffoptstr = stringutil.pprint( | ||
sorted((k, getattr(diffopts, k)) for k in mdiff.diffopts.defaults) | ||||
) | ||||
|
r40711 | return node.hex(hashlib.sha1(diffoptstr).digest())[:6] | ||
|
r39243 | |||
|
r43346 | |||
|
r39243 | _defaultdiffopthash = hashdiffopts(mdiff.defaultopts) | ||
|
r43346 | |||
|
r39243 | class annotateopts(object): | ||
"""like mercurial.mdiff.diffopts, but is for annotate | ||||
followrename: follow renames, like "hg annotate -f" | ||||
followmerge: follow p2 of a merge changeset, otherwise p2 is ignored | ||||
""" | ||||
defaults = { | ||||
|
r43347 | b'diffopts': None, | ||
b'followrename': True, | ||||
b'followmerge': True, | ||||
|
r39243 | } | ||
def __init__(self, **opts): | ||||
|
r40706 | opts = pycompat.byteskwargs(opts) | ||
|
r39243 | for k, v in self.defaults.iteritems(): | ||
setattr(self, k, opts.get(k, v)) | ||||
@util.propertycache | ||||
def shortstr(self): | ||||
"""represent opts in a short string, suitable for a directory name""" | ||||
|
r43347 | result = b'' | ||
|
r39243 | if not self.followrename: | ||
|
r43347 | result += b'r0' | ||
|
r39243 | if not self.followmerge: | ||
|
r43347 | result += b'm0' | ||
|
r39243 | if self.diffopts is not None: | ||
assert isinstance(self.diffopts, mdiff.diffopts) | ||||
diffopthash = hashdiffopts(self.diffopts) | ||||
if diffopthash != _defaultdiffopthash: | ||||
|
r43347 | result += b'i' + diffopthash | ||
return result or b'default' | ||||
|
r39243 | |||
|
r43346 | |||
|
r39243 | defaultopts = annotateopts() | ||
|
r43346 | |||
|
r39243 | class _annotatecontext(object): | ||
"""do not use this class directly as it does not use lock to protect | ||||
writes. use "with annotatecontext(...)" instead. | ||||
""" | ||||
def __init__(self, repo, path, linelogpath, revmappath, opts): | ||||
self.repo = repo | ||||
self.ui = repo.ui | ||||
self.path = path | ||||
self.opts = opts | ||||
self.linelogpath = linelogpath | ||||
self.revmappath = revmappath | ||||
self._linelog = None | ||||
self._revmap = None | ||||
|
r43346 | self._node2path = {} # {str: str} | ||
|
r39243 | |||
@property | ||||
def linelog(self): | ||||
if self._linelog is None: | ||||
if os.path.exists(self.linelogpath): | ||||
|
r43347 | with open(self.linelogpath, b'rb') as f: | ||
|
r39243 | try: | ||
self._linelog = linelogmod.linelog.fromdata(f.read()) | ||||
except linelogmod.LineLogError: | ||||
self._linelog = linelogmod.linelog() | ||||
else: | ||||
self._linelog = linelogmod.linelog() | ||||
return self._linelog | ||||
@property | ||||
def revmap(self): | ||||
if self._revmap is None: | ||||
self._revmap = revmapmod.revmap(self.revmappath) | ||||
return self._revmap | ||||
def close(self): | ||||
if self._revmap is not None: | ||||
self._revmap.flush() | ||||
self._revmap = None | ||||
if self._linelog is not None: | ||||
|
r43347 | with open(self.linelogpath, b'wb') as f: | ||
|
r39243 | f.write(self._linelog.encode()) | ||
self._linelog = None | ||||
__del__ = close | ||||
def rebuild(self): | ||||
"""delete linelog and revmap, useful for rebuilding""" | ||||
self.close() | ||||
self._node2path.clear() | ||||
_unlinkpaths([self.revmappath, self.linelogpath]) | ||||
@property | ||||
def lastnode(self): | ||||
"""return last node in revmap, or None if revmap is empty""" | ||||
if self._revmap is None: | ||||
# fast path, read revmap without loading its full content | ||||
return revmapmod.getlastnode(self.revmappath) | ||||
else: | ||||
return self._revmap.rev2hsh(self._revmap.maxrev) | ||||
def isuptodate(self, master, strict=True): | ||||
"""return True if the revmap / linelog is up-to-date, or the file | ||||
does not exist in the master revision. False otherwise. | ||||
it tries to be fast and could return false negatives, because of the | ||||
use of linkrev instead of introrev. | ||||
useful for both server and client to decide whether to update | ||||
fastannotate cache or not. | ||||
if strict is True, even if fctx exists in the revmap, but is not the | ||||
last node, isuptodate will return False. it's good for performance - no | ||||
expensive check was done. | ||||
if strict is False, if fctx exists in the revmap, this function may | ||||
return True. this is useful for the client to skip downloading the | ||||
cache if the client's master is behind the server's. | ||||
""" | ||||
lastnode = self.lastnode | ||||
try: | ||||
f = self._resolvefctx(master, resolverev=True) | ||||
# choose linkrev instead of introrev as the check is meant to be | ||||
# *fast*. | ||||
linknode = self.repo.changelog.node(f.linkrev()) | ||||
if not strict and lastnode and linknode != lastnode: | ||||
# check if f.node() is in the revmap. note: this loads the | ||||
# revmap and can be slow. | ||||
return self.revmap.hsh2rev(linknode) is not None | ||||
# avoid resolving old manifest, or slow adjustlinkrev to be fast, | ||||
# false negatives are acceptable in this case. | ||||
return linknode == lastnode | ||||
except LookupError: | ||||
# master does not have the file, or the revmap is ahead | ||||
return True | ||||
def annotate(self, rev, master=None, showpath=False, showlines=False): | ||||
"""incrementally update the cache so it includes revisions in the main | ||||
branch till 'master'. and run annotate on 'rev', which may or may not be | ||||
included in the main branch. | ||||
if master is None, do not update linelog. | ||||
the first value returned is the annotate result, it is [(node, linenum)] | ||||
by default. [(node, linenum, path)] if showpath is True. | ||||
if showlines is True, a second value will be returned, it is a list of | ||||
corresponding line contents. | ||||
""" | ||||
# the fast path test requires commit hash, convert rev number to hash, | ||||
# so it may hit the fast path. note: in the "fctx" mode, the "annotate" | ||||
# command could give us a revision number even if the user passes a | ||||
# commit hash. | ||||
if isinstance(rev, int): | ||||
rev = node.hex(self.repo.changelog.node(rev)) | ||||
# fast path: if rev is in the main branch already | ||||
directly, revfctx = self.canannotatedirectly(rev) | ||||
if directly: | ||||
if self.ui.debugflag: | ||||
|
r43346 | self.ui.debug( | ||
|
r43347 | b'fastannotate: %s: using fast path ' | ||
b'(resolved fctx: %s)\n' | ||||
|
r43346 | % ( | ||
self.path, | ||||
|
r43347 | stringutil.pprint(util.safehasattr(revfctx, b'node')), | ||
|
r43346 | ) | ||
) | ||||
|
r39243 | return self.annotatedirectly(revfctx, showpath, showlines) | ||
# resolve master | ||||
masterfctx = None | ||||
if master: | ||||
try: | ||||
|
r43346 | masterfctx = self._resolvefctx( | ||
master, resolverev=True, adjustctx=True | ||||
) | ||||
except LookupError: # master does not have the file | ||||
|
r39243 | pass | ||
else: | ||||
|
r43346 | if masterfctx in self.revmap: # no need to update linelog | ||
|
r39243 | masterfctx = None | ||
# ... - @ <- rev (can be an arbitrary changeset, | ||||
# / not necessarily a descendant | ||||
# master -> o of master) | ||||
# | | ||||
# a merge -> o 'o': new changesets in the main branch | ||||
# |\ '#': revisions in the main branch that | ||||
# o * exist in linelog / revmap | ||||
# | . '*': changesets in side branches, or | ||||
# last master -> # . descendants of master | ||||
# | . | ||||
# # * joint: '#', and is a parent of a '*' | ||||
# |/ | ||||
# a joint -> # ^^^^ --- side branches | ||||
# | | ||||
# ^ --- main branch (in linelog) | ||||
# these DFSes are similar to the traditional annotate algorithm. | ||||
# we cannot really reuse the code for perf reason. | ||||
# 1st DFS calculates merges, joint points, and needed. | ||||
# "needed" is a simple reference counting dict to free items in | ||||
# "hist", reducing its memory usage otherwise could be huge. | ||||
initvisit = [revfctx] | ||||
if masterfctx: | ||||
if masterfctx.rev() is None: | ||||
|
r43346 | raise error.Abort( | ||
|
r43347 | _(b'cannot update linelog to wdir()'), | ||
hint=_(b'set fastannotate.mainbranch'), | ||||
|
r43346 | ) | ||
|
r39243 | initvisit.append(masterfctx) | ||
visit = initvisit[:] | ||||
pcache = {} | ||||
needed = {revfctx: 1} | ||||
|
r43346 | hist = {} # {fctx: ([(llrev or fctx, linenum)], text)} | ||
|
r39243 | while visit: | ||
f = visit.pop() | ||||
if f in pcache or f in hist: | ||||
continue | ||||
|
r43346 | if f in self.revmap: # in the old main branch, it's a joint | ||
|
r39243 | llrev = self.revmap.hsh2rev(f.node()) | ||
self.linelog.annotate(llrev) | ||||
result = self.linelog.annotateresult | ||||
hist[f] = (result, f.data()) | ||||
continue | ||||
pl = self._parentfunc(f) | ||||
pcache[f] = pl | ||||
for p in pl: | ||||
needed[p] = needed.get(p, 0) + 1 | ||||
if p not in pcache: | ||||
visit.append(p) | ||||
# 2nd (simple) DFS calculates new changesets in the main branch | ||||
# ('o' nodes in # the above graph), so we know when to update linelog. | ||||
newmainbranch = set() | ||||
f = masterfctx | ||||
while f and f not in self.revmap: | ||||
newmainbranch.add(f) | ||||
pl = pcache[f] | ||||
if pl: | ||||
f = pl[0] | ||||
else: | ||||
f = None | ||||
break | ||||
# f, if present, is the position where the last build stopped at, and | ||||
# should be the "master" last time. check to see if we can continue | ||||
# building the linelog incrementally. (we cannot if diverged) | ||||
if masterfctx is not None: | ||||
self._checklastmasterhead(f) | ||||
if self.ui.debugflag: | ||||
if newmainbranch: | ||||
|
r43346 | self.ui.debug( | ||
|
r43347 | b'fastannotate: %s: %d new changesets in the main' | ||
b' branch\n' % (self.path, len(newmainbranch)) | ||||
|
r43346 | ) | ||
elif not hist: # no joints, no updates | ||||
self.ui.debug( | ||||
|
r43347 | b'fastannotate: %s: linelog cannot help in ' | ||
b'annotating this revision\n' % self.path | ||||
|
r43346 | ) | ||
|
r39243 | |||
# prepare annotateresult so we can update linelog incrementally | ||||
self.linelog.annotate(self.linelog.maxrev) | ||||
# 3rd DFS does the actual annotate | ||||
visit = initvisit[:] | ||||
|
r43346 | progress = self.ui.makeprogress( | ||
|
r43347 | b'building cache', total=len(newmainbranch) | ||
|
r43346 | ) | ||
|
r39243 | while visit: | ||
f = visit[-1] | ||||
if f in hist: | ||||
visit.pop() | ||||
continue | ||||
ready = True | ||||
pl = pcache[f] | ||||
for p in pl: | ||||
if p not in hist: | ||||
ready = False | ||||
visit.append(p) | ||||
if not ready: | ||||
continue | ||||
visit.pop() | ||||
|
r43346 | blocks = None # mdiff blocks, used for appending linelog | ||
ismainbranch = f in newmainbranch | ||||
|
r39243 | # curr is the same as the traditional annotate algorithm, | ||
# if we only care about linear history (do not follow merge), | ||||
# then curr is not actually used. | ||||
assert f not in hist | ||||
curr = _decorate(f) | ||||
for i, p in enumerate(pl): | ||||
bs = list(self._diffblocks(hist[p][1], curr[1])) | ||||
if i == 0 and ismainbranch: | ||||
blocks = bs | ||||
curr = _pair(hist[p], curr, bs) | ||||
if needed[p] == 1: | ||||
del hist[p] | ||||
del needed[p] | ||||
else: | ||||
needed[p] -= 1 | ||||
hist[f] = curr | ||||
del pcache[f] | ||||
|
r43346 | if ismainbranch: # need to write to linelog | ||
|
r40874 | progress.increment() | ||
|
r39243 | bannotated = None | ||
|
r43346 | if len(pl) == 2 and self.opts.followmerge: # merge | ||
|
r39243 | bannotated = curr[0] | ||
|
r43346 | if blocks is None: # no parents, add an empty one | ||
|
r43347 | blocks = list(self._diffblocks(b'', curr[1])) | ||
|
r39243 | self._appendrev(f, blocks, bannotated) | ||
|
r43346 | elif showpath: # not append linelog, but we need to record path | ||
|
r39243 | self._node2path[f.node()] = f.path() | ||
|
r40874 | progress.complete() | ||
|
r39243 | |||
result = [ | ||||
((self.revmap.rev2hsh(fr) if isinstance(fr, int) else fr.node()), l) | ||||
|
r43346 | for fr, l in hist[revfctx][0] | ||
] # [(node, linenumber)] | ||||
|
r39243 | return self._refineannotateresult(result, revfctx, showpath, showlines) | ||
def canannotatedirectly(self, rev): | ||||
"""(str) -> bool, fctx or node. | ||||
return (True, f) if we can annotate without updating the linelog, pass | ||||
f to annotatedirectly. | ||||
return (False, f) if we need extra calculation. f is the fctx resolved | ||||
from rev. | ||||
""" | ||||
result = True | ||||
f = None | ||||
if not isinstance(rev, int) and rev is not None: | ||||
hsh = {20: bytes, 40: node.bin}.get(len(rev), lambda x: None)(rev) | ||||
if hsh is not None and (hsh, self.path) in self.revmap: | ||||
f = hsh | ||||
if f is None: | ||||
|
r43347 | adjustctx = b'linkrev' if self._perfhack else True | ||
|
r39243 | f = self._resolvefctx(rev, adjustctx=adjustctx, resolverev=True) | ||
result = f in self.revmap | ||||
if not result and self._perfhack: | ||||
# redo the resolution without perfhack - as we are going to | ||||
# do write operations, we need a correct fctx. | ||||
f = self._resolvefctx(rev, adjustctx=True, resolverev=True) | ||||
return result, f | ||||
def annotatealllines(self, rev, showpath=False, showlines=False): | ||||
"""(rev : str) -> [(node : str, linenum : int, path : str)] | ||||
the result has the same format with annotate, but include all (including | ||||
deleted) lines up to rev. call this after calling annotate(rev, ...) for | ||||
better performance and accuracy. | ||||
""" | ||||
revfctx = self._resolvefctx(rev, resolverev=True, adjustctx=True) | ||||
# find a chain from rev to anything in the mainbranch | ||||
if revfctx not in self.revmap: | ||||
chain = [revfctx] | ||||
|
r43347 | a = b'' | ||
|
r39243 | while True: | ||
f = chain[-1] | ||||
pl = self._parentfunc(f) | ||||
if not pl: | ||||
break | ||||
if pl[0] in self.revmap: | ||||
a = pl[0].data() | ||||
break | ||||
chain.append(pl[0]) | ||||
# both self.linelog and self.revmap is backed by filesystem. now | ||||
# we want to modify them but do not want to write changes back to | ||||
# files. so we create in-memory objects and copy them. it's like | ||||
# a "fork". | ||||
linelog = linelogmod.linelog() | ||||
linelog.copyfrom(self.linelog) | ||||
linelog.annotate(linelog.maxrev) | ||||
revmap = revmapmod.revmap() | ||||
revmap.copyfrom(self.revmap) | ||||
for f in reversed(chain): | ||||
b = f.data() | ||||
blocks = list(self._diffblocks(a, b)) | ||||
self._doappendrev(linelog, revmap, f, blocks) | ||||
a = b | ||||
else: | ||||
# fastpath: use existing linelog, revmap as we don't write to them | ||||
linelog = self.linelog | ||||
revmap = self.revmap | ||||
lines = linelog.getalllines() | ||||
hsh = revfctx.node() | ||||
llrev = revmap.hsh2rev(hsh) | ||||
result = [(revmap.rev2hsh(r), l) for r, l in lines if r <= llrev] | ||||
# cannot use _refineannotateresult since we need custom logic for | ||||
# resolving line contents | ||||
if showpath: | ||||
result = self._addpathtoresult(result, revmap) | ||||
if showlines: | ||||
linecontents = self._resolvelines(result, revmap, linelog) | ||||
result = (result, linecontents) | ||||
return result | ||||
def _resolvelines(self, annotateresult, revmap, linelog): | ||||
"""(annotateresult) -> [line]. designed for annotatealllines. | ||||
this is probably the most inefficient code in the whole fastannotate | ||||
directory. but we have made a decision that the linelog does not | ||||
store line contents. so getting them requires random accesses to | ||||
the revlog data, since they can be many, it can be very slow. | ||||
""" | ||||
# [llrev] | ||||
revs = [revmap.hsh2rev(l[0]) for l in annotateresult] | ||||
result = [None] * len(annotateresult) | ||||
# {(rev, linenum): [lineindex]} | ||||
key2idxs = collections.defaultdict(list) | ||||
for i in pycompat.xrange(len(result)): | ||||
key2idxs[(revs[i], annotateresult[i][1])].append(i) | ||||
while key2idxs: | ||||
# find an unresolved line and its linelog rev to annotate | ||||
hsh = None | ||||
try: | ||||
for (rev, _linenum), idxs in key2idxs.iteritems(): | ||||
if revmap.rev2flag(rev) & revmapmod.sidebranchflag: | ||||
continue | ||||
hsh = annotateresult[idxs[0]][0] | ||||
break | ||||
|
r43346 | except StopIteration: # no more unresolved lines | ||
|
r39243 | return result | ||
if hsh is None: | ||||
# the remaining key2idxs are not in main branch, resolving them | ||||
# using the hard way... | ||||
revlines = {} | ||||
for (rev, linenum), idxs in key2idxs.iteritems(): | ||||
if rev not in revlines: | ||||
hsh = annotateresult[idxs[0]][0] | ||||
if self.ui.debugflag: | ||||
|
r43346 | self.ui.debug( | ||
|
r43347 | b'fastannotate: reading %s line #%d ' | ||
b'to resolve lines %r\n' | ||||
|
r43346 | % (node.short(hsh), linenum, idxs) | ||
) | ||||
|
r39243 | fctx = self._resolvefctx(hsh, revmap.rev2path(rev)) | ||
lines = mdiff.splitnewlines(fctx.data()) | ||||
revlines[rev] = lines | ||||
for idx in idxs: | ||||
result[idx] = revlines[rev][linenum] | ||||
assert all(x is not None for x in result) | ||||
return result | ||||
# run the annotate and the lines should match to the file content | ||||
|
r43346 | self.ui.debug( | ||
|
r43347 | b'fastannotate: annotate %s to resolve lines\n' | ||
% node.short(hsh) | ||||
|
r43346 | ) | ||
|
r39243 | linelog.annotate(rev) | ||
fctx = self._resolvefctx(hsh, revmap.rev2path(rev)) | ||||
annotated = linelog.annotateresult | ||||
lines = mdiff.splitnewlines(fctx.data()) | ||||
if len(lines) != len(annotated): | ||||
|
r43347 | raise faerror.CorruptedFileError(b'unexpected annotated lines') | ||
|
r39243 | # resolve lines from the annotate result | ||
for i, line in enumerate(lines): | ||||
k = annotated[i] | ||||
if k in key2idxs: | ||||
for idx in key2idxs[k]: | ||||
result[idx] = line | ||||
del key2idxs[k] | ||||
return result | ||||
def annotatedirectly(self, f, showpath, showlines): | ||||
"""like annotate, but when we know that f is in linelog. | ||||
f can be either a 20-char str (node) or a fctx. this is for perf - in | ||||
the best case, the user provides a node and we don't need to read the | ||||
filelog or construct any filecontext. | ||||
""" | ||||
|
r41297 | if isinstance(f, bytes): | ||
|
r39243 | hsh = f | ||
else: | ||||
hsh = f.node() | ||||
llrev = self.revmap.hsh2rev(hsh) | ||||
if not llrev: | ||||
|
r43346 | raise faerror.CorruptedFileError( | ||
|
r43347 | b'%s is not in revmap' % node.hex(hsh) | ||
|
r43346 | ) | ||
|
r39243 | if (self.revmap.rev2flag(llrev) & revmapmod.sidebranchflag) != 0: | ||
|
r43346 | raise faerror.CorruptedFileError( | ||
|
r43347 | b'%s is not in revmap mainbranch' % node.hex(hsh) | ||
|
r43346 | ) | ||
|
r39243 | self.linelog.annotate(llrev) | ||
|
r43346 | result = [ | ||
(self.revmap.rev2hsh(r), l) for r, l in self.linelog.annotateresult | ||||
] | ||||
|
r39243 | return self._refineannotateresult(result, f, showpath, showlines) | ||
def _refineannotateresult(self, result, f, showpath, showlines): | ||||
"""add the missing path or line contents, they can be expensive. | ||||
f could be either node or fctx. | ||||
""" | ||||
if showpath: | ||||
result = self._addpathtoresult(result) | ||||
if showlines: | ||||
|
r43346 | if isinstance(f, bytes): # f: node or fctx | ||
|
r39243 | llrev = self.revmap.hsh2rev(f) | ||
fctx = self._resolvefctx(f, self.revmap.rev2path(llrev)) | ||||
else: | ||||
fctx = f | ||||
lines = mdiff.splitnewlines(fctx.data()) | ||||
|
r43346 | if len(lines) != len(result): # linelog is probably corrupted | ||
|
r39243 | raise faerror.CorruptedFileError() | ||
result = (result, lines) | ||||
return result | ||||
def _appendrev(self, fctx, blocks, bannotated=None): | ||||
self._doappendrev(self.linelog, self.revmap, fctx, blocks, bannotated) | ||||
def _diffblocks(self, a, b): | ||||
return mdiff.allblocks(a, b, self.opts.diffopts) | ||||
@staticmethod | ||||
def _doappendrev(linelog, revmap, fctx, blocks, bannotated=None): | ||||
"""append a revision to linelog and revmap""" | ||||
def getllrev(f): | ||||
"""(fctx) -> int""" | ||||
# f should not be a linelog revision | ||||
if isinstance(f, int): | ||||
|
r43347 | raise error.ProgrammingError(b'f should not be an int') | ||
|
r39243 | # f is a fctx, allocate linelog rev on demand | ||
hsh = f.node() | ||||
rev = revmap.hsh2rev(hsh) | ||||
if rev is None: | ||||
rev = revmap.append(hsh, sidebranch=True, path=f.path()) | ||||
return rev | ||||
# append sidebranch revisions to revmap | ||||
siderevs = [] | ||||
|
r43346 | siderevmap = {} # node: int | ||
|
r39243 | if bannotated is not None: | ||
for (a1, a2, b1, b2), op in blocks: | ||||
|
r43347 | if op != b'=': | ||
|
r39243 | # f could be either linelong rev, or fctx. | ||
|
r43346 | siderevs += [ | ||
f | ||||
for f, l in bannotated[b1:b2] | ||||
if not isinstance(f, int) | ||||
] | ||||
|
r39243 | siderevs = set(siderevs) | ||
|
r43346 | if fctx in siderevs: # mainnode must be appended seperately | ||
|
r39243 | siderevs.remove(fctx) | ||
for f in siderevs: | ||||
siderevmap[f] = getllrev(f) | ||||
# the changeset in the main branch, could be a merge | ||||
llrev = revmap.append(fctx.node(), path=fctx.path()) | ||||
siderevmap[fctx] = llrev | ||||
for (a1, a2, b1, b2), op in reversed(blocks): | ||||
|
r43347 | if op == b'=': | ||
|
r39243 | continue | ||
if bannotated is None: | ||||
linelog.replacelines(llrev, a1, a2, b1, b2) | ||||
else: | ||||
|
r43346 | blines = [ | ||
((r if isinstance(r, int) else siderevmap[r]), l) | ||||
for r, l in bannotated[b1:b2] | ||||
] | ||||
|
r39243 | linelog.replacelines_vec(llrev, a1, a2, blines) | ||
def _addpathtoresult(self, annotateresult, revmap=None): | ||||
"""(revmap, [(node, linenum)]) -> [(node, linenum, path)]""" | ||||
if revmap is None: | ||||
revmap = self.revmap | ||||
def _getpath(nodeid): | ||||
path = self._node2path.get(nodeid) | ||||
if path is None: | ||||
path = revmap.rev2path(revmap.hsh2rev(nodeid)) | ||||
self._node2path[nodeid] = path | ||||
return path | ||||
return [(n, l, _getpath(n)) for n, l in annotateresult] | ||||
def _checklastmasterhead(self, fctx): | ||||
"""check if fctx is the master's head last time, raise if not""" | ||||
if fctx is None: | ||||
llrev = 0 | ||||
else: | ||||
llrev = self.revmap.hsh2rev(fctx.node()) | ||||
if not llrev: | ||||
raise faerror.CannotReuseError() | ||||
if self.linelog.maxrev != llrev: | ||||
raise faerror.CannotReuseError() | ||||
@util.propertycache | ||||
def _parentfunc(self): | ||||
"""-> (fctx) -> [fctx]""" | ||||
followrename = self.opts.followrename | ||||
followmerge = self.opts.followmerge | ||||
|
r43346 | |||
|
r39243 | def parents(f): | ||
pl = _parents(f, follow=followrename) | ||||
if not followmerge: | ||||
pl = pl[:1] | ||||
return pl | ||||
|
r43346 | |||
|
r39243 | return parents | ||
@util.propertycache | ||||
def _perfhack(self): | ||||
|
r43347 | return self.ui.configbool(b'fastannotate', b'perfhack') | ||
|
r39243 | |||
def _resolvefctx(self, rev, path=None, **kwds): | ||||
return resolvefctx(self.repo, rev, (path or self.path), **kwds) | ||||
|
r43346 | |||
|
r39243 | def _unlinkpaths(paths): | ||
"""silent, best-effort unlink""" | ||||
for path in paths: | ||||
try: | ||||
util.unlink(path) | ||||
except OSError: | ||||
pass | ||||
|
r43346 | |||
|
r39243 | class pathhelper(object): | ||
"""helper for getting paths for lockfile, linelog and revmap""" | ||||
def __init__(self, repo, path, opts=defaultopts): | ||||
# different options use different directories | ||||
|
r43346 | self._vfspath = os.path.join( | ||
|
r43347 | b'fastannotate', opts.shortstr, encodedir(path) | ||
|
r43346 | ) | ||
|
r39243 | self._repo = repo | ||
@property | ||||
def dirname(self): | ||||
return os.path.dirname(self._repo.vfs.join(self._vfspath)) | ||||
@property | ||||
def linelogpath(self): | ||||
|
r43347 | return self._repo.vfs.join(self._vfspath + b'.l') | ||
|
r39243 | |||
def lock(self): | ||||
|
r43347 | return lockmod.lock(self._repo.vfs, self._vfspath + b'.lock') | ||
|
r39243 | |||
@property | ||||
def revmappath(self): | ||||
|
r43347 | return self._repo.vfs.join(self._vfspath + b'.m') | ||
|
r39243 | |||
|
r43346 | |||
|
r39243 | @contextlib.contextmanager | ||
def annotatecontext(repo, path, opts=defaultopts, rebuild=False): | ||||
"""context needed to perform (fast) annotate on a file | ||||
an annotatecontext of a single file consists of two structures: the | ||||
linelog and the revmap. this function takes care of locking. only 1 | ||||
process is allowed to write that file's linelog and revmap at a time. | ||||
when something goes wrong, this function will assume the linelog and the | ||||
revmap are in a bad state, and remove them from disk. | ||||
use this function in the following way: | ||||
with annotatecontext(...) as actx: | ||||
actx. .... | ||||
""" | ||||
helper = pathhelper(repo, path, opts) | ||||
util.makedirs(helper.dirname) | ||||
revmappath = helper.revmappath | ||||
linelogpath = helper.linelogpath | ||||
actx = None | ||||
try: | ||||
with helper.lock(): | ||||
actx = _annotatecontext(repo, path, linelogpath, revmappath, opts) | ||||
if rebuild: | ||||
actx.rebuild() | ||||
yield actx | ||||
except Exception: | ||||
if actx is not None: | ||||
actx.rebuild() | ||||
|
r43347 | repo.ui.debug(b'fastannotate: %s: cache broken and deleted\n' % path) | ||
|
r39243 | raise | ||
finally: | ||||
if actx is not None: | ||||
actx.close() | ||||
|
r43346 | |||
|
r39243 | def fctxannotatecontext(fctx, follow=True, diffopts=None, rebuild=False): | ||
"""like annotatecontext but get the context from a fctx. convenient when | ||||
used in fctx.annotate | ||||
""" | ||||
repo = fctx._repo | ||||
path = fctx._path | ||||
|
r43347 | if repo.ui.configbool(b'fastannotate', b'forcefollow', True): | ||
|
r39243 | follow = True | ||
aopts = annotateopts(diffopts=diffopts, followrename=follow) | ||||
return annotatecontext(repo, path, aopts, rebuild) | ||||