comparison mercurial/ui.py @ 27266:4dccc37b87bd

ui: support declaring path push urls as sub-options Power users often want to apply per-path configuration options. For example, they may want to declare an alternate URL for push operations or declare a revset of revisions to push when `hg push` is used (as opposed to attempting to push all revisions by default). This patch establishes the use of sub-options (config options with ":" in the name) to declare additional behavior for paths. New sub-options are declared by using the new ``@ui.pathsuboption`` decorator. This decorator serves multiple purposes: * Declaring which sub-options are registered * Declaring how a sub-option maps to an attribute on ``path`` instances (this is needed to `hg paths` can render sub-options and values properly) * Validation and normalization of config options to attribute values * Allows extensions to declare new sub-options without monkeypatching * Allows extensions to overwrite built-in behavior for sub-option handling As convenient as the new option registration decorator is, extensions (and even core functionality) may still need an additional hook point to perform finalization of path instances. For example, they may wish to validate that multiple options/attributes aren't conflicting with each other. This hook point could be added later, if needed. To prove this new functionality works, we implement the "pushurl" path sub-option. This option declares the URL that `hg push` should use by default. We require that "pushurl" is an actual URL. This requirement might be controversial and could be dropped if there is opposition. However, objectors should read the complicated code in ui.path.__init__ and commands.push for resolving non-URL values before making a judgement. We also don't allow #fragment in the URLs. I intend to introduce a ":pushrev" (or similar) option to define a revset to control which revisions are pushed when "-r <rev>" isn't passed into `hg push`. This is much more powerful than #fragment and I don't think #fragment is useful enough to continue supporting. The [paths] section of the "config" help page has been updated significantly. `hg paths` has been taught to display path sub-options. The docs mention that "default-push" is now deprecated. However, there are several references to it that need to be cleaned up. A large part of this is converting more consumers to the new paths API. This will happen naturally as more path sub-options are added and more and more components need to access them.
author Gregory Szorc <gregory.szorc@gmail.com>
date Sat, 05 Dec 2015 21:11:04 -0800
parents 47539ea08bdb
children 82910fdc216f
comparison
equal deleted inserted replaced
27265:47539ea08bdb 27266:4dccc37b87bd
1061 loaded. 1061 loaded.
1062 """ 1062 """
1063 def __init__(self, ui): 1063 def __init__(self, ui):
1064 dict.__init__(self) 1064 dict.__init__(self)
1065 1065
1066 for name, loc in ui.configitems('paths'): 1066 for name, loc in ui.configitems('paths', ignoresub=True):
1067 # No location is the same as not existing. 1067 # No location is the same as not existing.
1068 if not loc: 1068 if not loc:
1069 continue 1069 continue
1070 1070
1071 # TODO ignore default-push once all consumers stop referencing it 1071 # TODO ignore default-push once all consumers stop referencing it
1072 # since it is handled specifically below. 1072 # since it is handled specifically below.
1073 1073
1074 self[name] = path(ui, name, rawloc=loc) 1074 loc, sub = ui.configsuboptions('paths', name)
1075 self[name] = path(ui, name, rawloc=loc, suboptions=sub)
1075 1076
1076 # Handle default-push, which is a one-off that defines the push URL for 1077 # Handle default-push, which is a one-off that defines the push URL for
1077 # the "default" path. 1078 # the "default" path.
1078 defaultpush = ui.config('paths', 'default-push') 1079 defaultpush = ui.config('paths', 'default-push')
1079 if defaultpush: 1080 if defaultpush:
1118 raise error.RepoError(_('repository %s does not exist') % 1119 raise error.RepoError(_('repository %s does not exist') %
1119 name) 1120 name)
1120 1121
1121 assert False 1122 assert False
1122 1123
1124 _pathsuboptions = {}
1125
1126 def pathsuboption(option, attr):
1127 """Decorator used to declare a path sub-option.
1128
1129 Arguments are the sub-option name and the attribute it should set on
1130 ``path`` instances.
1131
1132 The decorated function will receive as arguments a ``ui`` instance,
1133 ``path`` instance, and the string value of this option from the config.
1134 The function should return the value that will be set on the ``path``
1135 instance.
1136
1137 This decorator can be used to perform additional verification of
1138 sub-options and to change the type of sub-options.
1139 """
1140 def register(func):
1141 _pathsuboptions[option] = (attr, func)
1142 return func
1143 return register
1144
1145 @pathsuboption('pushurl', 'pushloc')
1146 def pushurlpathoption(ui, path, value):
1147 u = util.url(value)
1148 # Actually require a URL.
1149 if not u.scheme:
1150 ui.warn(_('(paths.%s:pushurl not a URL; ignoring)\n') % path.name)
1151 return None
1152
1153 # Don't support the #foo syntax in the push URL to declare branch to
1154 # push.
1155 if u.fragment:
1156 ui.warn(_('("#fragment" in paths.%s:pushurl not supported; '
1157 'ignoring)\n') % path.name)
1158 u.fragment = None
1159
1160 return str(u)
1161
1123 class path(object): 1162 class path(object):
1124 """Represents an individual path and its configuration.""" 1163 """Represents an individual path and its configuration."""
1125 1164
1126 def __init__(self, ui, name, rawloc=None, pushloc=None): 1165 def __init__(self, ui, name, rawloc=None, suboptions=None):
1127 """Construct a path from its config options. 1166 """Construct a path from its config options.
1128 1167
1129 ``ui`` is the ``ui`` instance the path is coming from. 1168 ``ui`` is the ``ui`` instance the path is coming from.
1130 ``name`` is the symbolic name of the path. 1169 ``name`` is the symbolic name of the path.
1131 ``rawloc`` is the raw location, as defined in the config. 1170 ``rawloc`` is the raw location, as defined in the config.
1149 self.branch = branch 1188 self.branch = branch
1150 1189
1151 self.name = name 1190 self.name = name
1152 self.rawloc = rawloc 1191 self.rawloc = rawloc
1153 self.loc = str(u) 1192 self.loc = str(u)
1154 self.pushloc = pushloc
1155 1193
1156 # When given a raw location but not a symbolic name, validate the 1194 # When given a raw location but not a symbolic name, validate the
1157 # location is valid. 1195 # location is valid.
1158 if not name and not u.scheme and not self._isvalidlocalpath(self.loc): 1196 if not name and not u.scheme and not self._isvalidlocalpath(self.loc):
1159 raise ValueError('location is not a URL or path to a local ' 1197 raise ValueError('location is not a URL or path to a local '
1160 'repo: %s' % rawloc) 1198 'repo: %s' % rawloc)
1199
1200 suboptions = suboptions or {}
1201
1202 # Now process the sub-options. If a sub-option is registered, its
1203 # attribute will always be present. The value will be None if there
1204 # was no valid sub-option.
1205 for suboption, (attr, func) in _pathsuboptions.iteritems():
1206 if suboption not in suboptions:
1207 setattr(self, attr, None)
1208 continue
1209
1210 value = func(ui, self, suboptions[suboption])
1211 setattr(self, attr, value)
1161 1212
1162 def _isvalidlocalpath(self, path): 1213 def _isvalidlocalpath(self, path):
1163 """Returns True if the given path is a potentially valid repository. 1214 """Returns True if the given path is a potentially valid repository.
1164 This is its own function so that extensions can change the definition of 1215 This is its own function so that extensions can change the definition of
1165 'valid' in this case (like when pulling from a git repo into a hg 1216 'valid' in this case (like when pulling from a git repo into a hg
1166 one).""" 1217 one)."""
1167 return os.path.isdir(os.path.join(path, '.hg')) 1218 return os.path.isdir(os.path.join(path, '.hg'))
1219
1220 @property
1221 def suboptions(self):
1222 """Return sub-options and their values for this path.
1223
1224 This is intended to be used for presentation purposes.
1225 """
1226 d = {}
1227 for subopt, (attr, _func) in _pathsuboptions.iteritems():
1228 value = getattr(self, attr)
1229 if value is not None:
1230 d[subopt] = value
1231 return d
1168 1232
1169 # we instantiate one globally shared progress bar to avoid 1233 # we instantiate one globally shared progress bar to avoid
1170 # competing progress bars when multiple UI objects get created 1234 # competing progress bars when multiple UI objects get created
1171 _progresssingleton = None 1235 _progresssingleton = None
1172 1236