Mercurial > public > mercurial-scm > evolve
diff hgext3rd/topic/__init__.py @ 6360:e959390490c2
branching: merge with stable
author | Anton Shestakov <av6@dwimlabs.net> |
---|---|
date | Fri, 09 Dec 2022 15:01:59 +0400 |
parents | 453861da6922 |
children | 573174ef1bbf |
line wrap: on
line diff
--- a/hgext3rd/topic/__init__.py Fri Dec 09 14:57:14 2022 +0400 +++ b/hgext3rd/topic/__init__.py Fri Dec 09 15:01:59 2022 +0400 @@ -168,7 +168,9 @@ changelog, cmdutil, commands, + configitems, context, + encoding, error, exchange, extensions, @@ -231,64 +233,86 @@ b'log.topic': b'green_background', } -__version__ = b'0.24.3.dev' +__version__ = b'0.25.0.dev' -testedwith = b'4.8 4.9 5.0 5.1 5.2 5.3 5.4 5.5 5.6 5.7 5.8 5.9 6.0 6.1 6.2 6.3' +testedwith = b'4.8 4.9 5.0 5.1 5.2 5.3 5.4 5.5 5.6 5.7 5.8 5.9 6.0 6.1 6.2' minimumhgversion = b'4.8' buglink = b'https://bz.mercurial-scm.org/' -if util.safehasattr(registrar, 'configitem'): - - from mercurial import configitems - - configtable = {} - configitem = registrar.configitem(configtable) +configtable = {} +configitem = registrar.configitem(configtable) - configitem(b'experimental', b'enforce-topic', - default=False, - ) - configitem(b'experimental', b'enforce-single-head', - default=False, - ) - configitem(b'experimental', b'topic-mode', - default=None, - ) - configitem(b'experimental', b'topic.publish-bare-branch', - default=False, - ) - configitem(b'experimental', b'topic.allow-publish', - default=configitems.dynamicdefault, - ) - configitem(b'_internal', b'keep-topic', - default=False, - ) - configitem(b'experimental', b'topic-mode.server', - default=configitems.dynamicdefault, - ) - configitem(b'experimental', b'topic.server-gate-topic-changesets', - default=False, - ) - configitem(b'experimental', b'topic.linear-merge', - default="reject", - ) +configitem(b'experimental', b'enforce-topic', + default=False, +) +configitem(b'experimental', b'enforce-single-head', + default=False, +) +configitem(b'experimental', b'topic-mode', + default=None, +) +configitem(b'experimental', b'topic.publish-bare-branch', + default=False, +) +configitem(b'experimental', b'topic.allow-publish', + default=configitems.dynamicdefault, +) +configitem(b'_internal', b'keep-topic', + default=False, +) +# used for signaling that ctx.branch() shouldn't return fqbn even if topic is +# enabled for local repo +configitem(b'_internal', b'tns-disable-fqbn', + default=False, +) +# used for signaling that push will publish changesets +configitem(b'_internal', b'tns-publish', + default=False, +) +configitem(b'experimental', b'topic-mode.server', + default=configitems.dynamicdefault, +) +configitem(b'experimental', b'topic.server-gate-topic-changesets', + default=False, +) +configitem(b'experimental', b'topic.linear-merge', + default="reject", +) - def extsetup(ui): - # register config that strictly belong to other code (thg, core, etc) - # - # To ensure all config items we used are registered, we register them if - # nobody else did so far. - from mercurial import configitems - extraitem = functools.partial(configitems._register, ui._knownconfig) - if (b'experimental' not in ui._knownconfig - or not ui._knownconfig[b'experimental'].get(b'thg.displaynames')): - extraitem(b'experimental', b'thg.displaynames', - default=None, - ) - if (b'devel' not in ui._knownconfig - or not ui._knownconfig[b'devel'].get(b'random')): - extraitem(b'devel', b'randomseed', - default=None, - ) +def extsetup(ui): + # register config that strictly belong to other code (thg, core, etc) + # + # To ensure all config items we used are registered, we register them if + # nobody else did so far. + extraitem = functools.partial(configitems._register, ui._knownconfig) + if (b'experimental' not in ui._knownconfig + or not ui._knownconfig[b'experimental'].get(b'thg.displaynames')): + extraitem(b'experimental', b'thg.displaynames', + default=None, + ) + if (b'devel' not in ui._knownconfig + or not ui._knownconfig[b'devel'].get(b'random')): + extraitem(b'devel', b'randomseed', + default=None, + ) + +def _contexttns(self, force=False): + if not force and not self.mutable(): + return b'default' + cache = getattr(self._repo, '_tnscache', None) + # topic loaded, but not enabled (eg: multiple repo in the same process) + if cache is None: + return b'default' + if self.rev() is None: + # don't cache volatile ctx instances that aren't stored on-disk yet + return self.extra().get(b'topic-namespace', b'default') + tns = cache.get(self.rev()) + if tns is None: + tns = self.extra().get(b'topic-namespace', b'default') + self._repo._tnscache[self.rev()] = tns + return tns + +context.basectx.topic_namespace = _contexttns def _contexttopic(self, force=False): if not (force or self.mutable()): @@ -322,6 +346,15 @@ return None context.basectx.topicidx = _contexttopicidx +def _contextfqbn(self): + """return branch//namespace/topic of the changeset, also known as fully + qualified branch name + """ + branch = encoding.tolocal(self.extra()[b'branch']) + return common.formatfqbn(branch, self.topic_namespace(), self.topic()) + +context.basectx.fqbn = _contextfqbn + stackrev = re.compile(br'^s\d+$') topicrev = re.compile(br'^t\d+$') @@ -475,6 +508,7 @@ def _restrictcapabilities(self, caps): caps = super(topicrepo, self)._restrictcapabilities(caps) caps.add(b'topics') + caps.add(b'topics-namespaces') if self.ui.configbool(b'phases', b'publish'): mode = b'all' elif self.ui.configbool(b'experimental', @@ -503,6 +537,24 @@ return super(topicrepo, self).commitctx(ctx, *args, **kwargs) @util.propertycache + def _tnscache(self): + return {} + + @property + def topic_namespaces(self): + if self._topic_namespaces is not None: + return self._topic_namespaces + namespaces = set([self.currenttns]) + for c in self.set(b'not public()'): + namespaces.add(c.topic_namespace()) + self._topic_namespaces = namespaces + return namespaces + + @property + def currenttns(self): + return self.vfs.tryread(b'topic-namespace') or b'default' + + @util.propertycache def _topiccache(self): return {} @@ -524,7 +576,26 @@ # overwritten at the instance level by topicmap.py _autobranchmaptopic = True - def branchmap(self, topic=None): + def branchmap(self, topic=None, convertbm=False): + if topic is None: + topic = getattr(self, '_autobranchmaptopic', False) + topicfilter = topicmap.topicfilter(self.filtername) + if not topic or topicfilter == self.filtername: + return super(topicrepo, self).branchmap() + bm = self.filtered(topicfilter).branchmap() + if convertbm: + entries = compat.bcentries(bm) + for key in list(entries): + branch, tns, topic = common.parsefqbn(key) + if topic: + value = entries.pop(key) + # we lose namespace when converting to ":" format + key = b'%s:%s' % (branch, topic) + entries[key] = value + return bm + + def branchmaptns(self, topic=None): + """branchmap using fqbn as keys""" if topic is None: topic = getattr(self, '_autobranchmaptopic', False) topicfilter = topicmap.topicfilter(self.filtername) @@ -535,8 +606,7 @@ def branchheads(self, branch=None, start=None, closed=False): if branch is None: branch = self[None].branch() - if self.currenttopic: - branch = b"%s:%s" % (branch, self.currenttopic) + branch = common.formatfqbn(branch, self.currenttns, self.currenttopic) return super(topicrepo, self).branchheads(branch=branch, start=start, closed=closed) @@ -548,6 +618,7 @@ def invalidatevolatilesets(self): # XXX we might be able to move this to something invalidated less often super(topicrepo, self).invalidatevolatilesets() + self._topic_namespaces = None self._topics = None def peer(self, *args, **kwargs): @@ -556,7 +627,11 @@ class topicpeer(peer.__class__): def branchmap(self): usetopic = not self._repo.publishing() - return self._repo.branchmap(topic=usetopic) + return self._repo.branchmap(topic=usetopic, convertbm=usetopic) + + def branchmaptns(self): + usetopic = not self._repo.publishing() + return self._repo.branchmaptns(topic=usetopic) peer.__class__ = topicpeer return peer @@ -568,13 +643,9 @@ reporef = weakref.ref(self) if self.ui.configbool(b'experimental', b'enforce-single-head'): - if util.safehasattr(tr, 'validator'): # hg <= 4.7 (ebbba3ba3f66) - origvalidator = tr.validator - elif util.safehasattr(tr, '_validator'): + if util.safehasattr(tr, '_validator'): # hg <= 5.3 (36f08ae87ef6) origvalidator = tr._validator - else: - origvalidator = None def _validate(tr2): repo = reporef() @@ -582,11 +653,9 @@ def validator(tr2): _validate(tr2) - origvalidator(tr2) + return origvalidator(tr2) - if util.safehasattr(tr, 'validator'): # hg <= 4.7 (ebbba3ba3f66) - tr.validator = validator - elif util.safehasattr(tr, '_validator'): + if util.safehasattr(tr, '_validator'): # hg <= 5.3 (36f08ae87ef6) tr._validator = validator else: @@ -598,13 +667,9 @@ b'topic.publish-bare-branch') ispush = desc.startswith((b'push', b'serve')) if (topicmodeserver != b'ignore' and ispush): - if util.safehasattr(tr, 'validator'): # hg <= 4.7 (ebbba3ba3f66) - origvalidator = tr.validator - elif util.safehasattr(tr, '_validator'): + if util.safehasattr(tr, '_validator'): # hg <= 5.3 (36f08ae87ef6) origvalidator = tr._validator - else: - origvalidator = None def _validate(tr2): repo = reporef() @@ -614,9 +679,7 @@ _validate(tr2) return origvalidator(tr2) - if util.safehasattr(tr, 'validator'): # hg <= 4.7 (ebbba3ba3f66) - tr.validator = validator - elif util.safehasattr(tr, '_validator'): + if util.safehasattr(tr, '_validator'): # hg <= 5.3 (36f08ae87ef6) tr._validator = validator else: @@ -636,13 +699,9 @@ b'topic.allow-publish', True) if not allow_publish: - if util.safehasattr(tr, 'validator'): # hg <= 4.7 (ebbba3ba3f66) - origvalidator = tr.validator - elif util.safehasattr(tr, '_validator'): + if util.safehasattr(tr, '_validator'): # hg <= 5.3 (36f08ae87ef6) origvalidator = tr._validator - else: - origvalidator = None def _validate(tr2): repo = reporef() @@ -652,9 +711,7 @@ _validate(tr2) return origvalidator(tr2) - if util.safehasattr(tr, 'validator'): # hg <= 4.7 (ebbba3ba3f66) - tr.validator = validator - elif util.safehasattr(tr, '_validator'): + if util.safehasattr(tr, '_validator'): # hg <= 5.3 (36f08ae87ef6) tr._validator = validator else: @@ -694,6 +751,7 @@ return tr repo.__class__ = topicrepo + repo._topic_namespaces = None repo._topics = None if util.safehasattr(repo, 'names'): repo.names.addnamespace(namespaces.namespace( @@ -714,10 +772,28 @@ ctx = context.resource(mapping, b'ctx') return ctx.topicidx() +@templatekeyword(b'topic_namespace', requires={b'ctx'}) +def topicnamespacekw(context, mapping): + """:topic_namespace: String. The topic namespace of the changeset""" + ctx = context.resource(mapping, b'ctx') + return ctx.topic_namespace() + +@templatekeyword(b'fqbn', requires={b'ctx'}) +def fqbnkw(context, mapping): + """:fqbn: String. The branch//namespace/topic of the changeset""" + ctx = context.resource(mapping, b'ctx') + return ctx.fqbn() + def wrapinit(orig, self, repo, *args, **kwargs): orig(self, repo, *args, **kwargs) if not hastopicext(repo): return + if b'topic-namespace' not in self._extra: + if getattr(repo, 'currenttns', b''): + self._extra[b'topic-namespace'] = repo.currenttns + else: + # Default value will be dropped from extra by another hack at the changegroup level + self._extra[b'topic-namespace'] = b'default' if constants.extrakey not in self._extra: if getattr(repo, 'currenttopic', b''): self._extra[constants.extrakey] = repo.currenttopic @@ -728,6 +804,9 @@ def wrapadd(orig, cl, manifest, files, desc, transaction, p1, p2, user, date=None, extra=None, p1copies=None, p2copies=None, filesadded=None, filesremoved=None): + if b'topic-namespace' in extra and extra[b'topic-namespace'] == b'default': + extra = extra.copy() + del extra[b'topic-namespace'] if constants.extrakey in extra and not extra[constants.extrakey]: extra = extra.copy() del extra[constants.extrakey] @@ -767,7 +846,8 @@ (b'', b'current', None, b'display the current topic only'), ] + commands.formatteropts, _(b'hg topics [OPTION]... [-r REV]... [TOPIC]'), - **compat.helpcategorykwargs('CATEGORY_CHANGE_ORGANIZATION')) + helpcategory=registrar.command.CATEGORY_CHANGE_ORGANIZATION, +) def topics(ui, repo, topic=None, **opts): """View current topic, set current topic, change topic for a set of revisions, or see all topics. @@ -812,13 +892,15 @@ age = opts.get('age') if current and topic: - raise error.Abort(_(b"cannot use --current when setting a topic")) + raise compat.InputError(_(b"cannot use --current when setting a topic")) if current and clear: - raise error.Abort(_(b"cannot use --current and --clear")) + raise compat.InputError(_(b"cannot use --current and --clear")) if clear and topic: - raise error.Abort(_(b"cannot use --clear when setting a topic")) + raise compat.InputError(_(b"cannot use --clear when setting a topic")) if age and topic: - raise error.Abort(_(b"cannot use --age while setting a topic")) + raise compat.InputError(_(b"cannot use --age while setting a topic")) + + compat.check_incompatible_arguments(opts, 'list', ('clear', 'rev')) touchedrevs = set() if rev: @@ -827,20 +909,24 @@ if topic: topic = topic.strip() if not topic: - raise error.Abort(_(b"topic name cannot consist entirely of whitespaces")) + raise compat.InputError(_(b"topic names cannot consist entirely of whitespace")) # Have some restrictions on the topic name just like bookmark name scmutil.checknewlabel(repo, topic, b'topic') - rmatch = re.match(br'[-_.\w]+', topic) - if not rmatch or rmatch.group(0) != topic: - helptxt = _(b"topic names can only consist of alphanumeric, '-'" - b" '_' and '.' characters") - raise error.Abort(_(b"invalid topic name: '%s'") % topic, hint=helptxt) + helptxt = _(b"topic names can only consist of alphanumeric, '-'" + b" '_' and '.' characters") + try: + utopic = encoding.unifromlocal(topic) + except error.Abort: + # Maybe we should allow these topic names as well, as long as they + # don't break any other rules + utopic = '' + rmatch = re.match(r'[-_.\w]+', utopic, re.UNICODE) + if not utopic or not rmatch or rmatch.group(0) != utopic: + raise compat.InputError(_(b"invalid topic name: '%s'") % topic, hint=helptxt) if list: ui.pager(b'topics') - if clear or rev: - raise error.Abort(_(b"cannot use --clear or --rev with --list")) if not topic: topic = repo.currenttopic if not topic: @@ -912,7 +998,8 @@ _(b'display data about children outside of the stack')) ] + commands.formatteropts, _(b'hg stack [TOPIC]'), - **compat.helpcategorykwargs('CATEGORY_CHANGE_NAVIGATION')) + helpcategory=registrar.command.CATEGORY_CHANGE_NAVIGATION, +) def cmdstack(ui, repo, topic=b'', **opts): """list all changesets in a topic and other information @@ -1058,8 +1145,7 @@ if newtopic: with repo.wlock(): - with repo.vfs.open(b'topic', b'w') as f: - f.write(newtopic) + repo.vfs.write(b'topic', newtopic) else: if repo.vfs.exists(b'topic'): repo.vfs.unlink(b'topic') @@ -1321,8 +1407,7 @@ hint = _(b"see 'hg help -e topic.topic-mode' for details") if opts.get('topic'): t = opts['topic'] - with repo.vfs.open(b'topic', b'w') as f: - f.write(t) + repo.vfs.write(b'topic', t) elif opts.get('amend'): pass elif notopic and mayabort: @@ -1333,8 +1418,7 @@ if not ui.quiet: ui.warn((b"(%s)\n") % hint) elif notopic and mayrandom: - with repo.vfs.open(b'topic', b'w') as f: - f.write(randomname.randomtopicname(ui)) + repo.vfs.write(b'topic', randomname.randomtopicname(ui)) return orig(ui, repo, *args, **opts) def committextwrap(orig, repo, ctx, subs, extramsg): @@ -1377,9 +1461,11 @@ ret = super(overridebranch, self).__getitem__(rev) if rev == node: b = ret.branch() + tns = ret.topic_namespace() t = ret.topic() + # topic is required for merging from bare branch if t: - ret.branch = lambda: b'%s//%s' % (b, t) + ret.branch = lambda: common.formatfqbn(b, tns, t) return ret unfi.__class__ = overridebranch if repo.filtername is not None: @@ -1401,18 +1487,23 @@ # if rebase is running and update the currenttopic to topic of new # rebased commit. We have explicitly stored in config if rebase is # running. + otns = repo.currenttns ot = repo.currenttopic if repo.ui.hasconfig(b'experimental', b'topicrebase'): isrebase = True if repo.ui.configbool(b'_internal', b'keep-topic'): ist0 = True if ((not partial and not branchmerge) or isrebase) and not ist0: + tns = b'default' t = b'' pctx = repo[node] if pctx.phase() > phases.public: + tns = pctx.topic_namespace() t = pctx.topic() - with repo.vfs.open(b'topic', b'w') as f: - f.write(t) + repo.vfs.write(b'topic-namespace', tns) + if tns != b'default' and tns != otns: + repo.ui.status(_(b"switching to topic-namespace %s\n") % tns) + repo.vfs.write(b'topic', t) if t and t != ot: repo.ui.status(_(b"switching to topic %s\n") % t) if ot and not t: @@ -1501,3 +1592,65 @@ # Restore the topic if need if topic: _changecurrenttopic(repo, topic) + +def _changecurrenttns(repo, tns): + if tns: + with repo.wlock(): + repo.vfs.write(b'topic-namespace', tns) + else: + repo.vfs.unlinkpath(b'topic-namespace', ignoremissing=True) + +@command(b'debug-topic-namespace', [ + (b'', b'clear', False, b'clear active topic namespace if any'), + ], + _(b'[NAMESPACE|--clear]')) +def debugtopicnamespace(ui, repo, tns=None, **opts): + """set or show the current topic namespace""" + if opts.get('clear'): + if tns: + raise error.Abort(_(b"cannot use --clear when setting a topic namespace")) + tns = None + elif not tns: + ui.write(b'%s\n' % repo.currenttns) + return + if tns: + tns = tns.strip() + if not tns: + raise error.Abort(_(b"topic namespace cannot consist entirely of whitespace")) + if b'/' in tns: + raise error.Abort(_(b"topic namespace cannot contain '/' character")) + scmutil.checknewlabel(repo, tns, b'topic namespace') + ctns = repo.currenttns + _changecurrenttns(repo, tns) + if ctns == b'default' and tns: + repo.ui.status(_(b'marked working directory as topic namespace: %s\n') + % tns) + +@command(b'debug-topic-namespaces', []) +def debugtopicnamespaces(ui, repo, **opts): + """list repository namespaces""" + for tns in repo.topic_namespaces: + ui.write(b'%s\n' % (tns,)) + +@command(b'debug-parse-fqbn', commands.formatteropts, _(b'FQBN'), optionalrepo=True) +def debugparsefqbn(ui, repo, fqbn, **opts): + """parse branch//namespace/topic string into its components""" + branch, tns, topic = common.parsefqbn(fqbn) + opts = pycompat.byteskwargs(opts) + fm = ui.formatter(b'debug-parse-namespace', opts) + fm.startitem() + fm.write(b'branch', b'branch: %s\n', branch) + fm.write(b'topic_namespace', b'namespace: %s\n', tns) + fm.write(b'topic', b'topic: %s\n', topic) + fm.end() + +@command(b'debug-format-fqbn', [ + (b'b', b'branch', b'', b'branch'), + (b'n', b'topic-namespace', b'', b'topic namespace'), + (b't', b'topic', b'', b'topic'), + (b's', b'short', False, b'short format'), + ], optionalrepo=True) +def debugformatfqbn(ui, repo, **opts): + """format branch, namespace and topic into branch//namespace/topic string""" + short = common.formatfqbn(opts.get('branch'), opts.get('topic_namespace'), opts.get('topic'), opts.get('short')) + ui.write(b'%s\n' % short)