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