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