##// END OF EJS Templates
wrapfunction: use sysstr instead of bytes as argument in "fastannotate"...
marmoute -
r51670:8dc2724d default
parent child Browse files
Show More
@@ -1,262 +1,262 b''
1 # Copyright 2016-present Facebook. All Rights Reserved.
1 # Copyright 2016-present Facebook. All Rights Reserved.
2 #
2 #
3 # protocol: logic for a server providing fastannotate support
3 # protocol: logic for a server providing fastannotate support
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 import contextlib
8 import contextlib
9 import os
9 import os
10
10
11 from mercurial.i18n import _
11 from mercurial.i18n import _
12 from mercurial.pycompat import open
12 from mercurial.pycompat import open
13 from mercurial import (
13 from mercurial import (
14 error,
14 error,
15 extensions,
15 extensions,
16 hg,
16 hg,
17 util,
17 util,
18 wireprotov1peer,
18 wireprotov1peer,
19 wireprotov1server,
19 wireprotov1server,
20 )
20 )
21 from mercurial.utils import (
21 from mercurial.utils import (
22 urlutil,
22 urlutil,
23 )
23 )
24 from . import context
24 from . import context
25
25
26 # common
26 # common
27
27
28
28
29 def _getmaster(ui):
29 def _getmaster(ui):
30 """get the mainbranch, and enforce it is set"""
30 """get the mainbranch, and enforce it is set"""
31 master = ui.config(b'fastannotate', b'mainbranch')
31 master = ui.config(b'fastannotate', b'mainbranch')
32 if not master:
32 if not master:
33 raise error.Abort(
33 raise error.Abort(
34 _(
34 _(
35 b'fastannotate.mainbranch is required '
35 b'fastannotate.mainbranch is required '
36 b'for both the client and the server'
36 b'for both the client and the server'
37 )
37 )
38 )
38 )
39 return master
39 return master
40
40
41
41
42 # server-side
42 # server-side
43
43
44
44
45 def _capabilities(orig, repo, proto):
45 def _capabilities(orig, repo, proto):
46 result = orig(repo, proto)
46 result = orig(repo, proto)
47 result.append(b'getannotate')
47 result.append(b'getannotate')
48 return result
48 return result
49
49
50
50
51 def _getannotate(repo, proto, path, lastnode):
51 def _getannotate(repo, proto, path, lastnode):
52 # output:
52 # output:
53 # FILE := vfspath + '\0' + str(size) + '\0' + content
53 # FILE := vfspath + '\0' + str(size) + '\0' + content
54 # OUTPUT := '' | FILE + OUTPUT
54 # OUTPUT := '' | FILE + OUTPUT
55 result = b''
55 result = b''
56 buildondemand = repo.ui.configbool(
56 buildondemand = repo.ui.configbool(
57 b'fastannotate', b'serverbuildondemand', True
57 b'fastannotate', b'serverbuildondemand', True
58 )
58 )
59 with context.annotatecontext(repo, path) as actx:
59 with context.annotatecontext(repo, path) as actx:
60 if buildondemand:
60 if buildondemand:
61 # update before responding to the client
61 # update before responding to the client
62 master = _getmaster(repo.ui)
62 master = _getmaster(repo.ui)
63 try:
63 try:
64 if not actx.isuptodate(master):
64 if not actx.isuptodate(master):
65 actx.annotate(master, master)
65 actx.annotate(master, master)
66 except Exception:
66 except Exception:
67 # non-fast-forward move or corrupted. rebuild automically.
67 # non-fast-forward move or corrupted. rebuild automically.
68 actx.rebuild()
68 actx.rebuild()
69 try:
69 try:
70 actx.annotate(master, master)
70 actx.annotate(master, master)
71 except Exception:
71 except Exception:
72 actx.rebuild() # delete files
72 actx.rebuild() # delete files
73 finally:
73 finally:
74 # although the "with" context will also do a close/flush, we
74 # although the "with" context will also do a close/flush, we
75 # need to do it early so we can send the correct respond to
75 # need to do it early so we can send the correct respond to
76 # client.
76 # client.
77 actx.close()
77 actx.close()
78 # send back the full content of revmap and linelog, in the future we
78 # send back the full content of revmap and linelog, in the future we
79 # may want to do some rsync-like fancy updating.
79 # may want to do some rsync-like fancy updating.
80 # the lastnode check is not necessary if the client and the server
80 # the lastnode check is not necessary if the client and the server
81 # agree where the main branch is.
81 # agree where the main branch is.
82 if actx.lastnode != lastnode:
82 if actx.lastnode != lastnode:
83 for p in [actx.revmappath, actx.linelogpath]:
83 for p in [actx.revmappath, actx.linelogpath]:
84 if not os.path.exists(p):
84 if not os.path.exists(p):
85 continue
85 continue
86 with open(p, b'rb') as f:
86 with open(p, b'rb') as f:
87 content = f.read()
87 content = f.read()
88 vfsbaselen = len(repo.vfs.base + b'/')
88 vfsbaselen = len(repo.vfs.base + b'/')
89 relpath = p[vfsbaselen:]
89 relpath = p[vfsbaselen:]
90 result += b'%s\0%d\0%s' % (relpath, len(content), content)
90 result += b'%s\0%d\0%s' % (relpath, len(content), content)
91 return result
91 return result
92
92
93
93
94 def _registerwireprotocommand():
94 def _registerwireprotocommand():
95 if b'getannotate' in wireprotov1server.commands:
95 if b'getannotate' in wireprotov1server.commands:
96 return
96 return
97 wireprotov1server.wireprotocommand(b'getannotate', b'path lastnode')(
97 wireprotov1server.wireprotocommand(b'getannotate', b'path lastnode')(
98 _getannotate
98 _getannotate
99 )
99 )
100
100
101
101
102 def serveruisetup(ui):
102 def serveruisetup(ui):
103 _registerwireprotocommand()
103 _registerwireprotocommand()
104 extensions.wrapfunction(wireprotov1server, b'_capabilities', _capabilities)
104 extensions.wrapfunction(wireprotov1server, '_capabilities', _capabilities)
105
105
106
106
107 # client-side
107 # client-side
108
108
109
109
110 def _parseresponse(payload):
110 def _parseresponse(payload):
111 result = {}
111 result = {}
112 i = 0
112 i = 0
113 l = len(payload) - 1
113 l = len(payload) - 1
114 state = 0 # 0: vfspath, 1: size
114 state = 0 # 0: vfspath, 1: size
115 vfspath = size = b''
115 vfspath = size = b''
116 while i < l:
116 while i < l:
117 ch = payload[i : i + 1]
117 ch = payload[i : i + 1]
118 if ch == b'\0':
118 if ch == b'\0':
119 if state == 1:
119 if state == 1:
120 result[vfspath] = payload[i + 1 : i + 1 + int(size)]
120 result[vfspath] = payload[i + 1 : i + 1 + int(size)]
121 i += int(size)
121 i += int(size)
122 state = 0
122 state = 0
123 vfspath = size = b''
123 vfspath = size = b''
124 elif state == 0:
124 elif state == 0:
125 state = 1
125 state = 1
126 else:
126 else:
127 if state == 1:
127 if state == 1:
128 size += ch
128 size += ch
129 elif state == 0:
129 elif state == 0:
130 vfspath += ch
130 vfspath += ch
131 i += 1
131 i += 1
132 return result
132 return result
133
133
134
134
135 def peersetup(ui, peer):
135 def peersetup(ui, peer):
136 class fastannotatepeer(peer.__class__):
136 class fastannotatepeer(peer.__class__):
137 @wireprotov1peer.batchable
137 @wireprotov1peer.batchable
138 def getannotate(self, path, lastnode=None):
138 def getannotate(self, path, lastnode=None):
139 if not self.capable(b'getannotate'):
139 if not self.capable(b'getannotate'):
140 ui.warn(_(b'remote peer cannot provide annotate cache\n'))
140 ui.warn(_(b'remote peer cannot provide annotate cache\n'))
141 return None, None
141 return None, None
142 else:
142 else:
143 args = {b'path': path, b'lastnode': lastnode or b''}
143 args = {b'path': path, b'lastnode': lastnode or b''}
144 return args, _parseresponse
144 return args, _parseresponse
145
145
146 peer.__class__ = fastannotatepeer
146 peer.__class__ = fastannotatepeer
147
147
148
148
149 @contextlib.contextmanager
149 @contextlib.contextmanager
150 def annotatepeer(repo):
150 def annotatepeer(repo):
151 ui = repo.ui
151 ui = repo.ui
152
152
153 remotedest = ui.config(b'fastannotate', b'remotepath', b'default')
153 remotedest = ui.config(b'fastannotate', b'remotepath', b'default')
154 remotepath = urlutil.get_unique_pull_path_obj(
154 remotepath = urlutil.get_unique_pull_path_obj(
155 b'fastannotate',
155 b'fastannotate',
156 ui,
156 ui,
157 remotedest,
157 remotedest,
158 )
158 )
159 peer = hg.peer(ui, {}, remotepath)
159 peer = hg.peer(ui, {}, remotepath)
160
160
161 try:
161 try:
162 yield peer
162 yield peer
163 finally:
163 finally:
164 peer.close()
164 peer.close()
165
165
166
166
167 def clientfetch(repo, paths, lastnodemap=None, peer=None):
167 def clientfetch(repo, paths, lastnodemap=None, peer=None):
168 """download annotate cache from the server for paths"""
168 """download annotate cache from the server for paths"""
169 if not paths:
169 if not paths:
170 return
170 return
171
171
172 if peer is None:
172 if peer is None:
173 with annotatepeer(repo) as peer:
173 with annotatepeer(repo) as peer:
174 return clientfetch(repo, paths, lastnodemap, peer)
174 return clientfetch(repo, paths, lastnodemap, peer)
175
175
176 if lastnodemap is None:
176 if lastnodemap is None:
177 lastnodemap = {}
177 lastnodemap = {}
178
178
179 ui = repo.ui
179 ui = repo.ui
180 results = []
180 results = []
181 with peer.commandexecutor() as batcher:
181 with peer.commandexecutor() as batcher:
182 ui.debug(b'fastannotate: requesting %d files\n' % len(paths))
182 ui.debug(b'fastannotate: requesting %d files\n' % len(paths))
183 for p in paths:
183 for p in paths:
184 results.append(
184 results.append(
185 batcher.callcommand(
185 batcher.callcommand(
186 b'getannotate',
186 b'getannotate',
187 {b'path': p, b'lastnode': lastnodemap.get(p)},
187 {b'path': p, b'lastnode': lastnodemap.get(p)},
188 )
188 )
189 )
189 )
190
190
191 for result in results:
191 for result in results:
192 r = result.result()
192 r = result.result()
193 # TODO: pconvert these paths on the server?
193 # TODO: pconvert these paths on the server?
194 r = {util.pconvert(p): v for p, v in r.items()}
194 r = {util.pconvert(p): v for p, v in r.items()}
195 for path in sorted(r):
195 for path in sorted(r):
196 # ignore malicious paths
196 # ignore malicious paths
197 if not path.startswith(b'fastannotate/') or b'/../' in (
197 if not path.startswith(b'fastannotate/') or b'/../' in (
198 path + b'/'
198 path + b'/'
199 ):
199 ):
200 ui.debug(
200 ui.debug(
201 b'fastannotate: ignored malicious path %s\n' % path
201 b'fastannotate: ignored malicious path %s\n' % path
202 )
202 )
203 continue
203 continue
204 content = r[path]
204 content = r[path]
205 if ui.debugflag:
205 if ui.debugflag:
206 ui.debug(
206 ui.debug(
207 b'fastannotate: writing %d bytes to %s\n'
207 b'fastannotate: writing %d bytes to %s\n'
208 % (len(content), path)
208 % (len(content), path)
209 )
209 )
210 repo.vfs.makedirs(os.path.dirname(path))
210 repo.vfs.makedirs(os.path.dirname(path))
211 with repo.vfs(path, b'wb') as f:
211 with repo.vfs(path, b'wb') as f:
212 f.write(content)
212 f.write(content)
213
213
214
214
215 def _filterfetchpaths(repo, paths):
215 def _filterfetchpaths(repo, paths):
216 """return a subset of paths whose history is long and need to fetch linelog
216 """return a subset of paths whose history is long and need to fetch linelog
217 from the server. works with remotefilelog and non-remotefilelog repos.
217 from the server. works with remotefilelog and non-remotefilelog repos.
218 """
218 """
219 threshold = repo.ui.configint(b'fastannotate', b'clientfetchthreshold', 10)
219 threshold = repo.ui.configint(b'fastannotate', b'clientfetchthreshold', 10)
220 if threshold <= 0:
220 if threshold <= 0:
221 return paths
221 return paths
222
222
223 result = []
223 result = []
224 for path in paths:
224 for path in paths:
225 try:
225 try:
226 if len(repo.file(path)) >= threshold:
226 if len(repo.file(path)) >= threshold:
227 result.append(path)
227 result.append(path)
228 except Exception: # file not found etc.
228 except Exception: # file not found etc.
229 result.append(path)
229 result.append(path)
230
230
231 return result
231 return result
232
232
233
233
234 def localreposetup(ui, repo):
234 def localreposetup(ui, repo):
235 class fastannotaterepo(repo.__class__):
235 class fastannotaterepo(repo.__class__):
236 def prefetchfastannotate(self, paths, peer=None):
236 def prefetchfastannotate(self, paths, peer=None):
237 master = _getmaster(self.ui)
237 master = _getmaster(self.ui)
238 needupdatepaths = []
238 needupdatepaths = []
239 lastnodemap = {}
239 lastnodemap = {}
240 try:
240 try:
241 for path in _filterfetchpaths(self, paths):
241 for path in _filterfetchpaths(self, paths):
242 with context.annotatecontext(self, path) as actx:
242 with context.annotatecontext(self, path) as actx:
243 if not actx.isuptodate(master, strict=False):
243 if not actx.isuptodate(master, strict=False):
244 needupdatepaths.append(path)
244 needupdatepaths.append(path)
245 lastnodemap[path] = actx.lastnode
245 lastnodemap[path] = actx.lastnode
246 if needupdatepaths:
246 if needupdatepaths:
247 clientfetch(self, needupdatepaths, lastnodemap, peer)
247 clientfetch(self, needupdatepaths, lastnodemap, peer)
248 except Exception as ex:
248 except Exception as ex:
249 # could be directory not writable or so, not fatal
249 # could be directory not writable or so, not fatal
250 self.ui.debug(b'fastannotate: prefetch failed: %r\n' % ex)
250 self.ui.debug(b'fastannotate: prefetch failed: %r\n' % ex)
251
251
252 repo.__class__ = fastannotaterepo
252 repo.__class__ = fastannotaterepo
253
253
254
254
255 def clientreposetup(ui, repo):
255 def clientreposetup(ui, repo):
256 _registerwireprotocommand()
256 _registerwireprotocommand()
257 if repo.local():
257 if repo.local():
258 localreposetup(ui, repo)
258 localreposetup(ui, repo)
259 # TODO: this mutates global state, but only if at least one repo
259 # TODO: this mutates global state, but only if at least one repo
260 # has the extension enabled. This is probably bad for hgweb.
260 # has the extension enabled. This is probably bad for hgweb.
261 if peersetup not in hg.wirepeersetupfuncs:
261 if peersetup not in hg.wirepeersetupfuncs:
262 hg.wirepeersetupfuncs.append(peersetup)
262 hg.wirepeersetupfuncs.append(peersetup)
@@ -1,136 +1,136 b''
1 # Copyright 2016-present Facebook. All Rights Reserved.
1 # Copyright 2016-present Facebook. All Rights Reserved.
2 #
2 #
3 # support: fastannotate support for hgweb, and filectx
3 # support: fastannotate support for hgweb, and filectx
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8
8
9 from mercurial.pycompat import getattr
9 from mercurial.pycompat import getattr
10 from mercurial import (
10 from mercurial import (
11 context as hgcontext,
11 context as hgcontext,
12 dagop,
12 dagop,
13 extensions,
13 extensions,
14 hgweb,
14 hgweb,
15 patch,
15 patch,
16 util,
16 util,
17 )
17 )
18
18
19 from . import (
19 from . import (
20 context,
20 context,
21 revmap,
21 revmap,
22 )
22 )
23
23
24
24
25 class _lazyfctx:
25 class _lazyfctx:
26 """delegates to fctx but do not construct fctx when unnecessary"""
26 """delegates to fctx but do not construct fctx when unnecessary"""
27
27
28 def __init__(self, repo, node, path):
28 def __init__(self, repo, node, path):
29 self._node = node
29 self._node = node
30 self._path = path
30 self._path = path
31 self._repo = repo
31 self._repo = repo
32
32
33 def node(self):
33 def node(self):
34 return self._node
34 return self._node
35
35
36 def path(self):
36 def path(self):
37 return self._path
37 return self._path
38
38
39 @util.propertycache
39 @util.propertycache
40 def _fctx(self):
40 def _fctx(self):
41 return context.resolvefctx(self._repo, self._node, self._path)
41 return context.resolvefctx(self._repo, self._node, self._path)
42
42
43 def __getattr__(self, name):
43 def __getattr__(self, name):
44 return getattr(self._fctx, name)
44 return getattr(self._fctx, name)
45
45
46
46
47 def _convertoutputs(repo, annotated, contents):
47 def _convertoutputs(repo, annotated, contents):
48 """convert fastannotate outputs to vanilla annotate format"""
48 """convert fastannotate outputs to vanilla annotate format"""
49 # fastannotate returns: [(nodeid, linenum, path)], [linecontent]
49 # fastannotate returns: [(nodeid, linenum, path)], [linecontent]
50 # convert to what fctx.annotate returns: [annotateline]
50 # convert to what fctx.annotate returns: [annotateline]
51 results = []
51 results = []
52 fctxmap = {}
52 fctxmap = {}
53 annotateline = dagop.annotateline
53 annotateline = dagop.annotateline
54 for i, (hsh, linenum, path) in enumerate(annotated):
54 for i, (hsh, linenum, path) in enumerate(annotated):
55 if (hsh, path) not in fctxmap:
55 if (hsh, path) not in fctxmap:
56 fctxmap[(hsh, path)] = _lazyfctx(repo, hsh, path)
56 fctxmap[(hsh, path)] = _lazyfctx(repo, hsh, path)
57 # linenum: the user wants 1-based, we have 0-based.
57 # linenum: the user wants 1-based, we have 0-based.
58 lineno = linenum + 1
58 lineno = linenum + 1
59 fctx = fctxmap[(hsh, path)]
59 fctx = fctxmap[(hsh, path)]
60 line = contents[i]
60 line = contents[i]
61 results.append(annotateline(fctx=fctx, lineno=lineno, text=line))
61 results.append(annotateline(fctx=fctx, lineno=lineno, text=line))
62 return results
62 return results
63
63
64
64
65 def _getmaster(fctx):
65 def _getmaster(fctx):
66 """(fctx) -> str"""
66 """(fctx) -> str"""
67 return fctx._repo.ui.config(b'fastannotate', b'mainbranch') or b'default'
67 return fctx._repo.ui.config(b'fastannotate', b'mainbranch') or b'default'
68
68
69
69
70 def _doannotate(fctx, follow=True, diffopts=None):
70 def _doannotate(fctx, follow=True, diffopts=None):
71 """like the vanilla fctx.annotate, but do it via fastannotate, and make
71 """like the vanilla fctx.annotate, but do it via fastannotate, and make
72 the output format compatible with the vanilla fctx.annotate.
72 the output format compatible with the vanilla fctx.annotate.
73 may raise Exception, and always return line numbers.
73 may raise Exception, and always return line numbers.
74 """
74 """
75 master = _getmaster(fctx)
75 master = _getmaster(fctx)
76
76
77 with context.fctxannotatecontext(fctx, follow, diffopts) as ac:
77 with context.fctxannotatecontext(fctx, follow, diffopts) as ac:
78 try:
78 try:
79 annotated, contents = ac.annotate(
79 annotated, contents = ac.annotate(
80 fctx.rev(), master=master, showpath=True, showlines=True
80 fctx.rev(), master=master, showpath=True, showlines=True
81 )
81 )
82 except Exception:
82 except Exception:
83 ac.rebuild() # try rebuild once
83 ac.rebuild() # try rebuild once
84 fctx._repo.ui.debug(
84 fctx._repo.ui.debug(
85 b'fastannotate: %s: rebuilding broken cache\n' % fctx._path
85 b'fastannotate: %s: rebuilding broken cache\n' % fctx._path
86 )
86 )
87 try:
87 try:
88 annotated, contents = ac.annotate(
88 annotated, contents = ac.annotate(
89 fctx.rev(), master=master, showpath=True, showlines=True
89 fctx.rev(), master=master, showpath=True, showlines=True
90 )
90 )
91 except Exception:
91 except Exception:
92 raise
92 raise
93
93
94 assert annotated and contents
94 assert annotated and contents
95 return _convertoutputs(fctx._repo, annotated, contents)
95 return _convertoutputs(fctx._repo, annotated, contents)
96
96
97
97
98 def _hgwebannotate(orig, fctx, ui):
98 def _hgwebannotate(orig, fctx, ui):
99 diffopts = patch.difffeatureopts(
99 diffopts = patch.difffeatureopts(
100 ui, untrusted=True, section=b'annotate', whitespace=True
100 ui, untrusted=True, section=b'annotate', whitespace=True
101 )
101 )
102 return _doannotate(fctx, diffopts=diffopts)
102 return _doannotate(fctx, diffopts=diffopts)
103
103
104
104
105 def _fctxannotate(
105 def _fctxannotate(
106 orig, self, follow=False, linenumber=False, skiprevs=None, diffopts=None
106 orig, self, follow=False, linenumber=False, skiprevs=None, diffopts=None
107 ):
107 ):
108 if skiprevs:
108 if skiprevs:
109 # skiprevs is not supported yet
109 # skiprevs is not supported yet
110 return orig(
110 return orig(
111 self, follow, linenumber, skiprevs=skiprevs, diffopts=diffopts
111 self, follow, linenumber, skiprevs=skiprevs, diffopts=diffopts
112 )
112 )
113 try:
113 try:
114 return _doannotate(self, follow, diffopts)
114 return _doannotate(self, follow, diffopts)
115 except Exception as ex:
115 except Exception as ex:
116 self._repo.ui.debug(
116 self._repo.ui.debug(
117 b'fastannotate: falling back to the vanilla annotate: %r\n' % ex
117 b'fastannotate: falling back to the vanilla annotate: %r\n' % ex
118 )
118 )
119 return orig(self, follow=follow, skiprevs=skiprevs, diffopts=diffopts)
119 return orig(self, follow=follow, skiprevs=skiprevs, diffopts=diffopts)
120
120
121
121
122 def _remotefctxannotate(orig, self, follow=False, skiprevs=None, diffopts=None):
122 def _remotefctxannotate(orig, self, follow=False, skiprevs=None, diffopts=None):
123 # skipset: a set-like used to test if a fctx needs to be downloaded
123 # skipset: a set-like used to test if a fctx needs to be downloaded
124 with context.fctxannotatecontext(self, follow, diffopts) as ac:
124 with context.fctxannotatecontext(self, follow, diffopts) as ac:
125 skipset = revmap.revmap(ac.revmappath)
125 skipset = revmap.revmap(ac.revmappath)
126 return orig(
126 return orig(
127 self, follow, skiprevs=skiprevs, diffopts=diffopts, prefetchskip=skipset
127 self, follow, skiprevs=skiprevs, diffopts=diffopts, prefetchskip=skipset
128 )
128 )
129
129
130
130
131 def replacehgwebannotate():
131 def replacehgwebannotate():
132 extensions.wrapfunction(hgweb.webutil, b'annotate', _hgwebannotate)
132 extensions.wrapfunction(hgweb.webutil, 'annotate', _hgwebannotate)
133
133
134
134
135 def replacefctxannotate():
135 def replacefctxannotate():
136 extensions.wrapfunction(hgcontext.basefilectx, b'annotate', _fctxannotate)
136 extensions.wrapfunction(hgcontext.basefilectx, 'annotate', _fctxannotate)
General Comments 0
You need to be logged in to leave comments. Login now