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