##// END OF EJS Templates
revbranchcache: disable the new part for narrow hg bundle...
Boris Feld -
r36985:f62873db default
parent child Browse files
Show More
@@ -1,489 +1,502
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 collections
11 11 import errno
12 12 import struct
13 13
14 14 from mercurial.i18n import _
15 15 from mercurial.node import (
16 16 bin,
17 17 nullid,
18 18 nullrev,
19 19 )
20 20 from mercurial import (
21 21 bundle2,
22 22 changegroup,
23 23 dagutil,
24 24 error,
25 25 exchange,
26 26 extensions,
27 27 narrowspec,
28 28 repair,
29 29 util,
30 30 wireproto,
31 31 )
32 32
33 33 NARROWCAP = 'narrow'
34 34 _NARROWACL_SECTION = 'narrowhgacl'
35 35 _CHANGESPECPART = NARROWCAP + ':changespec'
36 36 _SPECPART = NARROWCAP + ':spec'
37 37 _SPECPART_INCLUDE = 'include'
38 38 _SPECPART_EXCLUDE = 'exclude'
39 39 _KILLNODESIGNAL = 'KILL'
40 40 _DONESIGNAL = 'DONE'
41 41 _ELIDEDCSHEADER = '>20s20s20sl' # cset id, p1, p2, len(text)
42 42 _ELIDEDMFHEADER = '>20s20s20s20sl' # manifest id, p1, p2, link id, len(text)
43 43 _CSHEADERSIZE = struct.calcsize(_ELIDEDCSHEADER)
44 44 _MFHEADERSIZE = struct.calcsize(_ELIDEDMFHEADER)
45 45
46 46 # When advertising capabilities, always include narrow clone support.
47 47 def getrepocaps_narrow(orig, repo, **kwargs):
48 48 caps = orig(repo, **kwargs)
49 49 caps[NARROWCAP] = ['v0']
50 50 return caps
51 51
52 52 def _computeellipsis(repo, common, heads, known, match, depth=None):
53 53 """Compute the shape of a narrowed DAG.
54 54
55 55 Args:
56 56 repo: The repository we're transferring.
57 57 common: The roots of the DAG range we're transferring.
58 58 May be just [nullid], which means all ancestors of heads.
59 59 heads: The heads of the DAG range we're transferring.
60 60 match: The narrowmatcher that allows us to identify relevant changes.
61 61 depth: If not None, only consider nodes to be full nodes if they are at
62 62 most depth changesets away from one of heads.
63 63
64 64 Returns:
65 65 A tuple of (visitnodes, relevant_nodes, ellipsisroots) where:
66 66
67 67 visitnodes: The list of nodes (either full or ellipsis) which
68 68 need to be sent to the client.
69 69 relevant_nodes: The set of changelog nodes which change a file inside
70 70 the narrowspec. The client needs these as non-ellipsis nodes.
71 71 ellipsisroots: A dict of {rev: parents} that is used in
72 72 narrowchangegroup to produce ellipsis nodes with the
73 73 correct parents.
74 74 """
75 75 cl = repo.changelog
76 76 mfl = repo.manifestlog
77 77
78 78 cldag = dagutil.revlogdag(cl)
79 79 # dagutil does not like nullid/nullrev
80 80 commonrevs = cldag.internalizeall(common - set([nullid])) | set([nullrev])
81 81 headsrevs = cldag.internalizeall(heads)
82 82 if depth:
83 83 revdepth = {h: 0 for h in headsrevs}
84 84
85 85 ellipsisheads = collections.defaultdict(set)
86 86 ellipsisroots = collections.defaultdict(set)
87 87
88 88 def addroot(head, curchange):
89 89 """Add a root to an ellipsis head, splitting heads with 3 roots."""
90 90 ellipsisroots[head].add(curchange)
91 91 # Recursively split ellipsis heads with 3 roots by finding the
92 92 # roots' youngest common descendant which is an elided merge commit.
93 93 # That descendant takes 2 of the 3 roots as its own, and becomes a
94 94 # root of the head.
95 95 while len(ellipsisroots[head]) > 2:
96 96 child, roots = splithead(head)
97 97 splitroots(head, child, roots)
98 98 head = child # Recurse in case we just added a 3rd root
99 99
100 100 def splitroots(head, child, roots):
101 101 ellipsisroots[head].difference_update(roots)
102 102 ellipsisroots[head].add(child)
103 103 ellipsisroots[child].update(roots)
104 104 ellipsisroots[child].discard(child)
105 105
106 106 def splithead(head):
107 107 r1, r2, r3 = sorted(ellipsisroots[head])
108 108 for nr1, nr2 in ((r2, r3), (r1, r3), (r1, r2)):
109 109 mid = repo.revs('sort(merge() & %d::%d & %d::%d, -rev)',
110 110 nr1, head, nr2, head)
111 111 for j in mid:
112 112 if j == nr2:
113 113 return nr2, (nr1, nr2)
114 114 if j not in ellipsisroots or len(ellipsisroots[j]) < 2:
115 115 return j, (nr1, nr2)
116 116 raise error.Abort('Failed to split up ellipsis node! head: %d, '
117 117 'roots: %d %d %d' % (head, r1, r2, r3))
118 118
119 119 missing = list(cl.findmissingrevs(common=commonrevs, heads=headsrevs))
120 120 visit = reversed(missing)
121 121 relevant_nodes = set()
122 122 visitnodes = [cl.node(m) for m in missing]
123 123 required = set(headsrevs) | known
124 124 for rev in visit:
125 125 clrev = cl.changelogrevision(rev)
126 126 ps = cldag.parents(rev)
127 127 if depth is not None:
128 128 curdepth = revdepth[rev]
129 129 for p in ps:
130 130 revdepth[p] = min(curdepth + 1, revdepth.get(p, depth + 1))
131 131 needed = False
132 132 shallow_enough = depth is None or revdepth[rev] <= depth
133 133 if shallow_enough:
134 134 curmf = mfl[clrev.manifest].read()
135 135 if ps:
136 136 # We choose to not trust the changed files list in
137 137 # changesets because it's not always correct. TODO: could
138 138 # we trust it for the non-merge case?
139 139 p1mf = mfl[cl.changelogrevision(ps[0]).manifest].read()
140 140 needed = bool(curmf.diff(p1mf, match))
141 141 if not needed and len(ps) > 1:
142 142 # For merge changes, the list of changed files is not
143 143 # helpful, since we need to emit the merge if a file
144 144 # in the narrow spec has changed on either side of the
145 145 # merge. As a result, we do a manifest diff to check.
146 146 p2mf = mfl[cl.changelogrevision(ps[1]).manifest].read()
147 147 needed = bool(curmf.diff(p2mf, match))
148 148 else:
149 149 # For a root node, we need to include the node if any
150 150 # files in the node match the narrowspec.
151 151 needed = any(curmf.walk(match))
152 152
153 153 if needed:
154 154 for head in ellipsisheads[rev]:
155 155 addroot(head, rev)
156 156 for p in ps:
157 157 required.add(p)
158 158 relevant_nodes.add(cl.node(rev))
159 159 else:
160 160 if not ps:
161 161 ps = [nullrev]
162 162 if rev in required:
163 163 for head in ellipsisheads[rev]:
164 164 addroot(head, rev)
165 165 for p in ps:
166 166 ellipsisheads[p].add(rev)
167 167 else:
168 168 for p in ps:
169 169 ellipsisheads[p] |= ellipsisheads[rev]
170 170
171 171 # add common changesets as roots of their reachable ellipsis heads
172 172 for c in commonrevs:
173 173 for head in ellipsisheads[c]:
174 174 addroot(head, c)
175 175 return visitnodes, relevant_nodes, ellipsisroots
176 176
177 177 def _packellipsischangegroup(repo, common, match, relevant_nodes,
178 178 ellipsisroots, visitnodes, depth, source, version):
179 179 if version in ('01', '02'):
180 180 raise error.Abort(
181 181 'ellipsis nodes require at least cg3 on client and server, '
182 182 'but negotiated version %s' % version)
183 183 # We wrap cg1packer.revchunk, using a side channel to pass
184 184 # relevant_nodes into that area. Then if linknode isn't in the
185 185 # set, we know we have an ellipsis node and we should defer
186 186 # sending that node's data. We override close() to detect
187 187 # pending ellipsis nodes and flush them.
188 188 packer = changegroup.getbundler(version, repo)
189 189 # Let the packer have access to the narrow matcher so it can
190 190 # omit filelogs and dirlogs as needed
191 191 packer._narrow_matcher = lambda : match
192 192 # Give the packer the list of nodes which should not be
193 193 # ellipsis nodes. We store this rather than the set of nodes
194 194 # that should be an ellipsis because for very large histories
195 195 # we expect this to be significantly smaller.
196 196 packer.full_nodes = relevant_nodes
197 197 # Maps ellipsis revs to their roots at the changelog level.
198 198 packer.precomputed_ellipsis = ellipsisroots
199 199 # Maps CL revs to per-revlog revisions. Cleared in close() at
200 200 # the end of each group.
201 201 packer.clrev_to_localrev = {}
202 202 packer.next_clrev_to_localrev = {}
203 203 # Maps changelog nodes to changelog revs. Filled in once
204 204 # during changelog stage and then left unmodified.
205 205 packer.clnode_to_rev = {}
206 206 packer.changelog_done = False
207 207 # If true, informs the packer that it is serving shallow content and might
208 208 # need to pack file contents not introduced by the changes being packed.
209 209 packer.is_shallow = depth is not None
210 210
211 211 return packer.generate(common, visitnodes, False, source)
212 212
213 213 # Serve a changegroup for a client with a narrow clone.
214 214 def getbundlechangegrouppart_narrow(bundler, repo, source,
215 215 bundlecaps=None, b2caps=None, heads=None,
216 216 common=None, **kwargs):
217 217 cgversions = b2caps.get('changegroup')
218 218 if cgversions: # 3.1 and 3.2 ship with an empty value
219 219 cgversions = [v for v in cgversions
220 220 if v in changegroup.supportedoutgoingversions(repo)]
221 221 if not cgversions:
222 222 raise ValueError(_('no common changegroup version'))
223 223 version = max(cgversions)
224 224 else:
225 225 raise ValueError(_("server does not advertise changegroup version,"
226 226 " can't negotiate support for ellipsis nodes"))
227 227
228 228 include = sorted(filter(bool, kwargs.get(r'includepats', [])))
229 229 exclude = sorted(filter(bool, kwargs.get(r'excludepats', [])))
230 230 newmatch = narrowspec.match(repo.root, include=include, exclude=exclude)
231 231 if not repo.ui.configbool("experimental", "narrowservebrokenellipses"):
232 232 outgoing = exchange._computeoutgoing(repo, heads, common)
233 233 if not outgoing.missing:
234 234 return
235 235 def wrappedgetbundler(orig, *args, **kwargs):
236 236 bundler = orig(*args, **kwargs)
237 237 bundler._narrow_matcher = lambda : newmatch
238 238 return bundler
239 239 with extensions.wrappedfunction(changegroup, 'getbundler',
240 240 wrappedgetbundler):
241 241 cg = changegroup.makestream(repo, outgoing, version, source)
242 242 part = bundler.newpart('changegroup', data=cg)
243 243 part.addparam('version', version)
244 244 if 'treemanifest' in repo.requirements:
245 245 part.addparam('treemanifest', '1')
246 246
247 247 if include or exclude:
248 248 narrowspecpart = bundler.newpart(_SPECPART)
249 249 if include:
250 250 narrowspecpart.addparam(
251 251 _SPECPART_INCLUDE, '\n'.join(include), mandatory=True)
252 252 if exclude:
253 253 narrowspecpart.addparam(
254 254 _SPECPART_EXCLUDE, '\n'.join(exclude), mandatory=True)
255 255
256 256 return
257 257
258 258 depth = kwargs.get(r'depth', None)
259 259 if depth is not None:
260 260 depth = int(depth)
261 261 if depth < 1:
262 262 raise error.Abort(_('depth must be positive, got %d') % depth)
263 263
264 264 heads = set(heads or repo.heads())
265 265 common = set(common or [nullid])
266 266 oldinclude = sorted(filter(bool, kwargs.get(r'oldincludepats', [])))
267 267 oldexclude = sorted(filter(bool, kwargs.get(r'oldexcludepats', [])))
268 268 known = {bin(n) for n in kwargs.get(r'known', [])}
269 269 if known and (oldinclude != include or oldexclude != exclude):
270 270 # Steps:
271 271 # 1. Send kill for "$known & ::common"
272 272 #
273 273 # 2. Send changegroup for ::common
274 274 #
275 275 # 3. Proceed.
276 276 #
277 277 # In the future, we can send kills for only the specific
278 278 # nodes we know should go away or change shape, and then
279 279 # send a data stream that tells the client something like this:
280 280 #
281 281 # a) apply this changegroup
282 282 # b) apply nodes XXX, YYY, ZZZ that you already have
283 283 # c) goto a
284 284 #
285 285 # until they've built up the full new state.
286 286 # Convert to revnums and intersect with "common". The client should
287 287 # have made it a subset of "common" already, but let's be safe.
288 288 known = set(repo.revs("%ln & ::%ln", known, common))
289 289 # TODO: we could send only roots() of this set, and the
290 290 # list of nodes in common, and the client could work out
291 291 # what to strip, instead of us explicitly sending every
292 292 # single node.
293 293 deadrevs = known
294 294 def genkills():
295 295 for r in deadrevs:
296 296 yield _KILLNODESIGNAL
297 297 yield repo.changelog.node(r)
298 298 yield _DONESIGNAL
299 299 bundler.newpart(_CHANGESPECPART, data=genkills())
300 300 newvisit, newfull, newellipsis = _computeellipsis(
301 301 repo, set(), common, known, newmatch)
302 302 if newvisit:
303 303 cg = _packellipsischangegroup(
304 304 repo, common, newmatch, newfull, newellipsis,
305 305 newvisit, depth, source, version)
306 306 part = bundler.newpart('changegroup', data=cg)
307 307 part.addparam('version', version)
308 308 if 'treemanifest' in repo.requirements:
309 309 part.addparam('treemanifest', '1')
310 310
311 311 visitnodes, relevant_nodes, ellipsisroots = _computeellipsis(
312 312 repo, common, heads, set(), newmatch, depth=depth)
313 313
314 314 repo.ui.debug('Found %d relevant revs\n' % len(relevant_nodes))
315 315 if visitnodes:
316 316 cg = _packellipsischangegroup(
317 317 repo, common, newmatch, relevant_nodes, ellipsisroots,
318 318 visitnodes, depth, source, version)
319 319 part = bundler.newpart('changegroup', data=cg)
320 320 part.addparam('version', version)
321 321 if 'treemanifest' in repo.requirements:
322 322 part.addparam('treemanifest', '1')
323 323
324 324 def applyacl_narrow(repo, kwargs):
325 325 ui = repo.ui
326 326 username = ui.shortuser(ui.environ.get('REMOTE_USER') or ui.username())
327 327 user_includes = ui.configlist(
328 328 _NARROWACL_SECTION, username + '.includes',
329 329 ui.configlist(_NARROWACL_SECTION, 'default.includes'))
330 330 user_excludes = ui.configlist(
331 331 _NARROWACL_SECTION, username + '.excludes',
332 332 ui.configlist(_NARROWACL_SECTION, 'default.excludes'))
333 333 if not user_includes:
334 334 raise error.Abort(_("{} configuration for user {} is empty")
335 335 .format(_NARROWACL_SECTION, username))
336 336
337 337 user_includes = [
338 338 'path:.' if p == '*' else 'path:' + p for p in user_includes]
339 339 user_excludes = [
340 340 'path:.' if p == '*' else 'path:' + p for p in user_excludes]
341 341
342 342 req_includes = set(kwargs.get(r'includepats', []))
343 343 req_excludes = set(kwargs.get(r'excludepats', []))
344 344
345 345 req_includes, req_excludes, invalid_includes = narrowspec.restrictpatterns(
346 346 req_includes, req_excludes, user_includes, user_excludes)
347 347
348 348 if invalid_includes:
349 349 raise error.Abort(
350 350 _("The following includes are not accessible for {}: {}")
351 351 .format(username, invalid_includes))
352 352
353 353 new_args = {}
354 354 new_args.update(kwargs)
355 355 new_args['includepats'] = req_includes
356 356 if req_excludes:
357 357 new_args['excludepats'] = req_excludes
358 358 return new_args
359 359
360 360 @bundle2.parthandler(_SPECPART, (_SPECPART_INCLUDE, _SPECPART_EXCLUDE))
361 361 def _handlechangespec_2(op, inpart):
362 362 includepats = set(inpart.params.get(_SPECPART_INCLUDE, '').splitlines())
363 363 excludepats = set(inpart.params.get(_SPECPART_EXCLUDE, '').splitlines())
364 364 if not changegroup.NARROW_REQUIREMENT in op.repo.requirements:
365 365 op.repo.requirements.add(changegroup.NARROW_REQUIREMENT)
366 366 op.repo._writerequirements()
367 367 op.repo.setnarrowpats(includepats, excludepats)
368 368
369 369 @bundle2.parthandler(_CHANGESPECPART)
370 370 def _handlechangespec(op, inpart):
371 371 repo = op.repo
372 372 cl = repo.changelog
373 373
374 374 # changesets which need to be stripped entirely. either they're no longer
375 375 # needed in the new narrow spec, or the server is sending a replacement
376 376 # in the changegroup part.
377 377 clkills = set()
378 378
379 379 # A changespec part contains all the updates to ellipsis nodes
380 380 # that will happen as a result of widening or narrowing a
381 381 # repo. All the changes that this block encounters are ellipsis
382 382 # nodes or flags to kill an existing ellipsis.
383 383 chunksignal = changegroup.readexactly(inpart, 4)
384 384 while chunksignal != _DONESIGNAL:
385 385 if chunksignal == _KILLNODESIGNAL:
386 386 # a node used to be an ellipsis but isn't anymore
387 387 ck = changegroup.readexactly(inpart, 20)
388 388 if cl.hasnode(ck):
389 389 clkills.add(ck)
390 390 else:
391 391 raise error.Abort(
392 392 _('unexpected changespec node chunk type: %s') % chunksignal)
393 393 chunksignal = changegroup.readexactly(inpart, 4)
394 394
395 395 if clkills:
396 396 # preserve bookmarks that repair.strip() would otherwise strip
397 397 bmstore = repo._bookmarks
398 398 class dummybmstore(dict):
399 399 def applychanges(self, repo, tr, changes):
400 400 pass
401 401 def recordchange(self, tr): # legacy version
402 402 pass
403 403 repo._bookmarks = dummybmstore()
404 404 chgrpfile = repair.strip(op.ui, repo, list(clkills), backup=True,
405 405 topic='widen')
406 406 repo._bookmarks = bmstore
407 407 if chgrpfile:
408 408 # presence of _widen_bundle attribute activates widen handler later
409 409 op._widen_bundle = chgrpfile
410 410 # Set the new narrowspec if we're widening. The setnewnarrowpats() method
411 411 # will currently always be there when using the core+narrowhg server, but
412 412 # other servers may include a changespec part even when not widening (e.g.
413 413 # because we're deepening a shallow repo).
414 414 if util.safehasattr(repo, 'setnewnarrowpats'):
415 415 repo.setnewnarrowpats()
416 416
417 417 def handlechangegroup_widen(op, inpart):
418 418 """Changegroup exchange handler which restores temporarily-stripped nodes"""
419 419 # We saved a bundle with stripped node data we must now restore.
420 420 # This approach is based on mercurial/repair.py@6ee26a53c111.
421 421 repo = op.repo
422 422 ui = op.ui
423 423
424 424 chgrpfile = op._widen_bundle
425 425 del op._widen_bundle
426 426 vfs = repo.vfs
427 427
428 428 ui.note(_("adding branch\n"))
429 429 f = vfs.open(chgrpfile, "rb")
430 430 try:
431 431 gen = exchange.readbundle(ui, f, chgrpfile, vfs)
432 432 if not ui.verbose:
433 433 # silence internal shuffling chatter
434 434 ui.pushbuffer()
435 435 if isinstance(gen, bundle2.unbundle20):
436 436 with repo.transaction('strip') as tr:
437 437 bundle2.processbundle(repo, gen, lambda: tr)
438 438 else:
439 439 gen.apply(repo, 'strip', 'bundle:' + vfs.join(chgrpfile), True)
440 440 if not ui.verbose:
441 441 ui.popbuffer()
442 442 finally:
443 443 f.close()
444 444
445 445 # remove undo files
446 446 for undovfs, undofile in repo.undofiles():
447 447 try:
448 448 undovfs.unlink(undofile)
449 449 except OSError as e:
450 450 if e.errno != errno.ENOENT:
451 451 ui.warn(_('error removing %s: %s\n') %
452 452 (undovfs.join(undofile), util.forcebytestr(e)))
453 453
454 454 # Remove partial backup only if there were no exceptions
455 455 vfs.unlink(chgrpfile)
456 456
457 457 def setup():
458 458 """Enable narrow repo support in bundle2-related extension points."""
459 459 extensions.wrapfunction(bundle2, 'getrepocaps', getrepocaps_narrow)
460 460
461 461 wireproto.gboptsmap['narrow'] = 'boolean'
462 462 wireproto.gboptsmap['depth'] = 'plain'
463 463 wireproto.gboptsmap['oldincludepats'] = 'csv'
464 464 wireproto.gboptsmap['oldexcludepats'] = 'csv'
465 465 wireproto.gboptsmap['includepats'] = 'csv'
466 466 wireproto.gboptsmap['excludepats'] = 'csv'
467 467 wireproto.gboptsmap['known'] = 'csv'
468 468
469 469 # Extend changegroup serving to handle requests from narrow clients.
470 470 origcgfn = exchange.getbundle2partsmapping['changegroup']
471 471 def wrappedcgfn(*args, **kwargs):
472 472 repo = args[1]
473 473 if repo.ui.has_section(_NARROWACL_SECTION):
474 474 getbundlechangegrouppart_narrow(
475 475 *args, **applyacl_narrow(repo, kwargs))
476 476 elif kwargs.get(r'narrow', False):
477 477 getbundlechangegrouppart_narrow(*args, **kwargs)
478 478 else:
479 479 origcgfn(*args, **kwargs)
480 480 exchange.getbundle2partsmapping['changegroup'] = wrappedcgfn
481 481
482 # disable rev branch cache exchange when serving a narrow bundle
483 # (currently incompatible with that part)
484 origrbcfn = exchange.getbundle2partsmapping['cache:rev-branch-cache']
485 def wrappedcgfn(*args, **kwargs):
486 repo = args[1]
487 if repo.ui.has_section(_NARROWACL_SECTION):
488 return
489 elif kwargs.get(r'narrow', False):
490 return
491 else:
492 origrbcfn(*args, **kwargs)
493 exchange.getbundle2partsmapping['cache:rev-branch-cache'] = wrappedcgfn
494
482 495 # Extend changegroup receiver so client can fixup after widen requests.
483 496 origcghandler = bundle2.parthandlermapping['changegroup']
484 497 def wrappedcghandler(op, inpart):
485 498 origcghandler(op, inpart)
486 499 if util.safehasattr(op, '_widen_bundle'):
487 500 handlechangegroup_widen(op, inpart)
488 501 wrappedcghandler.params = origcghandler.params
489 502 bundle2.parthandlermapping['changegroup'] = wrappedcghandler
General Comments 0
You need to be logged in to leave comments. Login now