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