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