##// END OF EJS Templates
fastannotate: process files as they arrive...
Martin von Zweigbergk -
r39751:d8a7690c default
parent child Browse files
Show More
@@ -1,228 +1,228 b''
1 1 # Copyright 2016-present Facebook. All Rights Reserved.
2 2 #
3 3 # protocol: logic for a server providing fastannotate support
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7 from __future__ import absolute_import
8 8
9 9 import contextlib
10 10 import os
11 11
12 12 from mercurial.i18n import _
13 13 from mercurial import (
14 14 error,
15 15 extensions,
16 16 hg,
17 17 util,
18 18 wireprotov1peer,
19 19 wireprotov1server,
20 20 )
21 21 from . import context
22 22
23 23 # common
24 24
25 25 def _getmaster(ui):
26 26 """get the mainbranch, and enforce it is set"""
27 27 master = ui.config('fastannotate', 'mainbranch')
28 28 if not master:
29 29 raise error.Abort(_('fastannotate.mainbranch is required '
30 30 'for both the client and the server'))
31 31 return master
32 32
33 33 # server-side
34 34
35 35 def _capabilities(orig, repo, proto):
36 36 result = orig(repo, proto)
37 37 result.append('getannotate')
38 38 return result
39 39
40 40 def _getannotate(repo, proto, path, lastnode):
41 41 # output:
42 42 # FILE := vfspath + '\0' + str(size) + '\0' + content
43 43 # OUTPUT := '' | FILE + OUTPUT
44 44 result = ''
45 45 buildondemand = repo.ui.configbool('fastannotate', 'serverbuildondemand',
46 46 True)
47 47 with context.annotatecontext(repo, path) as actx:
48 48 if buildondemand:
49 49 # update before responding to the client
50 50 master = _getmaster(repo.ui)
51 51 try:
52 52 if not actx.isuptodate(master):
53 53 actx.annotate(master, master)
54 54 except Exception:
55 55 # non-fast-forward move or corrupted. rebuild automically.
56 56 actx.rebuild()
57 57 try:
58 58 actx.annotate(master, master)
59 59 except Exception:
60 60 actx.rebuild() # delete files
61 61 finally:
62 62 # although the "with" context will also do a close/flush, we
63 63 # need to do it early so we can send the correct respond to
64 64 # client.
65 65 actx.close()
66 66 # send back the full content of revmap and linelog, in the future we
67 67 # may want to do some rsync-like fancy updating.
68 68 # the lastnode check is not necessary if the client and the server
69 69 # agree where the main branch is.
70 70 if actx.lastnode != lastnode:
71 71 for p in [actx.revmappath, actx.linelogpath]:
72 72 if not os.path.exists(p):
73 73 continue
74 74 content = ''
75 75 with open(p, 'rb') as f:
76 76 content = f.read()
77 77 vfsbaselen = len(repo.vfs.base + '/')
78 78 relpath = p[vfsbaselen:]
79 79 result += '%s\0%d\0%s' % (relpath, len(content), content)
80 80 return result
81 81
82 82 def _registerwireprotocommand():
83 83 if 'getannotate' in wireprotov1server.commands:
84 84 return
85 85 wireprotov1server.wireprotocommand(
86 86 'getannotate', 'path lastnode')(_getannotate)
87 87
88 88 def serveruisetup(ui):
89 89 _registerwireprotocommand()
90 90 extensions.wrapfunction(wireprotov1server, '_capabilities', _capabilities)
91 91
92 92 # client-side
93 93
94 94 def _parseresponse(payload):
95 95 result = {}
96 96 i = 0
97 97 l = len(payload) - 1
98 98 state = 0 # 0: vfspath, 1: size
99 99 vfspath = size = ''
100 100 while i < l:
101 101 ch = payload[i]
102 102 if ch == '\0':
103 103 if state == 1:
104 104 result[vfspath] = buffer(payload, i + 1, int(size))
105 105 i += int(size)
106 106 state = 0
107 107 vfspath = size = ''
108 108 elif state == 0:
109 109 state = 1
110 110 else:
111 111 if state == 1:
112 112 size += ch
113 113 elif state == 0:
114 114 vfspath += ch
115 115 i += 1
116 116 return result
117 117
118 118 def peersetup(ui, peer):
119 119 class fastannotatepeer(peer.__class__):
120 120 @wireprotov1peer.batchable
121 121 def getannotate(self, path, lastnode=None):
122 122 if not self.capable('getannotate'):
123 123 ui.warn(_('remote peer cannot provide annotate cache\n'))
124 124 yield None, None
125 125 else:
126 126 args = {'path': path, 'lastnode': lastnode or ''}
127 127 f = wireprotov1peer.future()
128 128 yield args, f
129 129 yield _parseresponse(f.value)
130 130 peer.__class__ = fastannotatepeer
131 131
132 132 @contextlib.contextmanager
133 133 def annotatepeer(repo):
134 134 ui = repo.ui
135 135
136 136 remotepath = ui.expandpath(
137 137 ui.config('fastannotate', 'remotepath', 'default'))
138 138 peer = hg.peer(ui, {}, remotepath)
139 139
140 140 try:
141 141 yield peer
142 142 finally:
143 143 peer.close()
144 144
145 145 def clientfetch(repo, paths, lastnodemap=None, peer=None):
146 146 """download annotate cache from the server for paths"""
147 147 if not paths:
148 148 return
149 149
150 150 if peer is None:
151 151 with annotatepeer(repo) as peer:
152 152 return clientfetch(repo, paths, lastnodemap, peer)
153 153
154 154 if lastnodemap is None:
155 155 lastnodemap = {}
156 156
157 157 ui = repo.ui
158 158 results = []
159 159 with peer.commandexecutor() as batcher:
160 160 ui.debug('fastannotate: requesting %d files\n' % len(paths))
161 161 for p in paths:
162 162 results.append(batcher.callcommand(
163 163 'getannotate',
164 164 {'path': p, 'lastnode':lastnodemap.get(p)}))
165 165
166 ui.debug('fastannotate: server returned\n')
167 166 for result in results:
168 167 r = result.result()
169 168 # TODO: pconvert these paths on the server?
170 169 r = {util.pconvert(p): v for p, v in r.iteritems()}
171 170 for path in sorted(r):
172 171 # ignore malicious paths
173 if not path.startswith('fastannotate/') or '/../' in (path + '/'):
172 if (not path.startswith('fastannotate/')
173 or '/../' in (path + '/')):
174 174 ui.debug('fastannotate: ignored malicious path %s\n' % path)
175 175 continue
176 176 content = r[path]
177 177 if ui.debugflag:
178 178 ui.debug('fastannotate: writing %d bytes to %s\n'
179 179 % (len(content), path))
180 180 repo.vfs.makedirs(os.path.dirname(path))
181 181 with repo.vfs(path, 'wb') as f:
182 182 f.write(content)
183 183
184 184 def _filterfetchpaths(repo, paths):
185 185 """return a subset of paths whose history is long and need to fetch linelog
186 186 from the server. works with remotefilelog and non-remotefilelog repos.
187 187 """
188 188 threshold = repo.ui.configint('fastannotate', 'clientfetchthreshold', 10)
189 189 if threshold <= 0:
190 190 return paths
191 191
192 192 result = []
193 193 for path in paths:
194 194 try:
195 195 if len(repo.file(path)) >= threshold:
196 196 result.append(path)
197 197 except Exception: # file not found etc.
198 198 result.append(path)
199 199
200 200 return result
201 201
202 202 def localreposetup(ui, repo):
203 203 class fastannotaterepo(repo.__class__):
204 204 def prefetchfastannotate(self, paths, peer=None):
205 205 master = _getmaster(self.ui)
206 206 needupdatepaths = []
207 207 lastnodemap = {}
208 208 try:
209 209 for path in _filterfetchpaths(self, paths):
210 210 with context.annotatecontext(self, path) as actx:
211 211 if not actx.isuptodate(master, strict=False):
212 212 needupdatepaths.append(path)
213 213 lastnodemap[path] = actx.lastnode
214 214 if needupdatepaths:
215 215 clientfetch(self, needupdatepaths, lastnodemap, peer)
216 216 except Exception as ex:
217 217 # could be directory not writable or so, not fatal
218 218 self.ui.debug('fastannotate: prefetch failed: %r\n' % ex)
219 219 repo.__class__ = fastannotaterepo
220 220
221 221 def clientreposetup(ui, repo):
222 222 _registerwireprotocommand()
223 223 if repo.local():
224 224 localreposetup(ui, repo)
225 225 # TODO: this mutates global state, but only if at least one repo
226 226 # has the extension enabled. This is probably bad for hgweb.
227 227 if peersetup not in hg.wirepeersetupfuncs:
228 228 hg.wirepeersetupfuncs.append(peersetup)
@@ -1,215 +1,211 b''
1 1 $ cat >> $HGRCPATH << EOF
2 2 > [ui]
3 3 > ssh = "$PYTHON" "$TESTDIR/dummyssh"
4 4 > [extensions]
5 5 > fastannotate=
6 6 > [fastannotate]
7 7 > mainbranch=@
8 8 > EOF
9 9
10 10 $ HGMERGE=true; export HGMERGE
11 11
12 12 setup the server repo
13 13
14 14 $ hg init repo-server
15 15 $ cd repo-server
16 16 $ cat >> .hg/hgrc << EOF
17 17 > [fastannotate]
18 18 > server=1
19 19 > EOF
20 20 $ for i in 1 2 3 4; do
21 21 > echo $i >> a
22 22 > hg commit -A -m $i a
23 23 > done
24 24 $ [ -d .hg/fastannotate ]
25 25 [1]
26 26 $ hg bookmark @
27 27 $ cd ..
28 28
29 29 setup the local repo
30 30
31 31 $ hg clone 'ssh://user@dummy/repo-server' repo-local -q
32 32 $ cd repo-local
33 33 $ cat >> .hg/hgrc << EOF
34 34 > [fastannotate]
35 35 > client=1
36 36 > clientfetchthreshold=0
37 37 > EOF
38 38 $ [ -d .hg/fastannotate ]
39 39 [1]
40 40 $ hg fastannotate a --debug
41 41 running * (glob)
42 42 sending hello command
43 43 sending between command
44 44 remote: * (glob) (?)
45 45 remote: capabilities: * (glob)
46 46 remote: * (glob) (?)
47 47 sending protocaps command
48 48 fastannotate: requesting 1 files
49 49 sending getannotate command
50 fastannotate: server returned
51 50 fastannotate: writing 112 bytes to fastannotate/default/a.l
52 51 fastannotate: writing 94 bytes to fastannotate/default/a.m
53 52 fastannotate: a: using fast path (resolved fctx: True)
54 53 0: 1
55 54 1: 2
56 55 2: 3
57 56 3: 4
58 57
59 58 the cache could be reused and no download is necessary
60 59
61 60 $ hg fastannotate a --debug
62 61 fastannotate: a: using fast path (resolved fctx: True)
63 62 0: 1
64 63 1: 2
65 64 2: 3
66 65 3: 4
67 66
68 67 if the client agrees where the head of the master branch is, no re-download
69 68 happens even if the client has more commits
70 69
71 70 $ echo 5 >> a
72 71 $ hg commit -m 5
73 72 $ hg bookmark -r 3 @ -f
74 73 $ hg fastannotate a --debug
75 74 0: 1
76 75 1: 2
77 76 2: 3
78 77 3: 4
79 78 4: 5
80 79
81 80 if the client has a different "@" (head of the master branch) and "@" is ahead
82 81 of the server, the server can detect things are unchanged and does not return
83 82 full contents (not that there is no "writing ... to fastannotate"), but the
84 83 client can also build things up on its own (causing diverge)
85 84
86 85 $ hg bookmark -r 4 @ -f
87 86 $ hg fastannotate a --debug
88 87 running * (glob)
89 88 sending hello command
90 89 sending between command
91 90 remote: * (glob) (?)
92 91 remote: capabilities: * (glob)
93 92 remote: * (glob) (?)
94 93 sending protocaps command
95 94 fastannotate: requesting 1 files
96 95 sending getannotate command
97 fastannotate: server returned
98 96 fastannotate: a: 1 new changesets in the main branch
99 97 0: 1
100 98 1: 2
101 99 2: 3
102 100 3: 4
103 101 4: 5
104 102
105 103 if the client has a different "@" which is behind the server. no download is
106 104 necessary
107 105
108 106 $ hg fastannotate a --debug --config fastannotate.mainbranch=2
109 107 fastannotate: a: using fast path (resolved fctx: True)
110 108 0: 1
111 109 1: 2
112 110 2: 3
113 111 3: 4
114 112 4: 5
115 113
116 114 define fastannotate on-disk paths
117 115
118 116 $ p1=.hg/fastannotate/default
119 117 $ p2=../repo-server/.hg/fastannotate/default
120 118
121 119 revert bookmark change so the client is behind the server
122 120
123 121 $ hg bookmark -r 2 @ -f
124 122
125 123 in the "fctx" mode with the "annotate" command, the client also downloads the
126 124 cache. but not in the (default) "fastannotate" mode.
127 125
128 126 $ rm $p1/a.l $p1/a.m
129 127 $ hg annotate a --debug | grep 'fastannotate: writing'
130 128 [1]
131 129 $ hg annotate a --config fastannotate.modes=fctx --debug | grep 'fastannotate: writing' | sort
132 130 fastannotate: writing 112 bytes to fastannotate/default/a.l
133 131 fastannotate: writing 94 bytes to fastannotate/default/a.m
134 132
135 133 the fastannotate cache (built server-side, downloaded client-side) in two repos
136 134 have the same content (because the client downloads from the server)
137 135
138 136 $ diff $p1/a.l $p2/a.l
139 137 $ diff $p1/a.m $p2/a.m
140 138
141 139 in the "fctx" mode, the client could also build the cache locally
142 140
143 141 $ hg annotate a --config fastannotate.modes=fctx --debug --config fastannotate.mainbranch=4 | grep fastannotate
144 142 fastannotate: requesting 1 files
145 fastannotate: server returned
146 143 fastannotate: a: 1 new changesets in the main branch
147 144
148 145 the server would rebuild broken cache automatically
149 146
150 147 $ cp $p2/a.m $p2/a.m.bak
151 148 $ echo BROKEN1 > $p1/a.m
152 149 $ echo BROKEN2 > $p2/a.m
153 150 $ hg fastannotate a --debug | grep 'fastannotate: writing' | sort
154 151 fastannotate: writing 112 bytes to fastannotate/default/a.l
155 152 fastannotate: writing 94 bytes to fastannotate/default/a.m
156 153 $ diff $p1/a.m $p2/a.m
157 154 $ diff $p2/a.m $p2/a.m.bak
158 155
159 156 use the "debugbuildannotatecache" command to build annotate cache
160 157
161 158 $ rm -rf $p1 $p2
162 159 $ hg --cwd ../repo-server debugbuildannotatecache a --debug
163 160 fastannotate: a: 4 new changesets in the main branch
164 161 $ hg --cwd ../repo-local debugbuildannotatecache a --debug
165 162 running * (glob)
166 163 sending hello command
167 164 sending between command
168 165 remote: * (glob) (?)
169 166 remote: capabilities: * (glob)
170 167 remote: * (glob) (?)
171 168 sending protocaps command
172 169 fastannotate: requesting 1 files
173 170 sending getannotate command
174 fastannotate: server returned
175 171 fastannotate: writing * (glob)
176 172 fastannotate: writing * (glob)
177 173 $ diff $p1/a.l $p2/a.l
178 174 $ diff $p1/a.m $p2/a.m
179 175
180 176 with the clientfetchthreshold config option, the client can build up the cache
181 177 without downloading from the server
182 178
183 179 $ rm -rf $p1
184 180 $ hg fastannotate a --debug --config fastannotate.clientfetchthreshold=10
185 181 fastannotate: a: 3 new changesets in the main branch
186 182 0: 1
187 183 1: 2
188 184 2: 3
189 185 3: 4
190 186 4: 5
191 187
192 188 if the fastannotate directory is not writable, the fctx mode still works
193 189
194 190 $ rm -rf $p1
195 191 $ touch $p1
196 192 $ hg annotate a --debug --traceback --config fastannotate.modes=fctx
197 193 fastannotate: a: cache broken and deleted
198 194 fastannotate: prefetch failed: * (glob)
199 195 fastannotate: a: cache broken and deleted
200 196 fastannotate: falling back to the vanilla annotate: * (glob)
201 197 0: 1
202 198 1: 2
203 199 2: 3
204 200 3: 4
205 201 4: 5
206 202
207 203 with serverbuildondemand=False, the server will not build anything
208 204
209 205 $ cat >> ../repo-server/.hg/hgrc <<EOF
210 206 > [fastannotate]
211 207 > serverbuildondemand=False
212 208 > EOF
213 209 $ rm -rf $p1 $p2
214 210 $ hg fastannotate a --debug | grep 'fastannotate: writing'
215 211 [1]
General Comments 0
You need to be logged in to leave comments. Login now