comparison 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
comparison
equal deleted inserted replaced
6359:cb9e77506cbc 6360:e959390490c2
166 from mercurial import ( 166 from mercurial import (
167 bookmarks, 167 bookmarks,
168 changelog, 168 changelog,
169 cmdutil, 169 cmdutil,
170 commands, 170 commands,
171 configitems,
171 context, 172 context,
173 encoding,
172 error, 174 error,
173 exchange, 175 exchange,
174 extensions, 176 extensions,
175 hg, 177 hg,
176 localrepo, 178 localrepo,
229 # default color to help log output and thg 231 # default color to help log output and thg
230 # (first pick I could think off, update as needed 232 # (first pick I could think off, update as needed
231 b'log.topic': b'green_background', 233 b'log.topic': b'green_background',
232 } 234 }
233 235
234 __version__ = b'0.24.3.dev' 236 __version__ = b'0.25.0.dev'
235 237
236 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' 238 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'
237 minimumhgversion = b'4.8' 239 minimumhgversion = b'4.8'
238 buglink = b'https://bz.mercurial-scm.org/' 240 buglink = b'https://bz.mercurial-scm.org/'
239 241
240 if util.safehasattr(registrar, 'configitem'): 242 configtable = {}
241 243 configitem = registrar.configitem(configtable)
242 from mercurial import configitems 244
243 245 configitem(b'experimental', b'enforce-topic',
244 configtable = {} 246 default=False,
245 configitem = registrar.configitem(configtable) 247 )
246 248 configitem(b'experimental', b'enforce-single-head',
247 configitem(b'experimental', b'enforce-topic', 249 default=False,
248 default=False, 250 )
249 ) 251 configitem(b'experimental', b'topic-mode',
250 configitem(b'experimental', b'enforce-single-head', 252 default=None,
251 default=False, 253 )
252 ) 254 configitem(b'experimental', b'topic.publish-bare-branch',
253 configitem(b'experimental', b'topic-mode', 255 default=False,
254 default=None, 256 )
255 ) 257 configitem(b'experimental', b'topic.allow-publish',
256 configitem(b'experimental', b'topic.publish-bare-branch', 258 default=configitems.dynamicdefault,
257 default=False, 259 )
258 ) 260 configitem(b'_internal', b'keep-topic',
259 configitem(b'experimental', b'topic.allow-publish', 261 default=False,
260 default=configitems.dynamicdefault, 262 )
261 ) 263 # used for signaling that ctx.branch() shouldn't return fqbn even if topic is
262 configitem(b'_internal', b'keep-topic', 264 # enabled for local repo
263 default=False, 265 configitem(b'_internal', b'tns-disable-fqbn',
264 ) 266 default=False,
265 configitem(b'experimental', b'topic-mode.server', 267 )
266 default=configitems.dynamicdefault, 268 # used for signaling that push will publish changesets
267 ) 269 configitem(b'_internal', b'tns-publish',
268 configitem(b'experimental', b'topic.server-gate-topic-changesets', 270 default=False,
269 default=False, 271 )
270 ) 272 configitem(b'experimental', b'topic-mode.server',
271 configitem(b'experimental', b'topic.linear-merge', 273 default=configitems.dynamicdefault,
272 default="reject", 274 )
273 ) 275 configitem(b'experimental', b'topic.server-gate-topic-changesets',
274 276 default=False,
275 def extsetup(ui): 277 )
276 # register config that strictly belong to other code (thg, core, etc) 278 configitem(b'experimental', b'topic.linear-merge',
277 # 279 default="reject",
278 # To ensure all config items we used are registered, we register them if 280 )
279 # nobody else did so far. 281
280 from mercurial import configitems 282 def extsetup(ui):
281 extraitem = functools.partial(configitems._register, ui._knownconfig) 283 # register config that strictly belong to other code (thg, core, etc)
282 if (b'experimental' not in ui._knownconfig 284 #
283 or not ui._knownconfig[b'experimental'].get(b'thg.displaynames')): 285 # To ensure all config items we used are registered, we register them if
284 extraitem(b'experimental', b'thg.displaynames', 286 # nobody else did so far.
285 default=None, 287 extraitem = functools.partial(configitems._register, ui._knownconfig)
286 ) 288 if (b'experimental' not in ui._knownconfig
287 if (b'devel' not in ui._knownconfig 289 or not ui._knownconfig[b'experimental'].get(b'thg.displaynames')):
288 or not ui._knownconfig[b'devel'].get(b'random')): 290 extraitem(b'experimental', b'thg.displaynames',
289 extraitem(b'devel', b'randomseed', 291 default=None,
290 default=None, 292 )
291 ) 293 if (b'devel' not in ui._knownconfig
294 or not ui._knownconfig[b'devel'].get(b'random')):
295 extraitem(b'devel', b'randomseed',
296 default=None,
297 )
298
299 def _contexttns(self, force=False):
300 if not force and not self.mutable():
301 return b'default'
302 cache = getattr(self._repo, '_tnscache', None)
303 # topic loaded, but not enabled (eg: multiple repo in the same process)
304 if cache is None:
305 return b'default'
306 if self.rev() is None:
307 # don't cache volatile ctx instances that aren't stored on-disk yet
308 return self.extra().get(b'topic-namespace', b'default')
309 tns = cache.get(self.rev())
310 if tns is None:
311 tns = self.extra().get(b'topic-namespace', b'default')
312 self._repo._tnscache[self.rev()] = tns
313 return tns
314
315 context.basectx.topic_namespace = _contexttns
292 316
293 def _contexttopic(self, force=False): 317 def _contexttopic(self, force=False):
294 if not (force or self.mutable()): 318 if not (force or self.mutable()):
295 return b'' 319 return b''
296 cache = getattr(self._repo, '_topiccache', None) 320 cache = getattr(self._repo, '_topiccache', None)
319 return revlist.index(self.rev()) 343 return revlist.index(self.rev())
320 except IndexError: 344 except IndexError:
321 # Lets move to the last ctx of the current topic 345 # Lets move to the last ctx of the current topic
322 return None 346 return None
323 context.basectx.topicidx = _contexttopicidx 347 context.basectx.topicidx = _contexttopicidx
348
349 def _contextfqbn(self):
350 """return branch//namespace/topic of the changeset, also known as fully
351 qualified branch name
352 """
353 branch = encoding.tolocal(self.extra()[b'branch'])
354 return common.formatfqbn(branch, self.topic_namespace(), self.topic())
355
356 context.basectx.fqbn = _contextfqbn
324 357
325 stackrev = re.compile(br'^s\d+$') 358 stackrev = re.compile(br'^s\d+$')
326 topicrev = re.compile(br'^t\d+$') 359 topicrev = re.compile(br'^t\d+$')
327 360
328 hastopicext = common.hastopicext 361 hastopicext = common.hastopicext
473 hastopicext = True 506 hastopicext = True
474 507
475 def _restrictcapabilities(self, caps): 508 def _restrictcapabilities(self, caps):
476 caps = super(topicrepo, self)._restrictcapabilities(caps) 509 caps = super(topicrepo, self)._restrictcapabilities(caps)
477 caps.add(b'topics') 510 caps.add(b'topics')
511 caps.add(b'topics-namespaces')
478 if self.ui.configbool(b'phases', b'publish'): 512 if self.ui.configbool(b'phases', b'publish'):
479 mode = b'all' 513 mode = b'all'
480 elif self.ui.configbool(b'experimental', 514 elif self.ui.configbool(b'experimental',
481 b'topic.publish-bare-branch'): 515 b'topic.publish-bare-branch'):
482 mode = b'auto' 516 mode = b'auto'
499 if isinstance(ctx, context.workingcommitctx): 533 if isinstance(ctx, context.workingcommitctx):
500 current = self.currenttopic 534 current = self.currenttopic
501 if current: 535 if current:
502 ctx.extra()[constants.extrakey] = current 536 ctx.extra()[constants.extrakey] = current
503 return super(topicrepo, self).commitctx(ctx, *args, **kwargs) 537 return super(topicrepo, self).commitctx(ctx, *args, **kwargs)
538
539 @util.propertycache
540 def _tnscache(self):
541 return {}
542
543 @property
544 def topic_namespaces(self):
545 if self._topic_namespaces is not None:
546 return self._topic_namespaces
547 namespaces = set([self.currenttns])
548 for c in self.set(b'not public()'):
549 namespaces.add(c.topic_namespace())
550 self._topic_namespaces = namespaces
551 return namespaces
552
553 @property
554 def currenttns(self):
555 return self.vfs.tryread(b'topic-namespace') or b'default'
504 556
505 @util.propertycache 557 @util.propertycache
506 def _topiccache(self): 558 def _topiccache(self):
507 return {} 559 return {}
508 560
522 return self.vfs.tryread(b'topic') 574 return self.vfs.tryread(b'topic')
523 575
524 # overwritten at the instance level by topicmap.py 576 # overwritten at the instance level by topicmap.py
525 _autobranchmaptopic = True 577 _autobranchmaptopic = True
526 578
527 def branchmap(self, topic=None): 579 def branchmap(self, topic=None, convertbm=False):
580 if topic is None:
581 topic = getattr(self, '_autobranchmaptopic', False)
582 topicfilter = topicmap.topicfilter(self.filtername)
583 if not topic or topicfilter == self.filtername:
584 return super(topicrepo, self).branchmap()
585 bm = self.filtered(topicfilter).branchmap()
586 if convertbm:
587 entries = compat.bcentries(bm)
588 for key in list(entries):
589 branch, tns, topic = common.parsefqbn(key)
590 if topic:
591 value = entries.pop(key)
592 # we lose namespace when converting to ":" format
593 key = b'%s:%s' % (branch, topic)
594 entries[key] = value
595 return bm
596
597 def branchmaptns(self, topic=None):
598 """branchmap using fqbn as keys"""
528 if topic is None: 599 if topic is None:
529 topic = getattr(self, '_autobranchmaptopic', False) 600 topic = getattr(self, '_autobranchmaptopic', False)
530 topicfilter = topicmap.topicfilter(self.filtername) 601 topicfilter = topicmap.topicfilter(self.filtername)
531 if not topic or topicfilter == self.filtername: 602 if not topic or topicfilter == self.filtername:
532 return super(topicrepo, self).branchmap() 603 return super(topicrepo, self).branchmap()
533 return self.filtered(topicfilter).branchmap() 604 return self.filtered(topicfilter).branchmap()
534 605
535 def branchheads(self, branch=None, start=None, closed=False): 606 def branchheads(self, branch=None, start=None, closed=False):
536 if branch is None: 607 if branch is None:
537 branch = self[None].branch() 608 branch = self[None].branch()
538 if self.currenttopic: 609 branch = common.formatfqbn(branch, self.currenttns, self.currenttopic)
539 branch = b"%s:%s" % (branch, self.currenttopic)
540 return super(topicrepo, self).branchheads(branch=branch, 610 return super(topicrepo, self).branchheads(branch=branch,
541 start=start, 611 start=start,
542 closed=closed) 612 closed=closed)
543 613
544 def invalidatecaches(self): 614 def invalidatecaches(self):
546 super(topicrepo, self).invalidatecaches() 616 super(topicrepo, self).invalidatecaches()
547 617
548 def invalidatevolatilesets(self): 618 def invalidatevolatilesets(self):
549 # XXX we might be able to move this to something invalidated less often 619 # XXX we might be able to move this to something invalidated less often
550 super(topicrepo, self).invalidatevolatilesets() 620 super(topicrepo, self).invalidatevolatilesets()
621 self._topic_namespaces = None
551 self._topics = None 622 self._topics = None
552 623
553 def peer(self, *args, **kwargs): 624 def peer(self, *args, **kwargs):
554 peer = super(topicrepo, self).peer(*args, **kwargs) 625 peer = super(topicrepo, self).peer(*args, **kwargs)
555 if getattr(peer, '_repo', None) is not None: # localpeer 626 if getattr(peer, '_repo', None) is not None: # localpeer
556 class topicpeer(peer.__class__): 627 class topicpeer(peer.__class__):
557 def branchmap(self): 628 def branchmap(self):
558 usetopic = not self._repo.publishing() 629 usetopic = not self._repo.publishing()
559 return self._repo.branchmap(topic=usetopic) 630 return self._repo.branchmap(topic=usetopic, convertbm=usetopic)
631
632 def branchmaptns(self):
633 usetopic = not self._repo.publishing()
634 return self._repo.branchmaptns(topic=usetopic)
560 peer.__class__ = topicpeer 635 peer.__class__ = topicpeer
561 return peer 636 return peer
562 637
563 def transaction(self, desc, *a, **k): 638 def transaction(self, desc, *a, **k):
564 ctr = self.currenttransaction() 639 ctr = self.currenttransaction()
566 if desc in (b'strip', b'repair') or ctr is not None: 641 if desc in (b'strip', b'repair') or ctr is not None:
567 return tr 642 return tr
568 643
569 reporef = weakref.ref(self) 644 reporef = weakref.ref(self)
570 if self.ui.configbool(b'experimental', b'enforce-single-head'): 645 if self.ui.configbool(b'experimental', b'enforce-single-head'):
571 if util.safehasattr(tr, 'validator'): # hg <= 4.7 (ebbba3ba3f66) 646 if util.safehasattr(tr, '_validator'):
572 origvalidator = tr.validator
573 elif util.safehasattr(tr, '_validator'):
574 # hg <= 5.3 (36f08ae87ef6) 647 # hg <= 5.3 (36f08ae87ef6)
575 origvalidator = tr._validator 648 origvalidator = tr._validator
576 else:
577 origvalidator = None
578 649
579 def _validate(tr2): 650 def _validate(tr2):
580 repo = reporef() 651 repo = reporef()
581 flow.enforcesinglehead(repo, tr2) 652 flow.enforcesinglehead(repo, tr2)
582 653
583 def validator(tr2): 654 def validator(tr2):
584 _validate(tr2) 655 _validate(tr2)
585 origvalidator(tr2) 656 return origvalidator(tr2)
586 657
587 if util.safehasattr(tr, 'validator'): # hg <= 4.7 (ebbba3ba3f66) 658 if util.safehasattr(tr, '_validator'):
588 tr.validator = validator
589 elif util.safehasattr(tr, '_validator'):
590 # hg <= 5.3 (36f08ae87ef6) 659 # hg <= 5.3 (36f08ae87ef6)
591 tr._validator = validator 660 tr._validator = validator
592 else: 661 else:
593 tr.addvalidator(b'000-enforce-single-head', _validate) 662 tr.addvalidator(b'000-enforce-single-head', _validate)
594 663
596 b'topic-mode.server', b'ignore') 665 b'topic-mode.server', b'ignore')
597 publishbare = self.ui.configbool(b'experimental', 666 publishbare = self.ui.configbool(b'experimental',
598 b'topic.publish-bare-branch') 667 b'topic.publish-bare-branch')
599 ispush = desc.startswith((b'push', b'serve')) 668 ispush = desc.startswith((b'push', b'serve'))
600 if (topicmodeserver != b'ignore' and ispush): 669 if (topicmodeserver != b'ignore' and ispush):
601 if util.safehasattr(tr, 'validator'): # hg <= 4.7 (ebbba3ba3f66) 670 if util.safehasattr(tr, '_validator'):
602 origvalidator = tr.validator
603 elif util.safehasattr(tr, '_validator'):
604 # hg <= 5.3 (36f08ae87ef6) 671 # hg <= 5.3 (36f08ae87ef6)
605 origvalidator = tr._validator 672 origvalidator = tr._validator
606 else:
607 origvalidator = None
608 673
609 def _validate(tr2): 674 def _validate(tr2):
610 repo = reporef() 675 repo = reporef()
611 flow.rejectuntopicedchangeset(repo, tr2) 676 flow.rejectuntopicedchangeset(repo, tr2)
612 677
613 def validator(tr2): 678 def validator(tr2):
614 _validate(tr2) 679 _validate(tr2)
615 return origvalidator(tr2) 680 return origvalidator(tr2)
616 681
617 if util.safehasattr(tr, 'validator'): # hg <= 4.7 (ebbba3ba3f66) 682 if util.safehasattr(tr, '_validator'):
618 tr.validator = validator
619 elif util.safehasattr(tr, '_validator'):
620 # hg <= 5.3 (36f08ae87ef6) 683 # hg <= 5.3 (36f08ae87ef6)
621 tr._validator = validator 684 tr._validator = validator
622 else: 685 else:
623 tr.addvalidator(b'000-reject-untopiced', _validate) 686 tr.addvalidator(b'000-reject-untopiced', _validate)
624 687
634 tr.close = close 697 tr.close = close
635 allow_publish = self.ui.configbool(b'experimental', 698 allow_publish = self.ui.configbool(b'experimental',
636 b'topic.allow-publish', 699 b'topic.allow-publish',
637 True) 700 True)
638 if not allow_publish: 701 if not allow_publish:
639 if util.safehasattr(tr, 'validator'): # hg <= 4.7 (ebbba3ba3f66) 702 if util.safehasattr(tr, '_validator'):
640 origvalidator = tr.validator
641 elif util.safehasattr(tr, '_validator'):
642 # hg <= 5.3 (36f08ae87ef6) 703 # hg <= 5.3 (36f08ae87ef6)
643 origvalidator = tr._validator 704 origvalidator = tr._validator
644 else:
645 origvalidator = None
646 705
647 def _validate(tr2): 706 def _validate(tr2):
648 repo = reporef() 707 repo = reporef()
649 flow.reject_publish(repo, tr2) 708 flow.reject_publish(repo, tr2)
650 709
651 def validator(tr2): 710 def validator(tr2):
652 _validate(tr2) 711 _validate(tr2)
653 return origvalidator(tr2) 712 return origvalidator(tr2)
654 713
655 if util.safehasattr(tr, 'validator'): # hg <= 4.7 (ebbba3ba3f66) 714 if util.safehasattr(tr, '_validator'):
656 tr.validator = validator
657 elif util.safehasattr(tr, '_validator'):
658 # hg <= 5.3 (36f08ae87ef6) 715 # hg <= 5.3 (36f08ae87ef6)
659 tr._validator = validator 716 tr._validator = validator
660 else: 717 else:
661 tr.addvalidator(b'000-reject-publish', _validate) 718 tr.addvalidator(b'000-reject-publish', _validate)
662 719
692 749
693 tr.addpostclose(b'signalcurrenttopicempty', currenttopicempty) 750 tr.addpostclose(b'signalcurrenttopicempty', currenttopicempty)
694 return tr 751 return tr
695 752
696 repo.__class__ = topicrepo 753 repo.__class__ = topicrepo
754 repo._topic_namespaces = None
697 repo._topics = None 755 repo._topics = None
698 if util.safehasattr(repo, 'names'): 756 if util.safehasattr(repo, 'names'):
699 repo.names.addnamespace(namespaces.namespace( 757 repo.names.addnamespace(namespaces.namespace(
700 b'topics', b'topic', namemap=_namemap, nodemap=_nodemap, 758 b'topics', b'topic', namemap=_namemap, nodemap=_nodemap,
701 listnames=lambda repo: repo.topics)) 759 listnames=lambda repo: repo.topics))
712 def topicidxkw(context, mapping): 770 def topicidxkw(context, mapping):
713 """:topicidx: Integer. Index of the changeset as a stack alias""" 771 """:topicidx: Integer. Index of the changeset as a stack alias"""
714 ctx = context.resource(mapping, b'ctx') 772 ctx = context.resource(mapping, b'ctx')
715 return ctx.topicidx() 773 return ctx.topicidx()
716 774
775 @templatekeyword(b'topic_namespace', requires={b'ctx'})
776 def topicnamespacekw(context, mapping):
777 """:topic_namespace: String. The topic namespace of the changeset"""
778 ctx = context.resource(mapping, b'ctx')
779 return ctx.topic_namespace()
780
781 @templatekeyword(b'fqbn', requires={b'ctx'})
782 def fqbnkw(context, mapping):
783 """:fqbn: String. The branch//namespace/topic of the changeset"""
784 ctx = context.resource(mapping, b'ctx')
785 return ctx.fqbn()
786
717 def wrapinit(orig, self, repo, *args, **kwargs): 787 def wrapinit(orig, self, repo, *args, **kwargs):
718 orig(self, repo, *args, **kwargs) 788 orig(self, repo, *args, **kwargs)
719 if not hastopicext(repo): 789 if not hastopicext(repo):
720 return 790 return
791 if b'topic-namespace' not in self._extra:
792 if getattr(repo, 'currenttns', b''):
793 self._extra[b'topic-namespace'] = repo.currenttns
794 else:
795 # Default value will be dropped from extra by another hack at the changegroup level
796 self._extra[b'topic-namespace'] = b'default'
721 if constants.extrakey not in self._extra: 797 if constants.extrakey not in self._extra:
722 if getattr(repo, 'currenttopic', b''): 798 if getattr(repo, 'currenttopic', b''):
723 self._extra[constants.extrakey] = repo.currenttopic 799 self._extra[constants.extrakey] = repo.currenttopic
724 else: 800 else:
725 # Empty key will be dropped from extra by another hack at the changegroup level 801 # Empty key will be dropped from extra by another hack at the changegroup level
726 self._extra[constants.extrakey] = b'' 802 self._extra[constants.extrakey] = b''
727 803
728 def wrapadd(orig, cl, manifest, files, desc, transaction, p1, p2, user, 804 def wrapadd(orig, cl, manifest, files, desc, transaction, p1, p2, user,
729 date=None, extra=None, p1copies=None, p2copies=None, 805 date=None, extra=None, p1copies=None, p2copies=None,
730 filesadded=None, filesremoved=None): 806 filesadded=None, filesremoved=None):
807 if b'topic-namespace' in extra and extra[b'topic-namespace'] == b'default':
808 extra = extra.copy()
809 del extra[b'topic-namespace']
731 if constants.extrakey in extra and not extra[constants.extrakey]: 810 if constants.extrakey in extra and not extra[constants.extrakey]:
732 extra = extra.copy() 811 extra = extra.copy()
733 del extra[constants.extrakey] 812 del extra[constants.extrakey]
734 # hg <= 4.9 (0e41f40b01cc) 813 # hg <= 4.9 (0e41f40b01cc)
735 kwargs = {} 814 kwargs = {}
765 (b'l', b'list', False, b'show the stack of changeset in the topic'), 844 (b'l', b'list', False, b'show the stack of changeset in the topic'),
766 (b'', b'age', False, b'show when you last touched the topics'), 845 (b'', b'age', False, b'show when you last touched the topics'),
767 (b'', b'current', None, b'display the current topic only'), 846 (b'', b'current', None, b'display the current topic only'),
768 ] + commands.formatteropts, 847 ] + commands.formatteropts,
769 _(b'hg topics [OPTION]... [-r REV]... [TOPIC]'), 848 _(b'hg topics [OPTION]... [-r REV]... [TOPIC]'),
770 **compat.helpcategorykwargs('CATEGORY_CHANGE_ORGANIZATION')) 849 helpcategory=registrar.command.CATEGORY_CHANGE_ORGANIZATION,
850 )
771 def topics(ui, repo, topic=None, **opts): 851 def topics(ui, repo, topic=None, **opts):
772 """View current topic, set current topic, change topic for a set of revisions, or see all topics. 852 """View current topic, set current topic, change topic for a set of revisions, or see all topics.
773 853
774 Clear topic on existing topiced revisions:: 854 Clear topic on existing topiced revisions::
775 855
810 rev = opts.get('rev') 890 rev = opts.get('rev')
811 current = opts.get('current') 891 current = opts.get('current')
812 age = opts.get('age') 892 age = opts.get('age')
813 893
814 if current and topic: 894 if current and topic:
815 raise error.Abort(_(b"cannot use --current when setting a topic")) 895 raise compat.InputError(_(b"cannot use --current when setting a topic"))
816 if current and clear: 896 if current and clear:
817 raise error.Abort(_(b"cannot use --current and --clear")) 897 raise compat.InputError(_(b"cannot use --current and --clear"))
818 if clear and topic: 898 if clear and topic:
819 raise error.Abort(_(b"cannot use --clear when setting a topic")) 899 raise compat.InputError(_(b"cannot use --clear when setting a topic"))
820 if age and topic: 900 if age and topic:
821 raise error.Abort(_(b"cannot use --age while setting a topic")) 901 raise compat.InputError(_(b"cannot use --age while setting a topic"))
902
903 compat.check_incompatible_arguments(opts, 'list', ('clear', 'rev'))
822 904
823 touchedrevs = set() 905 touchedrevs = set()
824 if rev: 906 if rev:
825 touchedrevs = scmutil.revrange(repo, rev) 907 touchedrevs = scmutil.revrange(repo, rev)
826 908
827 if topic: 909 if topic:
828 topic = topic.strip() 910 topic = topic.strip()
829 if not topic: 911 if not topic:
830 raise error.Abort(_(b"topic name cannot consist entirely of whitespaces")) 912 raise compat.InputError(_(b"topic names cannot consist entirely of whitespace"))
831 # Have some restrictions on the topic name just like bookmark name 913 # Have some restrictions on the topic name just like bookmark name
832 scmutil.checknewlabel(repo, topic, b'topic') 914 scmutil.checknewlabel(repo, topic, b'topic')
833 915
834 rmatch = re.match(br'[-_.\w]+', topic) 916 helptxt = _(b"topic names can only consist of alphanumeric, '-'"
835 if not rmatch or rmatch.group(0) != topic: 917 b" '_' and '.' characters")
836 helptxt = _(b"topic names can only consist of alphanumeric, '-'" 918 try:
837 b" '_' and '.' characters") 919 utopic = encoding.unifromlocal(topic)
838 raise error.Abort(_(b"invalid topic name: '%s'") % topic, hint=helptxt) 920 except error.Abort:
921 # Maybe we should allow these topic names as well, as long as they
922 # don't break any other rules
923 utopic = ''
924 rmatch = re.match(r'[-_.\w]+', utopic, re.UNICODE)
925 if not utopic or not rmatch or rmatch.group(0) != utopic:
926 raise compat.InputError(_(b"invalid topic name: '%s'") % topic, hint=helptxt)
839 927
840 if list: 928 if list:
841 ui.pager(b'topics') 929 ui.pager(b'topics')
842 if clear or rev:
843 raise error.Abort(_(b"cannot use --clear or --rev with --list"))
844 if not topic: 930 if not topic:
845 topic = repo.currenttopic 931 topic = repo.currenttopic
846 if not topic: 932 if not topic:
847 raise error.Abort(_(b'no active topic to list')) 933 raise error.Abort(_(b'no active topic to list'))
848 return stack.showstack(ui, repo, topic=topic, 934 return stack.showstack(ui, repo, topic=topic,
910 @command(b'stack', [ 996 @command(b'stack', [
911 (b'c', b'children', None, 997 (b'c', b'children', None,
912 _(b'display data about children outside of the stack')) 998 _(b'display data about children outside of the stack'))
913 ] + commands.formatteropts, 999 ] + commands.formatteropts,
914 _(b'hg stack [TOPIC]'), 1000 _(b'hg stack [TOPIC]'),
915 **compat.helpcategorykwargs('CATEGORY_CHANGE_NAVIGATION')) 1001 helpcategory=registrar.command.CATEGORY_CHANGE_NAVIGATION,
1002 )
916 def cmdstack(ui, repo, topic=b'', **opts): 1003 def cmdstack(ui, repo, topic=b'', **opts):
917 """list all changesets in a topic and other information 1004 """list all changesets in a topic and other information
918 1005
919 List the current topic by default. 1006 List the current topic by default.
920 1007
1056 def _changecurrenttopic(repo, newtopic): 1143 def _changecurrenttopic(repo, newtopic):
1057 """changes the current topic.""" 1144 """changes the current topic."""
1058 1145
1059 if newtopic: 1146 if newtopic:
1060 with repo.wlock(): 1147 with repo.wlock():
1061 with repo.vfs.open(b'topic', b'w') as f: 1148 repo.vfs.write(b'topic', newtopic)
1062 f.write(newtopic)
1063 else: 1149 else:
1064 if repo.vfs.exists(b'topic'): 1150 if repo.vfs.exists(b'topic'):
1065 repo.vfs.unlink(b'topic') 1151 repo.vfs.unlink(b'topic')
1066 1152
1067 def _changetopics(ui, repo, revs, newtopic): 1153 def _changetopics(ui, repo, revs, newtopic):
1319 maywarn = False 1405 maywarn = False
1320 1406
1321 hint = _(b"see 'hg help -e topic.topic-mode' for details") 1407 hint = _(b"see 'hg help -e topic.topic-mode' for details")
1322 if opts.get('topic'): 1408 if opts.get('topic'):
1323 t = opts['topic'] 1409 t = opts['topic']
1324 with repo.vfs.open(b'topic', b'w') as f: 1410 repo.vfs.write(b'topic', t)
1325 f.write(t)
1326 elif opts.get('amend'): 1411 elif opts.get('amend'):
1327 pass 1412 pass
1328 elif notopic and mayabort: 1413 elif notopic and mayabort:
1329 msg = _(b"no active topic") 1414 msg = _(b"no active topic")
1330 raise error.Abort(msg, hint=hint) 1415 raise error.Abort(msg, hint=hint)
1331 elif notopic and maywarn: 1416 elif notopic and maywarn:
1332 ui.warn(_(b"warning: new draft commit without topic\n")) 1417 ui.warn(_(b"warning: new draft commit without topic\n"))
1333 if not ui.quiet: 1418 if not ui.quiet:
1334 ui.warn((b"(%s)\n") % hint) 1419 ui.warn((b"(%s)\n") % hint)
1335 elif notopic and mayrandom: 1420 elif notopic and mayrandom:
1336 with repo.vfs.open(b'topic', b'w') as f: 1421 repo.vfs.write(b'topic', randomname.randomtopicname(ui))
1337 f.write(randomname.randomtopicname(ui))
1338 return orig(ui, repo, *args, **opts) 1422 return orig(ui, repo, *args, **opts)
1339 1423
1340 def committextwrap(orig, repo, ctx, subs, extramsg): 1424 def committextwrap(orig, repo, ctx, subs, extramsg):
1341 ret = orig(repo, ctx, subs, extramsg) 1425 ret = orig(repo, ctx, subs, extramsg)
1342 if hastopicext(repo): 1426 if hastopicext(repo):
1375 class overridebranch(old): 1459 class overridebranch(old):
1376 def __getitem__(self, rev): 1460 def __getitem__(self, rev):
1377 ret = super(overridebranch, self).__getitem__(rev) 1461 ret = super(overridebranch, self).__getitem__(rev)
1378 if rev == node: 1462 if rev == node:
1379 b = ret.branch() 1463 b = ret.branch()
1464 tns = ret.topic_namespace()
1380 t = ret.topic() 1465 t = ret.topic()
1466 # topic is required for merging from bare branch
1381 if t: 1467 if t:
1382 ret.branch = lambda: b'%s//%s' % (b, t) 1468 ret.branch = lambda: common.formatfqbn(b, tns, t)
1383 return ret 1469 return ret
1384 unfi.__class__ = overridebranch 1470 unfi.__class__ = overridebranch
1385 if repo.filtername is not None: 1471 if repo.filtername is not None:
1386 repo = unfi.filtered(repo.filtername) 1472 repo = unfi.filtered(repo.filtername)
1387 1473
1399 # The mergeupdatewrap function makes the destination's topic as the 1485 # The mergeupdatewrap function makes the destination's topic as the
1400 # current topic. This is right for merge but wrong for rebase. We check 1486 # current topic. This is right for merge but wrong for rebase. We check
1401 # if rebase is running and update the currenttopic to topic of new 1487 # if rebase is running and update the currenttopic to topic of new
1402 # rebased commit. We have explicitly stored in config if rebase is 1488 # rebased commit. We have explicitly stored in config if rebase is
1403 # running. 1489 # running.
1490 otns = repo.currenttns
1404 ot = repo.currenttopic 1491 ot = repo.currenttopic
1405 if repo.ui.hasconfig(b'experimental', b'topicrebase'): 1492 if repo.ui.hasconfig(b'experimental', b'topicrebase'):
1406 isrebase = True 1493 isrebase = True
1407 if repo.ui.configbool(b'_internal', b'keep-topic'): 1494 if repo.ui.configbool(b'_internal', b'keep-topic'):
1408 ist0 = True 1495 ist0 = True
1409 if ((not partial and not branchmerge) or isrebase) and not ist0: 1496 if ((not partial and not branchmerge) or isrebase) and not ist0:
1497 tns = b'default'
1410 t = b'' 1498 t = b''
1411 pctx = repo[node] 1499 pctx = repo[node]
1412 if pctx.phase() > phases.public: 1500 if pctx.phase() > phases.public:
1501 tns = pctx.topic_namespace()
1413 t = pctx.topic() 1502 t = pctx.topic()
1414 with repo.vfs.open(b'topic', b'w') as f: 1503 repo.vfs.write(b'topic-namespace', tns)
1415 f.write(t) 1504 if tns != b'default' and tns != otns:
1505 repo.ui.status(_(b"switching to topic-namespace %s\n") % tns)
1506 repo.vfs.write(b'topic', t)
1416 if t and t != ot: 1507 if t and t != ot:
1417 repo.ui.status(_(b"switching to topic %s\n") % t) 1508 repo.ui.status(_(b"switching to topic %s\n") % t)
1418 if ot and not t: 1509 if ot and not t:
1419 st = stack.stack(repo, topic=ot) 1510 st = stack.stack(repo, topic=ot)
1420 if not st: 1511 if not st:
1499 original(repo, ui, prev, ctx) 1590 original(repo, ui, prev, ctx)
1500 1591
1501 # Restore the topic if need 1592 # Restore the topic if need
1502 if topic: 1593 if topic:
1503 _changecurrenttopic(repo, topic) 1594 _changecurrenttopic(repo, topic)
1595
1596 def _changecurrenttns(repo, tns):
1597 if tns:
1598 with repo.wlock():
1599 repo.vfs.write(b'topic-namespace', tns)
1600 else:
1601 repo.vfs.unlinkpath(b'topic-namespace', ignoremissing=True)
1602
1603 @command(b'debug-topic-namespace', [
1604 (b'', b'clear', False, b'clear active topic namespace if any'),
1605 ],
1606 _(b'[NAMESPACE|--clear]'))
1607 def debugtopicnamespace(ui, repo, tns=None, **opts):
1608 """set or show the current topic namespace"""
1609 if opts.get('clear'):
1610 if tns:
1611 raise error.Abort(_(b"cannot use --clear when setting a topic namespace"))
1612 tns = None
1613 elif not tns:
1614 ui.write(b'%s\n' % repo.currenttns)
1615 return
1616 if tns:
1617 tns = tns.strip()
1618 if not tns:
1619 raise error.Abort(_(b"topic namespace cannot consist entirely of whitespace"))
1620 if b'/' in tns:
1621 raise error.Abort(_(b"topic namespace cannot contain '/' character"))
1622 scmutil.checknewlabel(repo, tns, b'topic namespace')
1623 ctns = repo.currenttns
1624 _changecurrenttns(repo, tns)
1625 if ctns == b'default' and tns:
1626 repo.ui.status(_(b'marked working directory as topic namespace: %s\n')
1627 % tns)
1628
1629 @command(b'debug-topic-namespaces', [])
1630 def debugtopicnamespaces(ui, repo, **opts):
1631 """list repository namespaces"""
1632 for tns in repo.topic_namespaces:
1633 ui.write(b'%s\n' % (tns,))
1634
1635 @command(b'debug-parse-fqbn', commands.formatteropts, _(b'FQBN'), optionalrepo=True)
1636 def debugparsefqbn(ui, repo, fqbn, **opts):
1637 """parse branch//namespace/topic string into its components"""
1638 branch, tns, topic = common.parsefqbn(fqbn)
1639 opts = pycompat.byteskwargs(opts)
1640 fm = ui.formatter(b'debug-parse-namespace', opts)
1641 fm.startitem()
1642 fm.write(b'branch', b'branch: %s\n', branch)
1643 fm.write(b'topic_namespace', b'namespace: %s\n', tns)
1644 fm.write(b'topic', b'topic: %s\n', topic)
1645 fm.end()
1646
1647 @command(b'debug-format-fqbn', [
1648 (b'b', b'branch', b'', b'branch'),
1649 (b'n', b'topic-namespace', b'', b'topic namespace'),
1650 (b't', b'topic', b'', b'topic'),
1651 (b's', b'short', False, b'short format'),
1652 ], optionalrepo=True)
1653 def debugformatfqbn(ui, repo, **opts):
1654 """format branch, namespace and topic into branch//namespace/topic string"""
1655 short = common.formatfqbn(opts.get('branch'), opts.get('topic_namespace'), opts.get('topic'), opts.get('short'))
1656 ui.write(b'%s\n' % short)