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