##// END OF EJS Templates
narrow: validate patterns on incoming bundle2 part...
Gregory Szorc -
r39576:ce20caec default
parent child Browse files
Show More
@@ -1,332 +1,335
1 1 # narrowbundle2.py - bundle2 extensions for narrow repository support
2 2 #
3 3 # Copyright 2017 Google, Inc.
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
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import struct
12 12
13 13 from mercurial.i18n import _
14 14 from mercurial.node import (
15 15 bin,
16 16 nullid,
17 17 )
18 18 from mercurial import (
19 19 bundle2,
20 20 changegroup,
21 21 error,
22 22 exchange,
23 23 extensions,
24 24 narrowspec,
25 25 repair,
26 26 repository,
27 27 util,
28 28 wireprototypes,
29 29 )
30 30 from mercurial.utils import (
31 31 stringutil,
32 32 )
33 33
34 34 NARROWCAP = 'narrow'
35 35 _NARROWACL_SECTION = 'narrowhgacl'
36 36 _CHANGESPECPART = NARROWCAP + ':changespec'
37 37 _SPECPART = NARROWCAP + ':spec'
38 38 _SPECPART_INCLUDE = 'include'
39 39 _SPECPART_EXCLUDE = 'exclude'
40 40 _KILLNODESIGNAL = 'KILL'
41 41 _DONESIGNAL = 'DONE'
42 42 _ELIDEDCSHEADER = '>20s20s20sl' # cset id, p1, p2, len(text)
43 43 _ELIDEDMFHEADER = '>20s20s20s20sl' # manifest id, p1, p2, link id, len(text)
44 44 _CSHEADERSIZE = struct.calcsize(_ELIDEDCSHEADER)
45 45 _MFHEADERSIZE = struct.calcsize(_ELIDEDMFHEADER)
46 46
47 47 # When advertising capabilities, always include narrow clone support.
48 48 def getrepocaps_narrow(orig, repo, **kwargs):
49 49 caps = orig(repo, **kwargs)
50 50 caps[NARROWCAP] = ['v0']
51 51 return caps
52 52
53 53 def getbundlechangegrouppart_widen(bundler, repo, source, bundlecaps=None,
54 54 b2caps=None, heads=None, common=None,
55 55 **kwargs):
56 56 """Handling changegroup changegroup generation on the server when user
57 57 is widening their narrowspec"""
58 58
59 59 cgversions = b2caps.get('changegroup')
60 60 if cgversions: # 3.1 and 3.2 ship with an empty value
61 61 cgversions = [v for v in cgversions
62 62 if v in changegroup.supportedoutgoingversions(repo)]
63 63 if not cgversions:
64 64 raise ValueError(_('no common changegroup version'))
65 65 version = max(cgversions)
66 66 else:
67 67 raise ValueError(_("server does not advertise changegroup version,"
68 68 " can't negotiate support for ellipsis nodes"))
69 69
70 70 include = sorted(filter(bool, kwargs.get(r'includepats', [])))
71 71 exclude = sorted(filter(bool, kwargs.get(r'excludepats', [])))
72 72 newmatch = narrowspec.match(repo.root, include=include, exclude=exclude)
73 73 oldinclude = sorted(filter(bool, kwargs.get(r'oldincludepats', [])))
74 74 oldexclude = sorted(filter(bool, kwargs.get(r'oldexcludepats', [])))
75 75 common = set(common or [nullid])
76 76
77 77 if (oldinclude != include or oldexclude != exclude):
78 78 common = repo.revs("::%ln", common)
79 79 commonnodes = set()
80 80 cl = repo.changelog
81 81 for c in common:
82 82 commonnodes.add(cl.node(c))
83 83 if commonnodes:
84 84 # XXX: we should only send the filelogs (and treemanifest). user
85 85 # already has the changelog and manifest
86 86 packer = changegroup.getbundler(version, repo,
87 87 filematcher=newmatch,
88 88 fullnodes=commonnodes)
89 89 cgdata = packer.generate(set([nullid]), list(commonnodes), False,
90 90 source)
91 91
92 92 part = bundler.newpart('changegroup', data=cgdata)
93 93 part.addparam('version', version)
94 94 if 'treemanifest' in repo.requirements:
95 95 part.addparam('treemanifest', '1')
96 96
97 97 # Serve a changegroup for a client with a narrow clone.
98 98 def getbundlechangegrouppart_narrow(bundler, repo, source,
99 99 bundlecaps=None, b2caps=None, heads=None,
100 100 common=None, **kwargs):
101 101 assert repo.ui.configbool('experimental', 'narrowservebrokenellipses')
102 102
103 103 cgversions = b2caps.get('changegroup')
104 104 if cgversions: # 3.1 and 3.2 ship with an empty value
105 105 cgversions = [v for v in cgversions
106 106 if v in changegroup.supportedoutgoingversions(repo)]
107 107 if not cgversions:
108 108 raise ValueError(_('no common changegroup version'))
109 109 version = max(cgversions)
110 110 else:
111 111 raise ValueError(_("server does not advertise changegroup version,"
112 112 " can't negotiate support for ellipsis nodes"))
113 113
114 114 include = sorted(filter(bool, kwargs.get(r'includepats', [])))
115 115 exclude = sorted(filter(bool, kwargs.get(r'excludepats', [])))
116 116 newmatch = narrowspec.match(repo.root, include=include, exclude=exclude)
117 117
118 118 depth = kwargs.get(r'depth', None)
119 119 if depth is not None:
120 120 depth = int(depth)
121 121 if depth < 1:
122 122 raise error.Abort(_('depth must be positive, got %d') % depth)
123 123
124 124 heads = set(heads or repo.heads())
125 125 common = set(common or [nullid])
126 126 oldinclude = sorted(filter(bool, kwargs.get(r'oldincludepats', [])))
127 127 oldexclude = sorted(filter(bool, kwargs.get(r'oldexcludepats', [])))
128 128 known = {bin(n) for n in kwargs.get(r'known', [])}
129 129 if known and (oldinclude != include or oldexclude != exclude):
130 130 # Steps:
131 131 # 1. Send kill for "$known & ::common"
132 132 #
133 133 # 2. Send changegroup for ::common
134 134 #
135 135 # 3. Proceed.
136 136 #
137 137 # In the future, we can send kills for only the specific
138 138 # nodes we know should go away or change shape, and then
139 139 # send a data stream that tells the client something like this:
140 140 #
141 141 # a) apply this changegroup
142 142 # b) apply nodes XXX, YYY, ZZZ that you already have
143 143 # c) goto a
144 144 #
145 145 # until they've built up the full new state.
146 146 # Convert to revnums and intersect with "common". The client should
147 147 # have made it a subset of "common" already, but let's be safe.
148 148 known = set(repo.revs("%ln & ::%ln", known, common))
149 149 # TODO: we could send only roots() of this set, and the
150 150 # list of nodes in common, and the client could work out
151 151 # what to strip, instead of us explicitly sending every
152 152 # single node.
153 153 deadrevs = known
154 154 def genkills():
155 155 for r in deadrevs:
156 156 yield _KILLNODESIGNAL
157 157 yield repo.changelog.node(r)
158 158 yield _DONESIGNAL
159 159 bundler.newpart(_CHANGESPECPART, data=genkills())
160 160 newvisit, newfull, newellipsis = exchange._computeellipsis(
161 161 repo, set(), common, known, newmatch)
162 162 if newvisit:
163 163 packer = changegroup.getbundler(version, repo,
164 164 filematcher=newmatch,
165 165 ellipses=True,
166 166 shallow=depth is not None,
167 167 ellipsisroots=newellipsis,
168 168 fullnodes=newfull)
169 169 cgdata = packer.generate(common, newvisit, False, source)
170 170
171 171 part = bundler.newpart('changegroup', data=cgdata)
172 172 part.addparam('version', version)
173 173 if 'treemanifest' in repo.requirements:
174 174 part.addparam('treemanifest', '1')
175 175
176 176 visitnodes, relevant_nodes, ellipsisroots = exchange._computeellipsis(
177 177 repo, common, heads, set(), newmatch, depth=depth)
178 178
179 179 repo.ui.debug('Found %d relevant revs\n' % len(relevant_nodes))
180 180 if visitnodes:
181 181 packer = changegroup.getbundler(version, repo,
182 182 filematcher=newmatch,
183 183 ellipses=True,
184 184 shallow=depth is not None,
185 185 ellipsisroots=ellipsisroots,
186 186 fullnodes=relevant_nodes)
187 187 cgdata = packer.generate(common, visitnodes, False, source)
188 188
189 189 part = bundler.newpart('changegroup', data=cgdata)
190 190 part.addparam('version', version)
191 191 if 'treemanifest' in repo.requirements:
192 192 part.addparam('treemanifest', '1')
193 193
194 194 @bundle2.parthandler(_SPECPART, (_SPECPART_INCLUDE, _SPECPART_EXCLUDE))
195 195 def _handlechangespec_2(op, inpart):
196 196 includepats = set(inpart.params.get(_SPECPART_INCLUDE, '').splitlines())
197 197 excludepats = set(inpart.params.get(_SPECPART_EXCLUDE, '').splitlines())
198 narrowspec.validatepatterns(includepats)
199 narrowspec.validatepatterns(excludepats)
200
198 201 if not repository.NARROW_REQUIREMENT in op.repo.requirements:
199 202 op.repo.requirements.add(repository.NARROW_REQUIREMENT)
200 203 op.repo._writerequirements()
201 204 op.repo.setnarrowpats(includepats, excludepats)
202 205
203 206 @bundle2.parthandler(_CHANGESPECPART)
204 207 def _handlechangespec(op, inpart):
205 208 repo = op.repo
206 209 cl = repo.changelog
207 210
208 211 # changesets which need to be stripped entirely. either they're no longer
209 212 # needed in the new narrow spec, or the server is sending a replacement
210 213 # in the changegroup part.
211 214 clkills = set()
212 215
213 216 # A changespec part contains all the updates to ellipsis nodes
214 217 # that will happen as a result of widening or narrowing a
215 218 # repo. All the changes that this block encounters are ellipsis
216 219 # nodes or flags to kill an existing ellipsis.
217 220 chunksignal = changegroup.readexactly(inpart, 4)
218 221 while chunksignal != _DONESIGNAL:
219 222 if chunksignal == _KILLNODESIGNAL:
220 223 # a node used to be an ellipsis but isn't anymore
221 224 ck = changegroup.readexactly(inpart, 20)
222 225 if cl.hasnode(ck):
223 226 clkills.add(ck)
224 227 else:
225 228 raise error.Abort(
226 229 _('unexpected changespec node chunk type: %s') % chunksignal)
227 230 chunksignal = changegroup.readexactly(inpart, 4)
228 231
229 232 if clkills:
230 233 # preserve bookmarks that repair.strip() would otherwise strip
231 234 bmstore = repo._bookmarks
232 235 class dummybmstore(dict):
233 236 def applychanges(self, repo, tr, changes):
234 237 pass
235 238 def recordchange(self, tr): # legacy version
236 239 pass
237 240 repo._bookmarks = dummybmstore()
238 241 chgrpfile = repair.strip(op.ui, repo, list(clkills), backup=True,
239 242 topic='widen')
240 243 repo._bookmarks = bmstore
241 244 if chgrpfile:
242 245 op._widen_uninterr = repo.ui.uninterruptable()
243 246 op._widen_uninterr.__enter__()
244 247 # presence of _widen_bundle attribute activates widen handler later
245 248 op._widen_bundle = chgrpfile
246 249 # Set the new narrowspec if we're widening. The setnewnarrowpats() method
247 250 # will currently always be there when using the core+narrowhg server, but
248 251 # other servers may include a changespec part even when not widening (e.g.
249 252 # because we're deepening a shallow repo).
250 253 if util.safehasattr(repo, 'setnewnarrowpats'):
251 254 repo.setnewnarrowpats()
252 255
253 256 def handlechangegroup_widen(op, inpart):
254 257 """Changegroup exchange handler which restores temporarily-stripped nodes"""
255 258 # We saved a bundle with stripped node data we must now restore.
256 259 # This approach is based on mercurial/repair.py@6ee26a53c111.
257 260 repo = op.repo
258 261 ui = op.ui
259 262
260 263 chgrpfile = op._widen_bundle
261 264 del op._widen_bundle
262 265 vfs = repo.vfs
263 266
264 267 ui.note(_("adding branch\n"))
265 268 f = vfs.open(chgrpfile, "rb")
266 269 try:
267 270 gen = exchange.readbundle(ui, f, chgrpfile, vfs)
268 271 if not ui.verbose:
269 272 # silence internal shuffling chatter
270 273 ui.pushbuffer()
271 274 if isinstance(gen, bundle2.unbundle20):
272 275 with repo.transaction('strip') as tr:
273 276 bundle2.processbundle(repo, gen, lambda: tr)
274 277 else:
275 278 gen.apply(repo, 'strip', 'bundle:' + vfs.join(chgrpfile), True)
276 279 if not ui.verbose:
277 280 ui.popbuffer()
278 281 finally:
279 282 f.close()
280 283
281 284 # remove undo files
282 285 for undovfs, undofile in repo.undofiles():
283 286 try:
284 287 undovfs.unlink(undofile)
285 288 except OSError as e:
286 289 if e.errno != errno.ENOENT:
287 290 ui.warn(_('error removing %s: %s\n') %
288 291 (undovfs.join(undofile), stringutil.forcebytestr(e)))
289 292
290 293 # Remove partial backup only if there were no exceptions
291 294 op._widen_uninterr.__exit__(None, None, None)
292 295 vfs.unlink(chgrpfile)
293 296
294 297 def setup():
295 298 """Enable narrow repo support in bundle2-related extension points."""
296 299 extensions.wrapfunction(bundle2, 'getrepocaps', getrepocaps_narrow)
297 300
298 301 getbundleargs = wireprototypes.GETBUNDLE_ARGUMENTS
299 302
300 303 getbundleargs['narrow'] = 'boolean'
301 304 getbundleargs['widen'] = 'boolean'
302 305 getbundleargs['depth'] = 'plain'
303 306 getbundleargs['oldincludepats'] = 'csv'
304 307 getbundleargs['oldexcludepats'] = 'csv'
305 308 getbundleargs['includepats'] = 'csv'
306 309 getbundleargs['excludepats'] = 'csv'
307 310 getbundleargs['known'] = 'csv'
308 311
309 312 # Extend changegroup serving to handle requests from narrow clients.
310 313 origcgfn = exchange.getbundle2partsmapping['changegroup']
311 314 def wrappedcgfn(*args, **kwargs):
312 315 repo = args[1]
313 316 if repo.ui.has_section(_NARROWACL_SECTION):
314 317 kwargs = exchange.applynarrowacl(repo, kwargs)
315 318
316 319 if (kwargs.get(r'narrow', False) and
317 320 repo.ui.configbool('experimental', 'narrowservebrokenellipses')):
318 321 getbundlechangegrouppart_narrow(*args, **kwargs)
319 322 elif kwargs.get(r'widen', False) and kwargs.get(r'narrow', False):
320 323 getbundlechangegrouppart_widen(*args, **kwargs)
321 324 else:
322 325 origcgfn(*args, **kwargs)
323 326 exchange.getbundle2partsmapping['changegroup'] = wrappedcgfn
324 327
325 328 # Extend changegroup receiver so client can fixup after widen requests.
326 329 origcghandler = bundle2.parthandlermapping['changegroup']
327 330 def wrappedcghandler(op, inpart):
328 331 origcghandler(op, inpart)
329 332 if util.safehasattr(op, '_widen_bundle'):
330 333 handlechangegroup_widen(op, inpart)
331 334 wrappedcghandler.params = origcghandler.params
332 335 bundle2.parthandlermapping['changegroup'] = wrappedcghandler
General Comments 0
You need to be logged in to leave comments. Login now