##// END OF EJS Templates
typing: add some trivial type hints to `mercurial/bundlecaches.py`...
Matt Harbison -
r52565:138ab7c6 default
parent child Browse files
Show More
@@ -1,562 +1,562
1 1 # bundlecaches.py - utility to deal with pre-computed bundle for servers
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 4 # GNU General Public License version 2 or any later version.
5 5
6 6 import collections
7 7
8 8 from typing import (
9 9 Dict,
10 10 Union,
11 11 cast,
12 12 )
13 13
14 14 from .i18n import _
15 15
16 16 from .thirdparty import attr
17 17
18 18 from . import (
19 19 error,
20 20 requirements as requirementsmod,
21 21 sslutil,
22 22 util,
23 23 )
24 24 from .utils import stringutil
25 25
26 26 urlreq = util.urlreq
27 27
28 28 BUNDLE_CACHE_DIR = b'bundle-cache'
29 29 CB_MANIFEST_FILE = b'clonebundles.manifest'
30 30 CLONEBUNDLESCHEME = b"peer-bundle-cache://"
31 31
32 32
33 def get_manifest(repo):
33 def get_manifest(repo) -> bytes:
34 34 """get the bundle manifest to be served to a client from a server"""
35 35 raw_text = repo.vfs.tryread(CB_MANIFEST_FILE)
36 36 entries = [e.split(b' ', 1) for e in raw_text.splitlines()]
37 37
38 38 new_lines = []
39 39 for e in entries:
40 40 url = alter_bundle_url(repo, e[0])
41 41 if len(e) == 1:
42 42 line = url + b'\n'
43 43 else:
44 44 line = b"%s %s\n" % (url, e[1])
45 45 new_lines.append(line)
46 46 return b''.join(new_lines)
47 47
48 48
49 def alter_bundle_url(repo, url):
49 def alter_bundle_url(repo, url: bytes) -> bytes:
50 50 """a function that exist to help extension and hosting to alter the url
51 51
52 52 This will typically be used to inject authentication information in the url
53 53 of cached bundles."""
54 54 return url
55 55
56 56
57 57 SUPPORTED_CLONEBUNDLE_SCHEMES = [
58 58 b"http://",
59 59 b"https://",
60 60 b"largefile://",
61 61 CLONEBUNDLESCHEME,
62 62 ]
63 63
64 64
65 65 @attr.s
66 66 class bundlespec:
67 67 compression = attr.ib()
68 68 wirecompression = attr.ib()
69 69 version = attr.ib()
70 70 wireversion = attr.ib()
71 71 # parameters explicitly overwritten by the config or the specification
72 72 _explicit_params = attr.ib()
73 73 # default parameter for the version
74 74 #
75 75 # Keeping it separated is useful to check what was actually overwritten.
76 76 _default_opts = attr.ib()
77 77
78 78 @property
79 79 def params(self):
80 80 return collections.ChainMap(self._explicit_params, self._default_opts)
81 81
82 82 @property
83 83 def contentopts(self):
84 84 # kept for Backward Compatibility concerns.
85 85 return self.params
86 86
87 87 def set_param(self, key, value, overwrite=True):
88 88 """Set a bundle parameter value.
89 89
90 90 Will only overwrite if overwrite is true"""
91 91 if overwrite or key not in self._explicit_params:
92 92 self._explicit_params[key] = value
93 93
94 94 def as_spec(self):
95 95 parts = [b"%s-%s" % (self.compression, self.version)]
96 96 for param in sorted(self._explicit_params.items()):
97 97 parts.append(b'%s=%s' % param)
98 98 return b';'.join(parts)
99 99
100 100
101 101 # Maps bundle version human names to changegroup versions.
102 102 _bundlespeccgversions = {
103 103 b'v1': b'01',
104 104 b'v2': b'02',
105 105 b'v3': b'03',
106 106 b'packed1': b's1',
107 107 b'bundle2': b'02', # legacy
108 108 }
109 109
110 110 # Maps bundle version with content opts to choose which part to bundle
111 111 _bundlespeccontentopts: Dict[bytes, Dict[bytes, Union[bool, bytes]]] = {
112 112 b'v1': {
113 113 b'changegroup': True,
114 114 b'cg.version': b'01',
115 115 b'obsolescence': False,
116 116 b'phases': False,
117 117 b'tagsfnodescache': False,
118 118 b'revbranchcache': False,
119 119 },
120 120 b'v2': {
121 121 b'changegroup': True,
122 122 b'cg.version': b'02',
123 123 b'obsolescence': False,
124 124 b'phases': False,
125 125 b'tagsfnodescache': True,
126 126 b'revbranchcache': True,
127 127 },
128 128 b'v3': {
129 129 b'changegroup': True,
130 130 b'cg.version': b'03',
131 131 b'obsolescence': False,
132 132 b'phases': True,
133 133 b'tagsfnodescache': True,
134 134 b'revbranchcache': True,
135 135 },
136 136 b'streamv2': {
137 137 b'changegroup': False,
138 138 b'cg.version': b'02',
139 139 b'obsolescence': False,
140 140 b'phases': False,
141 141 b"stream": b"v2",
142 142 b'tagsfnodescache': False,
143 143 b'revbranchcache': False,
144 144 },
145 145 b'streamv3-exp': {
146 146 b'changegroup': False,
147 147 b'cg.version': b'03',
148 148 b'obsolescence': False,
149 149 b'phases': False,
150 150 b"stream": b"v3-exp",
151 151 b'tagsfnodescache': False,
152 152 b'revbranchcache': False,
153 153 },
154 154 b'packed1': {
155 155 b'cg.version': b's1',
156 156 },
157 157 b'bundle2': { # legacy
158 158 b'cg.version': b'02',
159 159 },
160 160 }
161 161 _bundlespeccontentopts[b'bundle2'] = _bundlespeccontentopts[b'v2']
162 162
163 163 # Compression engines allowed in version 1. THIS SHOULD NEVER CHANGE.
164 164 _bundlespecv1compengines = {b'gzip', b'bzip2', b'none'}
165 165
166 166
167 167 def param_bool(key, value):
168 168 """make a boolean out of a parameter value"""
169 169 b = stringutil.parsebool(value)
170 170 if b is None:
171 171 msg = _(b"parameter %s should be a boolean ('%s')")
172 172 msg %= (key, value)
173 173 raise error.InvalidBundleSpecification(msg)
174 174 return b
175 175
176 176
177 177 # mapping of known parameter name need their value processed
178 178 bundle_spec_param_processing = {
179 179 b"obsolescence": param_bool,
180 180 b"obsolescence-mandatory": param_bool,
181 181 b"phases": param_bool,
182 182 b"changegroup": param_bool,
183 183 b"tagsfnodescache": param_bool,
184 184 b"revbranchcache": param_bool,
185 185 }
186 186
187 187
188 188 def _parseparams(s):
189 189 """parse bundlespec parameter section
190 190
191 191 input: "comp-version;params" string
192 192
193 193 return: (spec; {param_key: param_value})
194 194 """
195 195 if b';' not in s:
196 196 return s, {}
197 197
198 198 params = {}
199 199 version, paramstr = s.split(b';', 1)
200 200
201 201 err = _(b'invalid bundle specification: missing "=" in parameter: %s')
202 202 for p in paramstr.split(b';'):
203 203 if b'=' not in p:
204 204 msg = err % p
205 205 raise error.InvalidBundleSpecification(msg)
206 206
207 207 key, value = p.split(b'=', 1)
208 208 key = urlreq.unquote(key)
209 209 value = urlreq.unquote(value)
210 210 process = bundle_spec_param_processing.get(key)
211 211 if process is not None:
212 212 value = process(key, value)
213 213 params[key] = value
214 214
215 215 return version, params
216 216
217 217
218 218 def parsebundlespec(repo, spec, strict=True):
219 219 """Parse a bundle string specification into parts.
220 220
221 221 Bundle specifications denote a well-defined bundle/exchange format.
222 222 The content of a given specification should not change over time in
223 223 order to ensure that bundles produced by a newer version of Mercurial are
224 224 readable from an older version.
225 225
226 226 The string currently has the form:
227 227
228 228 <compression>-<type>[;<parameter0>[;<parameter1>]]
229 229
230 230 Where <compression> is one of the supported compression formats
231 231 and <type> is (currently) a version string. A ";" can follow the type and
232 232 all text afterwards is interpreted as URI encoded, ";" delimited key=value
233 233 pairs.
234 234
235 235 If ``strict`` is True (the default) <compression> is required. Otherwise,
236 236 it is optional.
237 237
238 238 Returns a bundlespec object of (compression, version, parameters).
239 239 Compression will be ``None`` if not in strict mode and a compression isn't
240 240 defined.
241 241
242 242 An ``InvalidBundleSpecification`` is raised when the specification is
243 243 not syntactically well formed.
244 244
245 245 An ``UnsupportedBundleSpecification`` is raised when the compression or
246 246 bundle type/version is not recognized.
247 247
248 248 Note: this function will likely eventually return a more complex data
249 249 structure, including bundle2 part information.
250 250 """
251 251 if strict and b'-' not in spec:
252 252 raise error.InvalidBundleSpecification(
253 253 _(
254 254 b'invalid bundle specification; '
255 255 b'must be prefixed with compression: %s'
256 256 )
257 257 % spec
258 258 )
259 259
260 260 pre_args = spec.split(b';', 1)[0]
261 261 if b'-' in pre_args:
262 262 compression, version = spec.split(b'-', 1)
263 263
264 264 if compression not in util.compengines.supportedbundlenames:
265 265 raise error.UnsupportedBundleSpecification(
266 266 _(b'%s compression is not supported') % compression
267 267 )
268 268
269 269 version, params = _parseparams(version)
270 270
271 271 if version not in _bundlespeccontentopts:
272 272 raise error.UnsupportedBundleSpecification(
273 273 _(b'%s is not a recognized bundle version') % version
274 274 )
275 275 else:
276 276 # Value could be just the compression or just the version, in which
277 277 # case some defaults are assumed (but only when not in strict mode).
278 278 assert not strict
279 279
280 280 spec, params = _parseparams(spec)
281 281
282 282 if spec in util.compengines.supportedbundlenames:
283 283 compression = spec
284 284 version = b'v1'
285 285 # Generaldelta repos require v2.
286 286 if requirementsmod.GENERALDELTA_REQUIREMENT in repo.requirements:
287 287 version = b'v2'
288 288 elif requirementsmod.REVLOGV2_REQUIREMENT in repo.requirements:
289 289 version = b'v2'
290 290 # Modern compression engines require v2.
291 291 if compression not in _bundlespecv1compengines:
292 292 version = b'v2'
293 293 elif spec in _bundlespeccontentopts:
294 294 if spec == b'packed1':
295 295 compression = b'none'
296 296 else:
297 297 compression = b'bzip2'
298 298 version = spec
299 299 else:
300 300 raise error.UnsupportedBundleSpecification(
301 301 _(b'%s is not a recognized bundle specification') % spec
302 302 )
303 303
304 304 # Bundle version 1 only supports a known set of compression engines.
305 305 if version == b'v1' and compression not in _bundlespecv1compengines:
306 306 raise error.UnsupportedBundleSpecification(
307 307 _(b'compression engine %s is not supported on v1 bundles')
308 308 % compression
309 309 )
310 310
311 311 # The specification for packed1 can optionally declare the data formats
312 312 # required to apply it. If we see this metadata, compare against what the
313 313 # repo supports and error if the bundle isn't compatible.
314 314 if version == b'packed1' and b'requirements' in params:
315 315 requirements = set(cast(bytes, params[b'requirements']).split(b','))
316 316 missingreqs = requirements - requirementsmod.STREAM_FIXED_REQUIREMENTS
317 317 if missingreqs:
318 318 raise error.UnsupportedBundleSpecification(
319 319 _(b'missing support for repository features: %s')
320 320 % b', '.join(sorted(missingreqs))
321 321 )
322 322
323 323 # Compute contentopts based on the version
324 324 if b"stream" in params:
325 325 # This case is fishy as this mostly derails the version selection
326 326 # mechanism. `stream` bundles are quite specific and used differently
327 327 # as "normal" bundles.
328 328 #
329 329 # (we should probably define a cleaner way to do this and raise a
330 330 # warning when the old way is encountered)
331 331 if params[b"stream"] == b"v2":
332 332 version = b"streamv2"
333 333 if params[b"stream"] == b"v3-exp":
334 334 version = b"streamv3-exp"
335 335 contentopts = _bundlespeccontentopts.get(version, {}).copy()
336 336 if version == b"streamv2" or version == b"streamv3-exp":
337 337 # streamv2 have been reported as "v2" for a while.
338 338 version = b"v2"
339 339
340 340 engine = util.compengines.forbundlename(compression)
341 341 compression, wirecompression = engine.bundletype()
342 342 wireversion = _bundlespeccontentopts[version][b'cg.version']
343 343
344 344 return bundlespec(
345 345 compression, wirecompression, version, wireversion, params, contentopts
346 346 )
347 347
348 348
349 349 def parseclonebundlesmanifest(repo, s):
350 350 """Parses the raw text of a clone bundles manifest.
351 351
352 352 Returns a list of dicts. The dicts have a ``URL`` key corresponding
353 353 to the URL and other keys are the attributes for the entry.
354 354 """
355 355 m = []
356 356 for line in s.splitlines():
357 357 fields = line.split()
358 358 if not fields:
359 359 continue
360 360 attrs = {b'URL': fields[0]}
361 361 for rawattr in fields[1:]:
362 362 key, value = rawattr.split(b'=', 1)
363 363 key = util.urlreq.unquote(key)
364 364 value = util.urlreq.unquote(value)
365 365 attrs[key] = value
366 366
367 367 # Parse BUNDLESPEC into components. This makes client-side
368 368 # preferences easier to specify since you can prefer a single
369 369 # component of the BUNDLESPEC.
370 370 if key == b'BUNDLESPEC':
371 371 try:
372 372 bundlespec = parsebundlespec(repo, value)
373 373 attrs[b'COMPRESSION'] = bundlespec.compression
374 374 attrs[b'VERSION'] = bundlespec.version
375 375 except error.InvalidBundleSpecification:
376 376 pass
377 377 except error.UnsupportedBundleSpecification:
378 378 pass
379 379
380 380 m.append(attrs)
381 381
382 382 return m
383 383
384 384
385 385 def isstreamclonespec(bundlespec):
386 386 # Stream clone v1
387 387 if bundlespec.wirecompression == b'UN' and bundlespec.wireversion == b's1':
388 388 return True
389 389
390 390 # Stream clone v2
391 391 if (
392 392 bundlespec.wirecompression == b'UN'
393 393 and bundlespec.wireversion == b'02'
394 394 and bundlespec.contentopts.get(b'stream', None) in (b"v2", b"v3-exp")
395 395 ):
396 396 return True
397 397
398 398 return False
399 399
400 400
401 401 def filterclonebundleentries(
402 402 repo, entries, streamclonerequested=False, pullbundles=False
403 403 ):
404 404 """Remove incompatible clone bundle manifest entries.
405 405
406 406 Accepts a list of entries parsed with ``parseclonebundlesmanifest``
407 407 and returns a new list consisting of only the entries that this client
408 408 should be able to apply.
409 409
410 410 There is no guarantee we'll be able to apply all returned entries because
411 411 the metadata we use to filter on may be missing or wrong.
412 412 """
413 413 newentries = []
414 414 for entry in entries:
415 415 url = entry.get(b'URL')
416 416 if not pullbundles and not any(
417 417 [url.startswith(scheme) for scheme in SUPPORTED_CLONEBUNDLE_SCHEMES]
418 418 ):
419 419 repo.ui.debug(
420 420 b'filtering %s because not a supported clonebundle scheme\n'
421 421 % url
422 422 )
423 423 continue
424 424
425 425 spec = entry.get(b'BUNDLESPEC')
426 426 if spec:
427 427 try:
428 428 bundlespec = parsebundlespec(repo, spec, strict=True)
429 429
430 430 # If a stream clone was requested, filter out non-streamclone
431 431 # entries.
432 432 if streamclonerequested and not isstreamclonespec(bundlespec):
433 433 repo.ui.debug(
434 434 b'filtering %s because not a stream clone\n' % url
435 435 )
436 436 continue
437 437
438 438 except error.InvalidBundleSpecification as e:
439 439 repo.ui.debug(stringutil.forcebytestr(e) + b'\n')
440 440 continue
441 441 except error.UnsupportedBundleSpecification as e:
442 442 repo.ui.debug(
443 443 b'filtering %s because unsupported bundle '
444 444 b'spec: %s\n' % (url, stringutil.forcebytestr(e))
445 445 )
446 446 continue
447 447 # If we don't have a spec and requested a stream clone, we don't know
448 448 # what the entry is so don't attempt to apply it.
449 449 elif streamclonerequested:
450 450 repo.ui.debug(
451 451 b'filtering %s because cannot determine if a stream '
452 452 b'clone bundle\n' % url
453 453 )
454 454 continue
455 455
456 456 if b'REQUIRESNI' in entry and not sslutil.hassni:
457 457 repo.ui.debug(b'filtering %s because SNI not supported\n' % url)
458 458 continue
459 459
460 460 if b'REQUIREDRAM' in entry:
461 461 try:
462 462 requiredram = util.sizetoint(entry[b'REQUIREDRAM'])
463 463 except error.ParseError:
464 464 repo.ui.debug(
465 465 b'filtering %s due to a bad REQUIREDRAM attribute\n' % url
466 466 )
467 467 continue
468 468 actualram = repo.ui.estimatememory()
469 469 if actualram is not None and actualram * 0.66 < requiredram:
470 470 repo.ui.debug(
471 471 b'filtering %s as it needs more than 2/3 of system memory\n'
472 472 % url
473 473 )
474 474 continue
475 475
476 476 newentries.append(entry)
477 477
478 478 return newentries
479 479
480 480
481 481 class clonebundleentry:
482 482 """Represents an item in a clone bundles manifest.
483 483
484 484 This rich class is needed to support sorting since sorted() in Python 3
485 485 doesn't support ``cmp`` and our comparison is complex enough that ``key=``
486 486 won't work.
487 487 """
488 488
489 489 def __init__(self, value, prefers):
490 490 self.value = value
491 491 self.prefers = prefers
492 492
493 493 def _cmp(self, other):
494 494 for prefkey, prefvalue in self.prefers:
495 495 avalue = self.value.get(prefkey)
496 496 bvalue = other.value.get(prefkey)
497 497
498 498 # Special case for b missing attribute and a matches exactly.
499 499 if avalue is not None and bvalue is None and avalue == prefvalue:
500 500 return -1
501 501
502 502 # Special case for a missing attribute and b matches exactly.
503 503 if bvalue is not None and avalue is None and bvalue == prefvalue:
504 504 return 1
505 505
506 506 # We can't compare unless attribute present on both.
507 507 if avalue is None or bvalue is None:
508 508 continue
509 509
510 510 # Same values should fall back to next attribute.
511 511 if avalue == bvalue:
512 512 continue
513 513
514 514 # Exact matches come first.
515 515 if avalue == prefvalue:
516 516 return -1
517 517 if bvalue == prefvalue:
518 518 return 1
519 519
520 520 # Fall back to next attribute.
521 521 continue
522 522
523 523 # If we got here we couldn't sort by attributes and prefers. Fall
524 524 # back to index order.
525 525 return 0
526 526
527 527 def __lt__(self, other):
528 528 return self._cmp(other) < 0
529 529
530 530 def __gt__(self, other):
531 531 return self._cmp(other) > 0
532 532
533 533 def __eq__(self, other):
534 534 return self._cmp(other) == 0
535 535
536 536 def __le__(self, other):
537 537 return self._cmp(other) <= 0
538 538
539 539 def __ge__(self, other):
540 540 return self._cmp(other) >= 0
541 541
542 542 def __ne__(self, other):
543 543 return self._cmp(other) != 0
544 544
545 545
546 546 def sortclonebundleentries(ui, entries):
547 547 prefers = ui.configlist(b'ui', b'clonebundleprefers')
548 548 if not prefers:
549 549 return list(entries)
550 550
551 551 def _split(p):
552 552 if b'=' not in p:
553 553 hint = _(b"each comma separated item should be key=value pairs")
554 554 raise error.Abort(
555 555 _(b"invalid ui.clonebundleprefers item: %s") % p, hint=hint
556 556 )
557 557 return p.split(b'=', 1)
558 558
559 559 prefers = [_split(p) for p in prefers]
560 560
561 561 items = sorted(clonebundleentry(v, prefers) for v in entries)
562 562 return [i.value for i in items]
General Comments 0
You need to be logged in to leave comments. Login now