Mercurial > public > mercurial-scm > hg-stable
diff 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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mercurial/utils/urlutil.py Sun Apr 11 23:54:35 2021 +0200 @@ -0,0 +1,249 @@ +# utils.urlutil - code related to [paths] management +# +# Copyright 2005-2021 Olivia Mackall <olivia@selenic.com> and others +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. +import os + +from ..i18n import _ +from ..pycompat import ( + getattr, + setattr, +) +from .. import ( + error, + pycompat, + util, +) + + +class paths(dict): + """Represents a collection of paths and their configs. + + Data is initially derived from ui instances and the config files they have + loaded. + """ + + def __init__(self, ui): + dict.__init__(self) + + for name, loc in ui.configitems(b'paths', ignoresub=True): + # No location is the same as not existing. + if not loc: + continue + loc, sub_opts = ui.configsuboptions(b'paths', name) + self[name] = path(ui, name, rawloc=loc, suboptions=sub_opts) + + for name, p in sorted(self.items()): + p.chain_path(ui, self) + + def getpath(self, ui, name, default=None): + """Return a ``path`` from a string, falling back to default. + + ``name`` can be a named path or locations. Locations are filesystem + paths or URIs. + + Returns None if ``name`` is not a registered path, a URI, or a local + path to a repo. + """ + # Only fall back to default if no path was requested. + if name is None: + if not default: + default = () + elif not isinstance(default, (tuple, list)): + default = (default,) + for k in default: + try: + return self[k] + except KeyError: + continue + return None + + # Most likely empty string. + # This may need to raise in the future. + if not name: + return None + + try: + return self[name] + except KeyError: + # Try to resolve as a local path or URI. + try: + # we pass the ui instance are warning might need to be issued + return path(ui, None, rawloc=name) + except ValueError: + raise error.RepoError(_(b'repository %s does not exist') % name) + + +_pathsuboptions = {} + + +def pathsuboption(option, attr): + """Decorator used to declare a path sub-option. + + Arguments are the sub-option name and the attribute it should set on + ``path`` instances. + + The decorated function will receive as arguments a ``ui`` instance, + ``path`` instance, and the string value of this option from the config. + The function should return the value that will be set on the ``path`` + instance. + + This decorator can be used to perform additional verification of + sub-options and to change the type of sub-options. + """ + + def register(func): + _pathsuboptions[option] = (attr, func) + return func + + return register + + +@pathsuboption(b'pushurl', b'pushloc') +def pushurlpathoption(ui, path, value): + u = util.url(value) + # Actually require a URL. + if not u.scheme: + ui.warn(_(b'(paths.%s:pushurl not a URL; ignoring)\n') % path.name) + return None + + # Don't support the #foo syntax in the push URL to declare branch to + # push. + if u.fragment: + ui.warn( + _( + b'("#fragment" in paths.%s:pushurl not supported; ' + b'ignoring)\n' + ) + % path.name + ) + u.fragment = None + + return bytes(u) + + +@pathsuboption(b'pushrev', b'pushrev') +def pushrevpathoption(ui, path, value): + return value + + +class path(object): + """Represents an individual path and its configuration.""" + + def __init__(self, ui, name, rawloc=None, suboptions=None): + """Construct a path from its config options. + + ``ui`` is the ``ui`` instance the path is coming from. + ``name`` is the symbolic name of the path. + ``rawloc`` is the raw location, as defined in the config. + ``pushloc`` is the raw locations pushes should be made to. + + If ``name`` is not defined, we require that the location be a) a local + filesystem path with a .hg directory or b) a URL. If not, + ``ValueError`` is raised. + """ + if not rawloc: + raise ValueError(b'rawloc must be defined') + + # Locations may define branches via syntax <base>#<branch>. + u = util.url(rawloc) + branch = None + if u.fragment: + branch = u.fragment + u.fragment = None + + self.url = u + # the url from the config/command line before dealing with `path://` + self.raw_url = u.copy() + self.branch = branch + + self.name = name + self.rawloc = rawloc + self.loc = b'%s' % u + + self._validate_path() + + _path, sub_opts = ui.configsuboptions(b'paths', b'*') + self._own_sub_opts = {} + if suboptions is not None: + self._own_sub_opts = suboptions.copy() + sub_opts.update(suboptions) + self._all_sub_opts = sub_opts.copy() + + self._apply_suboptions(ui, sub_opts) + + def chain_path(self, ui, paths): + if self.url.scheme == b'path': + assert self.url.path is None + try: + subpath = paths[self.url.host] + except KeyError: + m = _('cannot use `%s`, "%s" is not a known path') + m %= (self.rawloc, self.url.host) + raise error.Abort(m) + if subpath.raw_url.scheme == b'path': + m = _('cannot use `%s`, "%s" is also define as a `path://`') + m %= (self.rawloc, self.url.host) + raise error.Abort(m) + self.url = subpath.url + self.rawloc = subpath.rawloc + self.loc = subpath.loc + if self.branch is None: + self.branch = subpath.branch + else: + base = self.rawloc.rsplit(b'#', 1)[0] + self.rawloc = b'%s#%s' % (base, self.branch) + suboptions = subpath._all_sub_opts.copy() + suboptions.update(self._own_sub_opts) + self._apply_suboptions(ui, suboptions) + + def _validate_path(self): + # When given a raw location but not a symbolic name, validate the + # location is valid. + if ( + not self.name + and not self.url.scheme + and not self._isvalidlocalpath(self.loc) + ): + raise ValueError( + b'location is not a URL or path to a local ' + b'repo: %s' % self.rawloc + ) + + def _apply_suboptions(self, ui, sub_options): + # Now process the sub-options. If a sub-option is registered, its + # attribute will always be present. The value will be None if there + # was no valid sub-option. + for suboption, (attr, func) in pycompat.iteritems(_pathsuboptions): + if suboption not in sub_options: + setattr(self, attr, None) + continue + + value = func(ui, self, sub_options[suboption]) + setattr(self, attr, value) + + def _isvalidlocalpath(self, path): + """Returns True if the given path is a potentially valid repository. + This is its own function so that extensions can change the definition of + 'valid' in this case (like when pulling from a git repo into a hg + one).""" + try: + return os.path.isdir(os.path.join(path, b'.hg')) + # Python 2 may return TypeError. Python 3, ValueError. + except (TypeError, ValueError): + return False + + @property + def suboptions(self): + """Return sub-options and their values for this path. + + This is intended to be used for presentation purposes. + """ + d = {} + for subopt, (attr, _func) in pycompat.iteritems(_pathsuboptions): + value = getattr(self, attr) + if value is not None: + d[subopt] = value + return d