# __init__.py - topic extension## This software may be used and distributed according to the terms of the# GNU General Public License version 2 or any later version."""support for topic branchesTopic branches are lightweight branches which disappear when changes arefinalized (move to the public phase).Compared to bookmark, topic is reference carried by each changesets of theseries instead of just the single head revision. Topic are quite similar tothe way named branch work, except they eventually fade away when the changesetbecomes part of the immutable history. Changeset can belong to both a topic anda named branch, but as long as it is mutable, its topic identity will prevail.As a result, default destination for 'update', 'merge', etc... will take topicinto account. When a topic is active these operations will only consider otherchangesets on that topic (and, in some occurrence, bare changeset on samebranch). When no topic is active, changeset with topic will be ignored andonly bare one on the same branch will be taken in account.There is currently two commands to be used with that extension: 'topics' and'stack'.The 'hg topics' command is used to set the current topic, change and listexisting one. 'hg topics --verbose' will list various information related toeach topic.The 'stack' will show you information about the stack of commit belonging toyour current topic.Topic is offering you aliases reference to changeset in your current topicstack as 's#'. For example, 's1' refers to the root of your stack, 's2' to thesecond commits, etc. The 'hg stack' command show these number. 's0' can be usedto refer to the parent of the topic root. Updating using `hg up s0` will keepthe topic active.Push behavior will change a bit with topic. When pushing to a publishingrepository the changesets will turn public and the topic data on them will fadeaway. The logic regarding pushing new heads will behave has before, ignore anytopic related data. When pushing to a non-publishing repository (supportingtopic), the head checking will be done taking topic data into account.Push will complain about multiple heads on a branch if you push multiple headswith no topic information on them (or multiple public heads). But pushing a newtopic will not requires any specific flag. However, pushing multiple heads on atopic will be met with the usual warning.The 'evolve' extension takes 'topic' into account. 'hg evolve --all'will evolve all changesets in the active topic. In addition, by default. 'hgnext' and 'hg prev' will stick to the current topic.Be aware that this extension is still an experiment, commands and other featuresare likely to be change/adjusted/dropped over time as we refine the concept.topic-mode==========The topic extension can be configured to ensure the user do not forget to adda topic when committing a new topic:: [experimental] # behavior when commit is made without an active topic topic-mode = ignore # do nothing special (default) topic-mode = warning # print a warning topic-mode = enforce # abort the commit (except for merge) topic-mode = enforce-all # abort the commit (even for merge) topic-mode = random # use a randomized generated topic (except for merge) topic-mode = random-all # use a randomized generated topic (even for merge)Single head enforcing=====================The extensions come with an option to enforce that there is only one heads foreach name in the repository at any time.:: [experimental] enforce-single-head = yesPublishing behavior===================Topic vanish when changeset move to the public phases. Moving to the publicphase usually happens on push, but it is possible to update that behavior. Theserver needs to have specific config for this.* everything pushed become public (the default):: [phase] publish = yes* nothing push turned public:: [phase] publish = no* topic branches are not published, changeset without topic are:: [phase] publish = no [experimental] topic.publish-bare-branch = yesIn addition, the topic extension adds a ``--publish`` flag on :hg:`push`. Whenused, the pushed revisions are published if the push succeeds. It also appliesto common revisions selected by the push.One can prevent any publishing to happens in a repository using:: [experimental] topic.allow-publish = no"""from__future__importabsolute_importimportfunctoolsimportreimporttimeimportweakreffrommercurial.i18nimport_frommercurialimport(bookmarks,changelog,cmdutil,commands,context,error,extensions,hg,localrepo,lockaslockmod,merge,namespaces,node,obsolete,patch,phases,pycompat,registrar,scmutil,templatefilters,templatekw,util,)from.import(common,compat,constants,destination,discovery,flow,randomname,revsetastopicrevset,stack,topicmap,)cmdtable={}command=registrar.command(cmdtable)colortable={b'topic.active':b'green',b'topic.list.unstablecount':b'red',b'topic.list.headcount.multiple':b'yellow',b'topic.list.behindcount':b'cyan',b'topic.list.behinderror':b'red',b'stack.index':b'yellow',b'stack.index.base':b'none dim',b'stack.desc.base':b'none dim',b'stack.shortnode.base':b'none dim',b'stack.state.base':b'dim',b'stack.state.clean':b'green',b'stack.index.current':b'cyan',# random pickb'stack.state.current':b'cyan bold',# random pickb'stack.desc.current':b'cyan',# random pickb'stack.shortnode.current':b'cyan',# random pickb'stack.state.orphan':b'red',b'stack.state.content-divergent':b'red',b'stack.state.phase-divergent':b'red',b'stack.summary.behindcount':b'cyan',b'stack.summary.behinderror':b'red',b'stack.summary.headcount.multiple':b'yellow',# default color to help log output and thg# (first pick I could think off, update as neededb'log.topic':b'green_background',b'topic.active':b'green',}__version__=b'0.17.2.dev'testedwith=b'4.5.2 4.6.2 4.7 4.8 4.9 5.0 5.1'minimumhgversion=b'4.5'buglink=b'https://bz.mercurial-scm.org/'ifutil.safehasattr(registrar,'configitem'):frommercurialimportconfigitemsconfigtable={}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,)defextsetup(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.frommercurialimportconfigitemsextraitem=functools.partial(configitems._register,ui._knownconfig)if(b'experimental'notinui._knownconfigornotui._knownconfig[b'experimental'].get(b'thg.displaynames')):extraitem(b'experimental',b'thg.displaynames',default=None,)if(b'devel'notinui._knownconfigornotui._knownconfig[b'devel'].get(b'random')):extraitem(b'devel',b'randomseed',default=None,)# we need to do old style declaration for <= 4.5templatekeyword=registrar.templatekeyword()post45template=r'requires='intemplatekeyword.__doc__def_contexttopic(self,force=False):ifnot(forceorself.mutable()):returnb''returnself.extra().get(constants.extrakey,b'')context.basectx.topic=_contexttopicdef_contexttopicidx(self):topic=self.topic()ifnottopicorself.obsolete():# XXX we might want to include s0 here,# however s0 is related to 'currenttopic' which has no place here.returnNonerevlist=stack.stack(self._repo,topic=topic)try:returnrevlist.index(self.rev())exceptIndexError:# Lets move to the last ctx of the current topicreturnNonecontext.basectx.topicidx=_contexttopicidxstackrev=re.compile(br'^s\d+$')topicrev=re.compile(br'^t\d+$')hastopicext=common.hastopicextdef_namemap(repo,name):revs=Noneifstackrev.match(name):idx=int(name[1:])tname=topic=repo.currenttopiciftopic:ttype=b'topic'revs=list(stack.stack(repo,topic=topic))else:ttype=b'branch'tname=branch=repo[None].branch()revs=list(stack.stack(repo,branch=branch))eliftopicrev.match(name):idx=int(name[1:])ttype=b'topic'tname=topic=repo.currenttopicifnottname:raiseerror.Abort(_(b'cannot resolve "%s": no active topic')%name)revs=list(stack.stack(repo,topic=topic))ifrevsisnotNone:try:r=revs[idx]exceptIndexError:ifttype==b'topic':msg=_(b'cannot resolve "%s": %s "%s" has only %d changesets')elifttype==b'branch':msg=_(b'cannot resolve "%s": %s "%s" has only %d non-public changesets')raiseerror.Abort(msg%(name,ttype,tname,len(revs)-1))# t0 or s0 can be Noneifr==-1andidx==0:msg=_(b'the %s "%s" has no %s')raiseerror.Abort(msg%(ttype,tname,name))return[repo[r].node()]ifnamenotinrepo.topics:return[]node=repo.changelog.nodereturn[node(rev)forrevinrepo.revs(b'topic(%s)',name)]def_nodemap(repo,node):ctx=repo[node]t=ctx.topic()iftandctx.phase()>phases.public:return[t]return[]defuisetup(ui):destination.modsetup(ui)discovery.modsetup(ui)topicmap.modsetup(ui)setupimportexport(ui)extensions.afterloaded(b'rebase',_fixrebase)flow.installpushflag(ui)entry=extensions.wrapcommand(commands.table,b'commit',commitwrap)entry[1].append((b't',b'topic',b'',_(b"use specified topic"),_(b'TOPIC')))entry=extensions.wrapcommand(commands.table,b'push',pushoutgoingwrap)entry[1].append((b't',b'topic',b'',_(b"topic to push"),_(b'TOPIC')))entry=extensions.wrapcommand(commands.table,b'outgoing',pushoutgoingwrap)entry[1].append((b't',b'topic',b'',_(b"topic to push"),_(b'TOPIC')))extensions.wrapfunction(cmdutil,'buildcommittext',committextwrap)extensions.wrapfunction(merge,'update',mergeupdatewrap)# We need to check whether t0 or b0 or s0 is passed to override the default update# behaviour of changing topic and I can't find a better way# to do that as scmutil.revsingle returns the rev number and hence we can't# plug into logic for this into mergemod.update().extensions.wrapcommand(commands.table,b'update',checkt0)try:evolve=extensions.find(b'evolve')extensions.wrapfunction(evolve.rewriteutil,"presplitupdate",presplitupdatetopic)except(KeyError,AttributeError):passcmdutil.summaryhooks.add(b'topic',summaryhook)ifnotpost45template:templatekw.keywords[b'topic']=topickwtemplatekw.keywords[b'topicidx']=topicidxkw# Wrap workingctx extra to return the topic nameextensions.wrapfunction(context.workingctx,'__init__',wrapinit)# Wrap changelog.add to drop empty topicextensions.wrapfunction(changelog.changelog,'add',wrapadd)defreposetup(ui,repo):ifnotisinstance(repo,localrepo.localrepository):return# this can be a peer in the ssh case (puzzling)repo=repo.unfiltered()ifrepo.ui.config(b'experimental',b'thg.displaynames')isNone:repo.ui.setconfig(b'experimental',b'thg.displaynames',b'topics',source=b'topic-extension')classtopicrepo(repo.__class__):# attribute for other code to distinct between repo with topic and repo withouthastopicext=Truedef_restrictcapabilities(self,caps):caps=super(topicrepo,self)._restrictcapabilities(caps)caps.add(b'topics')returncapsdefcommit(self,*args,**kwargs):backup=self.ui.backupconfig(b'ui',b'allowemptycommit')try:ifself.currenttopic!=self[b'.'].topic():# bypass the core "nothing changed" logicself.ui.setconfig(b'ui',b'allowemptycommit',True)returnsuper(topicrepo,self).commit(*args,**kwargs)finally:self.ui.restoreconfig(backup)defcommitctx(self,ctx,*args,**kwargs):topicfilter=topicmap.topicfilter(self.filtername)iftopicfilter!=self.filtername:other=self.filtered(topicmap.topicfilter(self.filtername))other.commitctx(ctx,*args,**kwargs)ifisinstance(ctx,context.workingcommitctx):current=self.currenttopicifcurrent:ctx.extra()[constants.extrakey]=currentif(isinstance(ctx,context.memctx)andctx.extra().get(b'amend_source')andctx.topic()andnotself.currenttopic):# we are amending and need to remove a topicdelctx.extra()[constants.extrakey]returnsuper(topicrepo,self).commitctx(ctx,*args,**kwargs)@propertydeftopics(self):ifself._topicsisnotNone:returnself._topicstopics=set([b'',self.currenttopic])forcinself.set(b'not public()'):topics.add(c.topic())topics.remove(b'')self._topics=topicsreturntopics@propertydefcurrenttopic(self):returnself.vfs.tryread(b'topic')# overwritten at the instance level by topicmap.py_autobranchmaptopic=Truedefbranchmap(self,topic=None):iftopicisNone:topic=getattr(self,'_autobranchmaptopic',False)topicfilter=topicmap.topicfilter(self.filtername)ifnottopicortopicfilter==self.filtername:returnsuper(topicrepo,self).branchmap()returnself.filtered(topicfilter).branchmap()defbranchheads(self,branch=None,start=None,closed=False):ifbranchisNone:branch=self[None].branch()ifself.currenttopic:branch=b"%s:%s"%(branch,self.currenttopic)returnsuper(topicrepo,self).branchheads(branch=branch,start=start,closed=closed)definvalidatevolatilesets(self):# XXX we might be able to move this to something invalidated less oftensuper(topicrepo,self).invalidatevolatilesets()self._topics=Nonedefpeer(self):peer=super(topicrepo,self).peer()ifgetattr(peer,'_repo',None)isnotNone:# localpeerclasstopicpeer(peer.__class__):defbranchmap(self):usetopic=notself._repo.publishing()returnself._repo.branchmap(topic=usetopic)peer.__class__=topicpeerreturnpeerdeftransaction(self,desc,*a,**k):ctr=self.currenttransaction()tr=super(topicrepo,self).transaction(desc,*a,**k)ifdescin(b'strip',b'repair')orctrisnotNone:returntrreporef=weakref.ref(self)ifself.ui.configbool(b'experimental',b'enforce-single-head'):ifutil.safehasattr(tr,'validator'):# hg <= 4.7origvalidator=tr.validatorelse:origvalidator=tr._validatordefvalidator(tr2):repo=reporef()flow.enforcesinglehead(repo,tr2)origvalidator(tr2)ifutil.safehasattr(tr,'validator'):# hg <= 4.7tr.validator=validatorelse:tr._validator=validatortopicmodeserver=self.ui.config(b'experimental',b'topic-mode.server',b'ignore')ispush=(desc.startswith(b'push')ordesc.startswith(b'serve'))if(topicmodeserver!=b'ignore'andispush):ifutil.safehasattr(tr,'validator'):# hg <= 4.7origvalidator=tr.validatorelse:origvalidator=tr._validatordefvalidator(tr2):repo=reporef()flow.rejectuntopicedchangeset(repo,tr2)returnorigvalidator(tr2)ifutil.safehasattr(tr,'validator'):# hg <= 4.7tr.validator=validatorelse:tr._validator=validatorelif(self.ui.configbool(b'experimental',b'topic.publish-bare-branch')and(desc.startswith(b'push')ordesc.startswith(b'serve'))):origclose=tr.closetrref=weakref.ref(tr)defclose():repo=reporef()tr2=trref()flow.publishbarebranch(repo,tr2)origclose()tr.close=closeallow_publish=self.ui.configbool(b'experimental',b'topic.allow-publish',True)ifnotallow_publish:ifutil.safehasattr(tr,'validator'):# hg <= 4.7origvalidator=tr.validatorelse:origvalidator=tr._validatordefvalidator(tr2):repo=reporef()flow.reject_publish(repo,tr2)returnorigvalidator(tr2)ifutil.safehasattr(tr,'validator'):# hg <= 4.7tr.validator=validatorelse:tr._validator=validator# real transaction startct=self.currenttopicifnotct:returntrctwasempty=stack.stack(self,topic=ct).changesetcount==0reporef=weakref.ref(self)defcurrenttopicempty(tr):# check active topic emptinessrepo=reporef()csetcount=stack.stack(repo,topic=ct).changesetcountempty=csetcount==0ifemptyandnotctwasempty:ui.status(b"active topic '%s' is now empty\n"%ct)trnames=getattr(tr,'names',getattr(tr,'_names',()))if(b'phase'intrnamesorany(n.startswith(b'push-response')fornintrnames)):ui.status(_(b"(use 'hg topic --clear' to clear it if needed)\n"))hint=_(b"(see 'hg help topics' for more information)\n")ifctwasemptyandnotempty:ifcsetcount==1:msg=_(b"active topic '%s' grew its first changeset\n%s")ui.status(msg%(ct,hint))else:msg=_(b"active topic '%s' grew its %s first changesets\n%s")ui.status(msg%(ct,csetcount,hint))tr.addpostclose(b'signalcurrenttopicempty',currenttopicempty)returntrrepo.__class__=topicreporepo._topics=Noneifutil.safehasattr(repo,'names'):repo.names.addnamespace(namespaces.namespace(b'topics',b'topic',namemap=_namemap,nodemap=_nodemap,listnames=lambdarepo:repo.topics))ifpost45template:@templatekeyword(b'topic',requires={b'ctx'})deftopickw(context,mapping):""":topic: String. The topic of the changeset"""ctx=context.resource(mapping,b'ctx')returnctx.topic()@templatekeyword(b'topicidx',requires={b'ctx'})deftopicidxkw(context,mapping):""":topicidx: Integer. Index of the changeset as a stack alias"""ctx=context.resource(mapping,b'ctx')returnctx.topicidx()else:deftopickw(**args):""":topic: String. The topic of the changeset"""returnargs[b'ctx'].topic()deftopicidxkw(**args):""":topicidx: Integer. Index of the changeset as a stack alias"""returnargs[b'ctx'].topicidx()defwrapinit(orig,self,repo,*args,**kwargs):orig(self,repo,*args,**kwargs)ifnothastopicext(repo):returnifconstants.extrakeynotinself._extra:ifgetattr(repo,'currenttopic',b''):self._extra[constants.extrakey]=repo.currenttopicelse:# Empty key will be dropped from extra by another hack at the changegroup levelself._extra[constants.extrakey]=b''defwrapadd(orig,cl,manifest,files,desc,transaction,p1,p2,user,date=None,extra=None,p1copies=None,p2copies=None,filesadded=None,filesremoved=None):ifconstants.extrakeyinextraandnotextra[constants.extrakey]:extra=extra.copy()delextra[constants.extrakey]# hg <= 4.9 (0e41f40b01cc)kwargs={}ifp1copiesisnotNone:kwargs['p1copies']=p1copiesifp2copiesisnotNone:kwargs['p2copies']=p2copies# hg <= 5.0 (f385ba70e4af)iffilesaddedisnotNone:kwargs['filesadded']=filesaddediffilesremovedisnotNone:kwargs['filesremoved']=filesremovedreturnorig(cl,manifest,files,desc,transaction,p1,p2,user,date=date,extra=extra,**kwargs)# revset predicates are automatically registered at loading via this symbolrevsetpredicate=topicrevset.revsetpredicate@command(b'topics',[(b'',b'clear',False,b'clear active topic if any'),(b'r',b'rev',[],b'revset of existing revisions',_(b'REV')),(b'l',b'list',False,b'show the stack of changeset in the topic'),(b'',b'age',False,b'show when you last touched the topics'),(b'',b'current',None,b'display the current topic only'),]+commands.formatteropts,_(b'hg topics [TOPIC]'))deftopics(ui,repo,topic=None,**opts):"""View current topic, set current topic, change topic for a set of revisions, or see all topics. Clear topic on existing topiced revisions:: hg topics --rev <related revset> --clear Change topic on some revisions:: hg topics <newtopicname> --rev <related revset> Clear current topic:: hg topics --clear Set current topic:: hg topics <topicname> List of topics:: hg topics List of topics sorted according to their last touched time displaying last touched time and the user who last touched the topic:: hg topics --age The active topic (if any) will be prepended with a "*". The `--current` flag helps to take active topic into account. For example, if you want to set the topic on all the draft changesets to the active topic, you can do: `hg topics -r "draft()" --current` The --verbose version of this command display various information on the state of each topic."""clear=opts.get('clear')list=opts.get('list')rev=opts.get('rev')current=opts.get('current')age=opts.get('age')ifcurrentandtopic:raiseerror.Abort(_(b"cannot use --current when setting a topic"))ifcurrentandclear:raiseerror.Abort(_(b"cannot use --current and --clear"))ifclearandtopic:raiseerror.Abort(_(b"cannot use --clear when setting a topic"))ifageandtopic:raiseerror.Abort(_(b"cannot use --age while setting a topic"))touchedrevs=set()ifrev:touchedrevs=scmutil.revrange(repo,rev)iftopic:topic=topic.strip()ifnottopic:raiseerror.Abort(_(b"topic name cannot consist entirely of whitespaces"))# Have some restrictions on the topic name just like bookmark namescmutil.checknewlabel(repo,topic,b'topic')rmatch=re.match(br'[-_.\w]+',topic)ifnotrmatchorrmatch.group(0)!=topic:helptxt=_(b"topic names can only consist of alphanumeric, '-'"b" '_' and '.' characters")raiseerror.Abort(_(b"invalid topic name: '%s'")%topic,hint=helptxt)iflist:ui.pager(b'topics')ifclearorrev:raiseerror.Abort(_(b"cannot use --clear or --rev with --list"))ifnottopic:topic=repo.currenttopicifnottopic:raiseerror.Abort(_(b'no active topic to list'))returnstack.showstack(ui,repo,topic=topic,opts=pycompat.byteskwargs(opts))iftouchedrevs:ifnotobsolete.isenabled(repo,obsolete.createmarkersopt):raiseerror.Abort(_(b'must have obsolete enabled to change topics'))ifclear:topic=Noneelifopts.get('current'):topic=repo.currenttopicelifnottopic:raiseerror.Abort(b'changing topic requires a topic name or --clear')ifrepo.revs(b'%ld and public()',touchedrevs):raiseerror.Abort(b"can't change topic of a public change")wl=lock=txn=Nonetry:wl=repo.wlock()lock=repo.lock()txn=repo.transaction(b'rewrite-topics')rewrote=_changetopics(ui,repo,touchedrevs,topic)txn.close()iftopicisNone:ui.status(b'cleared topic on %d changesets\n'%rewrote)else:ui.status(b'changed topic on %d changesets to "%s"\n'%(rewrote,topic))finally:lockmod.release(txn,lock,wl)repo.invalidate()returnct=repo.currenttopicifclear:ifct:st=stack.stack(repo,topic=ct)ifnotst:ui.status(_(b'clearing empty topic "%s"\n')%ct)return_changecurrenttopic(repo,None)iftopic:ifnotct:ui.status(_(b'marked working directory as topic: %s\n')%topic)return_changecurrenttopic(repo,topic)ui.pager(b'topics')# `hg topic --current`ret=0ifcurrentandnotct:ui.write_err(_(b'no active topic\n'))ret=1elifcurrent:fm=ui.formatter(b'topic',pycompat.byteskwargs(opts))namemask=b'%s\n'label=b'topic.active'fm.startitem()fm.write(b'topic',namemask,ct,label=label)fm.end()else:_listtopics(ui,repo,opts)returnret@command(b'stack',[(b'c',b'children',None,_(b'display data about children outside of the stack'))]+commands.formatteropts,_(b'hg stack [TOPIC]'))defcmdstack(ui,repo,topic=b'',**opts):"""list all changesets in a topic and other information List the current topic by default. The --verbose version shows short nodes for the commits also. """ifnottopic:topic=Nonebranch=NoneiftopicisNoneandrepo.currenttopic:topic=repo.currenttopiciftopicisNone:branch=repo[None].branch()ui.pager(b'stack')returnstack.showstack(ui,repo,branch=branch,topic=topic,opts=pycompat.byteskwargs(opts))@command(b'debugcb|debugconvertbookmark',[(b'b',b'bookmark',b'',_(b'bookmark to convert to topic')),(b'',b'all',None,_(b'convert all bookmarks to topics')),],_(b'[-b BOOKMARK] [--all]'))defdebugconvertbookmark(ui,repo,**opts):"""Converts a bookmark to a topic with the same name. """bookmark=opts.get('bookmark')convertall=opts.get('all')ifconvertallandbookmark:raiseerror.Abort(_(b"cannot use '--all' and '-b' together"))ifnot(convertallorbookmark):raiseerror.Abort(_(b"you must specify either '--all' or '-b'"))bmstore=repo._bookmarksnodetobook={}forbook,revnodeinbmstore.items():ifnodetobook.get(revnode):nodetobook[revnode].append(book)else:nodetobook[revnode]=[book]# a list of nodes which we have skipped so that we don't print the skip# warning repeatedlyskipped=[]actions={}lock=wlock=tr=Nonetry:wlock=repo.wlock()lock=repo.lock()ifbookmark:try:node=bmstore[bookmark]exceptKeyError:raiseerror.Abort(_(b"no such bookmark exists: '%s'")%bookmark)revnum=repo[node].rev()iflen(nodetobook[node])>1:ui.status(_(b"skipping revision '%d' as it has multiple bookmarks "b"on it\n")%revnum)returntargetrevs=_findconvertbmarktopic(repo,bookmark)iftargetrevs:actions[(bookmark,revnum)]=targetrevselifconvertall:forbmark,revnodeinsorted(bmstore.items()):revnum=repo[revnode].rev()ifrevnuminskipped:continueiflen(nodetobook[revnode])>1:ui.status(_(b"skipping '%d' as it has multiple bookmarks on"b" it\n")%revnum)skipped.append(revnum)continueifbmark==b'@':continuetargetrevs=_findconvertbmarktopic(repo,bmark)iftargetrevs:actions[(bmark,revnum)]=targetrevsifactions:try:tr=repo.transaction(b'debugconvertbookmark')for((bmark,revnum),targetrevs)insorted(actions.items()):_applyconvertbmarktopic(ui,repo,targetrevs,revnum,bmark,tr)tr.close()finally:tr.release()finally:lockmod.release(lock,wlock)# inspired from mercurial.repair.stripbmrevsetCONVERTBOOKREVSET=b"""not public() and ( ancestors(bookmark(%s)) and not ancestors( ( (head() and not bookmark(%s)) or (bookmark() - bookmark(%s)) ) - ( descendants(bookmark(%s)) - bookmark(%s) ) ))"""def_findconvertbmarktopic(repo,bmark):"""find revisions unambiguously defined by a bookmark find all changesets under the bookmark and under that bookmark only. """returnrepo.revs(CONVERTBOOKREVSET,bmark,bmark,bmark,bmark,bmark)def_applyconvertbmarktopic(ui,repo,revs,old,bmark,tr):"""apply bookmark conversion to topic Sets a topic as same as bname to all the changesets under the bookmark and delete the bookmark, if topic is set to any changeset old is the revision on which bookmark bmark is and tr is transaction object. """rewrote=_changetopics(ui,repo,revs,bmark)# We didn't changed topic to any changesets because the revset# returned an empty set of revisions, so let's skip deleting the# bookmark corresponding to which we didn't put a topic on any# changesetifrewrote==0:returnui.status(_(b'changed topic to "%s" on %d revisions\n')%(bmark,rewrote))ui.debug(b'removing bookmark "%s" from "%d"'%(bmark,old))bookmarks.delete(repo,tr,[bmark])def_changecurrenttopic(repo,newtopic):"""changes the current topic."""ifnewtopic:withrepo.wlock():withrepo.vfs.open(b'topic',b'w')asf:f.write(newtopic)else:ifrepo.vfs.exists(b'topic'):repo.vfs.unlink(b'topic')def_changetopics(ui,repo,revs,newtopic):""" Changes topic to newtopic of all the revisions in the revset and return the count of revisions whose topic has been changed. """rewrote=0p1=Nonep2=Nonesuccessors={}forrinrevs:c=repo[r]deffilectxfn(repo,ctx,path):try:returnc[path]excepterror.ManifestLookupError:returnNonefixedextra=dict(c.extra())ui.debug(b'old node id is %s\n'%node.hex(c.node()))ui.debug(b'origextra: %r\n'%fixedextra)oldtopic=fixedextra.get(constants.extrakey,None)ifoldtopic==newtopic:continueifnewtopicisNone:delfixedextra[constants.extrakey]else:fixedextra[constants.extrakey]=newtopicfixedextra[constants.changekey]=c.hex()ifb'amend_source'infixedextra:# TODO: right now the commitctx wrapper in# topicrepo overwrites the topic in extra if# amend_source is set to support 'hg commit# --amend'. Support for amend should be adjusted# to not be so invasive.delfixedextra[b'amend_source']ui.debug(b'changing topic of %s from %s to %s\n'%(c,oldtopicorb'<none>',newtopicorb'<none>'))ui.debug(b'fixedextra: %r\n'%fixedextra)# While changing topic of set of linear commits, make sure that# we base our commits on new parent rather than old parent which# was obsoleted while changing the topicp1=c.p1().node()p2=c.p2().node()ifp1insuccessors:p1=successors[p1][0]ifp2insuccessors:p2=successors[p2][0]mc=context.memctx(repo,(p1,p2),c.description(),c.files(),filectxfn,user=c.user(),date=c.date(),extra=fixedextra)# phase handlingcommitphase=c.phase()overrides={(b'phases',b'new-commit'):commitphase}withrepo.ui.configoverride(overrides,b'changetopic'):newnode=repo.commitctx(mc)successors[c.node()]=(newnode,)ui.debug(b'new node id is %s\n'%node.hex(newnode))rewrote+=1# create obsmarkers and move bookmarks# XXX we should be creating marker as we go instead of only at the end,# this makes the operations more modularsscmutil.cleanupnodes(repo,successors,b'changetopics')# move the working copy toowctx=repo[None]# in-progress merge is a bit too complex for now.iflen(wctx.parents())==1:newid=successors.get(wctx.p1().node())ifnewidisnotNone:hg.update(repo,newid[0],quietempty=True)returnrewrotedef_listtopics(ui,repo,opts):fm=ui.formatter(b'topics',pycompat.byteskwargs(opts))activetopic=repo.currenttopicnamemask=b'%s'ifrepo.topics:maxwidth=max(len(t)fortinrepo.topics)namemask=b'%%-%is'%maxwidthifopts.get('age'):# here we sort by age and topic nametopicsdata=sorted(_getlasttouched(repo,repo.topics))else:# here we sort by topic name onlytopicsdata=((None,topic,None,None)fortopicinsorted(repo.topics))forage,topic,date,userintopicsdata:fm.startitem()marker=b' 'label=b'topic'active=(topic==activetopic)ifactive:marker=b'*'label=b'topic.active'ifnotui.quiet:# registering the active data is made explicitly laterfm.plain(b' %s '%marker,label=label)fm.write(b'topic',namemask,topic,label=label)fm.data(active=active)ifui.quiet:fm.plain(b'\n')continuefm.plain(b' (')ifdate:ifage==-1:timestr=b'empty and active'else:timestr=templatefilters.age(date)fm.write(b'lasttouched',b'%s',timestr,label=b'topic.list.time')ifuser:fm.write(b'usertouched',b' by %s',user,label=b'topic.list.user')ifdate:fm.plain(b', ')data=stack.stack(repo,topic=topic)ifui.verbose:fm.write(b'branches+',b'on branch: %s',b'+'.join(data.branches),# XXX use list directly after 4.0 is releasedlabel=b'topic.list.branches')fm.plain(b', ')fm.write(b'changesetcount',b'%d changesets',data.changesetcount,label=b'topic.list.changesetcount')ifdata.unstablecount:fm.plain(b', ')fm.write(b'unstablecount',b'%d unstable',data.unstablecount,label=b'topic.list.unstablecount')headcount=len(data.heads)if1<headcount:fm.plain(b', ')fm.write(b'headcount',b'%d heads',headcount,label=b'topic.list.headcount.multiple')ifui.verbose:# XXX we should include the data even when not verbosebehindcount=data.behindcountif0<behindcount:fm.plain(b', ')fm.write(b'behindcount',b'%d behind',behindcount,label=b'topic.list.behindcount')elif-1==behindcount:fm.plain(b', ')fm.write(b'behinderror',b'%s',_(b'ambiguous destination: %s')%data.behinderror,label=b'topic.list.behinderror')fm.plain(b')\n')fm.end()def_getlasttouched(repo,topics):""" Calculates the last time a topic was used. Returns a generator of 4-tuples: (age in seconds, topic name, date, and user who last touched the topic). """curtime=time.time()fortopicintopics:age=-1user=Nonemaxtime=(0,0)trevs=repo.revs(b"topic(%s)",topic)# Need to check for the time of all changesets in the topic, whether# they are obsolete of non-heads# XXX: can we just rely on the max rev number for thisforrevsintrevs:rt=repo[revs].date()ifrt[0]>=maxtime[0]:# Can store the rev to gather more info# latesthead = revsmaxtime=rtuser=repo[revs].user()# looking on the markers also to get more information and accurate# last touch time.obsmarkers=compat.getmarkers(repo,[repo[revs].node()])formarkerinobsmarkers:rt=marker.date()ifrt[0]>maxtime[0]:user=marker.metadata().get(b'user',user)maxtime=rtusername=stack.parseusername(user)iftrevs:age=curtime-maxtime[0]yield(age,topic,maxtime,username)defsummaryhook(ui,repo):t=getattr(repo,'currenttopic',b'')ifnott:return# i18n: column positioning for "hg summary"ui.write(_(b"topic: %s\n")%ui.label(t,b'topic.active'))_validmode=[b'ignore',b'warning',b'enforce',b'enforce-all',b'random',b'random-all',]def_configtopicmode(ui):""" Parse the config to get the topicmode """topicmode=ui.config(b'experimental',b'topic-mode')# Fallback to read enforce-topiciftopicmodeisNone:enforcetopic=ui.configbool(b'experimental',b'enforce-topic')ifenforcetopic:topicmode=b"enforce"iftopicmodenotin_validmode:topicmode=_validmode[0]returntopicmodedefcommitwrap(orig,ui,repo,*args,**opts):ifnothastopicext(repo):returnorig(ui,repo,*args,**opts)withrepo.wlock():topicmode=_configtopicmode(ui)ismergecommit=len(repo[None].parents())==2notopic=notrepo.currenttopicmayabort=(topicmode==b"enforce"andnotismergecommit)maywarn=(topicmode==b"warning"or(topicmode==b"enforce"andismergecommit))mayrandom=Falseiftopicmode==b"random":mayrandom=notismergecommiteliftopicmode==b"random-all":mayrandom=Trueiftopicmode==b'enforce-all':ismergecommit=Falsemayabort=Truemaywarn=Falsehint=_(b"see 'hg help -e topic.topic-mode' for details")ifopts.get('topic'):t=opts['topic']withrepo.vfs.open(b'topic',b'w')asf:f.write(t)elifopts.get('amend'):passelifnotopicandmayabort:msg=_(b"no active topic")raiseerror.Abort(msg,hint=hint)elifnotopicandmaywarn:ui.warn(_(b"warning: new draft commit without topic\n"))ifnotui.quiet:ui.warn((b"(%s)\n")%hint)elifnotopicandmayrandom:withrepo.vfs.open(b'topic',b'w')asf:f.write(randomname.randomtopicname(ui))returnorig(ui,repo,*args,**opts)defcommittextwrap(orig,repo,ctx,subs,extramsg):ret=orig(repo,ctx,subs,extramsg)ifhastopicext(repo):t=repo.currenttopicift:ret=ret.replace(b"\nHG: branch",b"\nHG: topic '%s'\nHG: branch"%t)returnretdefpushoutgoingwrap(orig,ui,repo,*args,**opts):ifopts.get('topic'):topicrevs=repo.revs(b'topic(%s) - obsolete()',opts['topic'])opts.setdefault('rev',[]).extend(topicrevs)returnorig(ui,repo,*args,**opts)defmergeupdatewrap(orig,repo,node,branchmerge,force,*args,**kwargs):matcher=kwargs.get('matcher')partial=not(matcherisNoneormatcher.always())wlock=repo.wlock()isrebase=Falseist0=Falsetry:ret=orig(repo,node,branchmerge,force,*args,**kwargs)ifnothastopicext(repo):returnret# The mergeupdatewrap function makes the destination's topic as the# current topic. This is right for merge but wrong for rebase. We check# if rebase is running and update the currenttopic to topic of new# rebased commit. We have explicitly stored in config if rebase is# running.ot=repo.currenttopicifrepo.ui.hasconfig(b'experimental',b'topicrebase'):isrebase=Trueifrepo.ui.configbool(b'_internal',b'keep-topic'):ist0=Trueif((notpartialandnotbranchmerge)orisrebase)andnotist0:t=b''pctx=repo[node]ifpctx.phase()>phases.public:t=pctx.topic()withrepo.vfs.open(b'topic',b'w')asf:f.write(t)iftandt!=ot:repo.ui.status(_(b"switching to topic %s\n")%t)ifotandnott:st=stack.stack(repo,topic=ot)ifnotst:repo.ui.status(_(b'clearing empty topic "%s"\n')%ot)elifist0:repo.ui.status(_(b"preserving the current topic '%s'\n")%ot)returnretfinally:wlock.release()defcheckt0(orig,ui,repo,node=None,rev=None,*args,**kwargs):thezeros=set([b't0',b'b0',b's0'])backup=repo.ui.backupconfig(b'_internal',b'keep-topic')try:ifnodeinthezerosorrevinthezeros:repo.ui.setconfig(b'_internal',b'keep-topic',b'yes',source=b'topic-extension')returnorig(ui,repo,node=node,rev=rev,*args,**kwargs)finally:repo.ui.restoreconfig(backup)def_fixrebase(loaded):ifnotloaded:returndefsavetopic(ctx,extra):ifctx.topic():extra[constants.extrakey]=ctx.topic()defsetrebaseconfig(orig,ui,repo,**opts):repo.ui.setconfig(b'experimental',b'topicrebase',b'yes',source=b'topic-extension')returnorig(ui,repo,**opts)defnew_init(orig,*args,**kwargs):runtime=orig(*args,**kwargs)ifutil.safehasattr(runtime,'extrafns'):runtime.extrafns.append(savetopic)returnruntimetry:rebase=extensions.find(b"rebase")extensions.wrapfunction(rebase.rebaseruntime,'__init__',new_init)# This exists to store in the config that rebase is running so that we can# update the topic according to rebase. This is a hack and should be removed# when we have better options.extensions.wrapcommand(rebase.cmdtable,b'rebase',setrebaseconfig)exceptKeyError:pass## preserve topic during import/exportdef_exporttopic(seq,ctx):topic=ctx.topic()iftopic:returnb'EXP-Topic %s'%topicreturnNonedef_importtopic(repo,patchdata,extra,opts):ifb'topic'inpatchdata:extra[b'topic']=patchdata[b'topic']defsetupimportexport(ui):"""run at ui setup time to install import/export logic"""cmdutil.extraexport.append(b'topic')cmdutil.extraexportmap[b'topic']=_exporttopiccmdutil.extrapreimport.append(b'topic')cmdutil.extrapreimportmap[b'topic']=_importtopicpatch.patchheadermap.append((b'EXP-Topic',b'topic'))## preserve topic during splitdefpresplitupdatetopic(original,repo,ui,prev,ctx):# Save topic of revisiontopic=Noneifutil.safehasattr(ctx,'topic'):topic=ctx.topic()# Update the working directoryoriginal(repo,ui,prev,ctx)# Restore the topic if neediftopic:_changecurrenttopic(repo,topic)