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