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