Mercurial > public > mercurial-scm > hg-stable
comparison mercurial/utils/urlutil.py @ 46906:33524c46a092
urlutil: extract `path` related code into a new module
They are a lot of code related to url and path handling scattering into various
large module. To consolidate the code before doing more change (for defining
"multi-path"), we gather it together.
Differential Revision: https://phab.mercurial-scm.org/D10373
author | Pierre-Yves David <pierre-yves.david@octobus.net> |
---|---|
date | Sun, 11 Apr 2021 23:54:35 +0200 |
parents | |
children | ffd3e823a7e5 |
comparison
equal
deleted
inserted
replaced
46905:95a5ed7db9ca | 46906:33524c46a092 |
---|---|
1 # utils.urlutil - code related to [paths] management | |
2 # | |
3 # Copyright 2005-2021 Olivia Mackall <olivia@selenic.com> and others | |
4 # | |
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. | |
7 import os | |
8 | |
9 from ..i18n import _ | |
10 from ..pycompat import ( | |
11 getattr, | |
12 setattr, | |
13 ) | |
14 from .. import ( | |
15 error, | |
16 pycompat, | |
17 util, | |
18 ) | |
19 | |
20 | |
21 class paths(dict): | |
22 """Represents a collection of paths and their configs. | |
23 | |
24 Data is initially derived from ui instances and the config files they have | |
25 loaded. | |
26 """ | |
27 | |
28 def __init__(self, ui): | |
29 dict.__init__(self) | |
30 | |
31 for name, loc in ui.configitems(b'paths', ignoresub=True): | |
32 # No location is the same as not existing. | |
33 if not loc: | |
34 continue | |
35 loc, sub_opts = ui.configsuboptions(b'paths', name) | |
36 self[name] = path(ui, name, rawloc=loc, suboptions=sub_opts) | |
37 | |
38 for name, p in sorted(self.items()): | |
39 p.chain_path(ui, self) | |
40 | |
41 def getpath(self, ui, name, default=None): | |
42 """Return a ``path`` from a string, falling back to default. | |
43 | |
44 ``name`` can be a named path or locations. Locations are filesystem | |
45 paths or URIs. | |
46 | |
47 Returns None if ``name`` is not a registered path, a URI, or a local | |
48 path to a repo. | |
49 """ | |
50 # Only fall back to default if no path was requested. | |
51 if name is None: | |
52 if not default: | |
53 default = () | |
54 elif not isinstance(default, (tuple, list)): | |
55 default = (default,) | |
56 for k in default: | |
57 try: | |
58 return self[k] | |
59 except KeyError: | |
60 continue | |
61 return None | |
62 | |
63 # Most likely empty string. | |
64 # This may need to raise in the future. | |
65 if not name: | |
66 return None | |
67 | |
68 try: | |
69 return self[name] | |
70 except KeyError: | |
71 # Try to resolve as a local path or URI. | |
72 try: | |
73 # we pass the ui instance are warning might need to be issued | |
74 return path(ui, None, rawloc=name) | |
75 except ValueError: | |
76 raise error.RepoError(_(b'repository %s does not exist') % name) | |
77 | |
78 | |
79 _pathsuboptions = {} | |
80 | |
81 | |
82 def pathsuboption(option, attr): | |
83 """Decorator used to declare a path sub-option. | |
84 | |
85 Arguments are the sub-option name and the attribute it should set on | |
86 ``path`` instances. | |
87 | |
88 The decorated function will receive as arguments a ``ui`` instance, | |
89 ``path`` instance, and the string value of this option from the config. | |
90 The function should return the value that will be set on the ``path`` | |
91 instance. | |
92 | |
93 This decorator can be used to perform additional verification of | |
94 sub-options and to change the type of sub-options. | |
95 """ | |
96 | |
97 def register(func): | |
98 _pathsuboptions[option] = (attr, func) | |
99 return func | |
100 | |
101 return register | |
102 | |
103 | |
104 @pathsuboption(b'pushurl', b'pushloc') | |
105 def pushurlpathoption(ui, path, value): | |
106 u = util.url(value) | |
107 # Actually require a URL. | |
108 if not u.scheme: | |
109 ui.warn(_(b'(paths.%s:pushurl not a URL; ignoring)\n') % path.name) | |
110 return None | |
111 | |
112 # Don't support the #foo syntax in the push URL to declare branch to | |
113 # push. | |
114 if u.fragment: | |
115 ui.warn( | |
116 _( | |
117 b'("#fragment" in paths.%s:pushurl not supported; ' | |
118 b'ignoring)\n' | |
119 ) | |
120 % path.name | |
121 ) | |
122 u.fragment = None | |
123 | |
124 return bytes(u) | |
125 | |
126 | |
127 @pathsuboption(b'pushrev', b'pushrev') | |
128 def pushrevpathoption(ui, path, value): | |
129 return value | |
130 | |
131 | |
132 class path(object): | |
133 """Represents an individual path and its configuration.""" | |
134 | |
135 def __init__(self, ui, name, rawloc=None, suboptions=None): | |
136 """Construct a path from its config options. | |
137 | |
138 ``ui`` is the ``ui`` instance the path is coming from. | |
139 ``name`` is the symbolic name of the path. | |
140 ``rawloc`` is the raw location, as defined in the config. | |
141 ``pushloc`` is the raw locations pushes should be made to. | |
142 | |
143 If ``name`` is not defined, we require that the location be a) a local | |
144 filesystem path with a .hg directory or b) a URL. If not, | |
145 ``ValueError`` is raised. | |
146 """ | |
147 if not rawloc: | |
148 raise ValueError(b'rawloc must be defined') | |
149 | |
150 # Locations may define branches via syntax <base>#<branch>. | |
151 u = util.url(rawloc) | |
152 branch = None | |
153 if u.fragment: | |
154 branch = u.fragment | |
155 u.fragment = None | |
156 | |
157 self.url = u | |
158 # the url from the config/command line before dealing with `path://` | |
159 self.raw_url = u.copy() | |
160 self.branch = branch | |
161 | |
162 self.name = name | |
163 self.rawloc = rawloc | |
164 self.loc = b'%s' % u | |
165 | |
166 self._validate_path() | |
167 | |
168 _path, sub_opts = ui.configsuboptions(b'paths', b'*') | |
169 self._own_sub_opts = {} | |
170 if suboptions is not None: | |
171 self._own_sub_opts = suboptions.copy() | |
172 sub_opts.update(suboptions) | |
173 self._all_sub_opts = sub_opts.copy() | |
174 | |
175 self._apply_suboptions(ui, sub_opts) | |
176 | |
177 def chain_path(self, ui, paths): | |
178 if self.url.scheme == b'path': | |
179 assert self.url.path is None | |
180 try: | |
181 subpath = paths[self.url.host] | |
182 except KeyError: | |
183 m = _('cannot use `%s`, "%s" is not a known path') | |
184 m %= (self.rawloc, self.url.host) | |
185 raise error.Abort(m) | |
186 if subpath.raw_url.scheme == b'path': | |
187 m = _('cannot use `%s`, "%s" is also define as a `path://`') | |
188 m %= (self.rawloc, self.url.host) | |
189 raise error.Abort(m) | |
190 self.url = subpath.url | |
191 self.rawloc = subpath.rawloc | |
192 self.loc = subpath.loc | |
193 if self.branch is None: | |
194 self.branch = subpath.branch | |
195 else: | |
196 base = self.rawloc.rsplit(b'#', 1)[0] | |
197 self.rawloc = b'%s#%s' % (base, self.branch) | |
198 suboptions = subpath._all_sub_opts.copy() | |
199 suboptions.update(self._own_sub_opts) | |
200 self._apply_suboptions(ui, suboptions) | |
201 | |
202 def _validate_path(self): | |
203 # When given a raw location but not a symbolic name, validate the | |
204 # location is valid. | |
205 if ( | |
206 not self.name | |
207 and not self.url.scheme | |
208 and not self._isvalidlocalpath(self.loc) | |
209 ): | |
210 raise ValueError( | |
211 b'location is not a URL or path to a local ' | |
212 b'repo: %s' % self.rawloc | |
213 ) | |
214 | |
215 def _apply_suboptions(self, ui, sub_options): | |
216 # Now process the sub-options. If a sub-option is registered, its | |
217 # attribute will always be present. The value will be None if there | |
218 # was no valid sub-option. | |
219 for suboption, (attr, func) in pycompat.iteritems(_pathsuboptions): | |
220 if suboption not in sub_options: | |
221 setattr(self, attr, None) | |
222 continue | |
223 | |
224 value = func(ui, self, sub_options[suboption]) | |
225 setattr(self, attr, value) | |
226 | |
227 def _isvalidlocalpath(self, path): | |
228 """Returns True if the given path is a potentially valid repository. | |
229 This is its own function so that extensions can change the definition of | |
230 'valid' in this case (like when pulling from a git repo into a hg | |
231 one).""" | |
232 try: | |
233 return os.path.isdir(os.path.join(path, b'.hg')) | |
234 # Python 2 may return TypeError. Python 3, ValueError. | |
235 except (TypeError, ValueError): | |
236 return False | |
237 | |
238 @property | |
239 def suboptions(self): | |
240 """Return sub-options and their values for this path. | |
241 | |
242 This is intended to be used for presentation purposes. | |
243 """ | |
244 d = {} | |
245 for subopt, (attr, _func) in pycompat.iteritems(_pathsuboptions): | |
246 value = getattr(self, attr) | |
247 if value is not None: | |
248 d[subopt] = value | |
249 return d |