# states.py - introduce the state concept for mercurial changeset## Copyright 2011 Peter Arrenbrecht <peter.arrenbrecht@gmail.com># Logilab SA <contact@logilab.fr># Pierre-Yves David <pierre-yves.david@ens-lyon.org>## This software may be used and distributed according to the terms of the# GNU General Public License version 2 or any later version.'''A set of command to make changeset evolve.'''frommercurialimportcmdutilfrommercurialimportscmutilfrommercurialimportnodefrommercurialimporterrorfrommercurialimportextensionsfrommercurialimportcommandsfrommercurialimportbookmarksfrommercurialimportphasesfrommercurialimportcontextfrommercurialimportcommandsfrommercurialimportutilfrommercurial.i18nimport_frommercurial.commandsimportwalkopts,commitopts,commitopts2,logopts### util function#############################defnoderange(repo,revsets):"""The same as revrange but return node"""returnmap(repo.changelog.node,scmutil.revrange(repo,revsets))### extension check#############################defextsetup(ui):try:obsolete=extensions.find('obsolete')exceptKeyError:raiseerror.Abort(_('evolution extension require obsolete extension.'))try:rebase=extensions.find('rebase')exceptKeyError:raiseerror.Abort(_('evolution extension require rebase extension.'))### changeset rewriting logic#############################defrewrite(repo,old,updates,head,newbases,commitopts):iflen(old.parents())>1:#XXX remove this unecessary limitation.raiseerror.Abort(_('cannot amend merge changesets'))base=old.p1()bm=bookmarks.readcurrent(repo)wlock=repo.wlock()try:# commit a new version of the old changeset, including the update# collect all files which might be affectedfiles=set(old.files())foruinupdates:files.update(u.files())# prune files which were reverted by the updatesdefsamefile(f):iffinhead.manifest():a=head.filectx(f)iffinbase.manifest():b=base.filectx(f)return(a.data()==b.data()anda.flags()==b.flags()anda.renamed()==b.renamed())else:returnFalseelse:returnfnotinbase.manifest()files=[fforfinfilesifnotsamefile(f)]# commit version of these files as defined by headheadmf=head.manifest()deffilectxfn(repo,ctx,path):ifpathinheadmf:returnhead.filectx(path)raiseIOError()ifcommitopts.get('message')andcommitopts.get('logfile'):raiseutil.Abort(_('options --message and --logfile are mutually'' exclusive'))ifcommitopts.get('logfile'):message=open(commitopts['logfile']).read()elifcommitopts.get('message'):message=commitopts['message']else:message=old.description()new=context.memctx(repo,parents=newbases,text=message,files=files,filectxfn=filectxfn,user=commitopts.get('user')orNone,date=commitopts.get('date')orNone,extra=commitopts.get('extra')orNone)ifcommitopts.get('edit'):new._text=cmdutil.commitforceeditor(repo,new,[])newid=repo.commitctx(new)new=repo[newid]# update the bookmarkifbm:repo._bookmarks[bm]=newidbookmarks.write(repo)# hide obsolete csetsrepo.changelog.hiddeninit=False# add evolution metadatarepo.addobsolete(new.node(),old.node())foruinupdates:repo.addobsolete(u.node(),old.node())repo.addobsolete(new.node(),u.node())finally:wlock.release()returnnewiddefrelocate(repo,rev,dest):"""rewrite <rev> on dest"""try:rebase=extensions.find('rebase')# dummy state to trick rebase nodeassertrepo[rev].p2().rev()==node.nullrev,'no support yet'cmdutil.duplicatecopies(repo,rev,repo[dest].node(),repo[rev].p2().node())rebase.rebasenode(repo,rev,dest,{node.nullrev:node.nullrev})nodenew=rebase.concludenode(repo,rev,dest,node.nullid)nodesrc=repo.changelog.node(rev)repo.addobsolete(nodenew,nodesrc)phases.retractboundary(repo,repo[nodesrc].phase(),[nodenew])oldbookmarks=repo.nodebookmarks(nodesrc)forbookinoldbookmarks:repo._bookmarks[book]=nodenewifoldbookmarks:bookmarks.write(repo)exceptutil.Abort:# Invalidate the previous setparentsrepo.dirstate.invalidate()raise### new command#############################cmdtable={}command=cmdutil.command(cmdtable)@command('^evolve',[],'')defevolve(ui,repo):"""suggest the next evolution step"""obsolete=extensions.find('obsolete')next=min(obsolete.unstables(repo))obs=repo[next].parents()[0]ifnotobs.obsolete():obs=next.parents()[1]assertobs.obsolete()newer=obsolete.newerversion(repo,obs.node())target=newer[-1]repo.ui.status('hg relocate --rev %s%s\n'%(repo[next],repo[target]))@command('^relocate',[('r','rev','.',_('revision to relocate')),],'')defcmdrelocate(ui,repo,dest,rev='.'):"""relocate a changeset"""wlock=repo.wlock()try:src=scmutil.revsingle(repo,rev,rev)dest=scmutil.revsingle(repo,dest,dest)ifsrc==src.ancestor(dest):raiseutil.Abort(_('source is ancestor of destination'))relocate(repo,src.rev(),dest.rev())return0finally:wlock.release()@command('^kill',[],'<revs>')defkill(ui,repo,*revs):"""mark a changeset as obsolete This update the parent directory to a not-killed parent if the current working directory parent are killed. XXX bookmark support XXX handle merge XXX check immutable first """wlock=repo.wlock()try:targetnodes=set(noderange(repo,revs))fornintargetnodes:repo.addobsolete(node.nullid,n)# update to an unkilled parentwdp=repo['.']newnode=wdpwhilenewnode.obsolete():newnode=newnode.parents()[0]ifnewnode.node()!=wdp.node():commands.update(ui,repo,newnode.rev())ui.status(_('working directory now at %s\n')%newnode)finally:wlock.release()@command('^amend',[('A','addremove',None,_('mark new/missing files as added/removed before committing')),('n','note','',_('use text as commit message for this update')),('c','change','',_('specifies the changeset to amend'),_('REV')),('b','branch','',_('specifies a branch for the new.'),_('REV')),('e','edit',False,_('edit commit message.'),_('')),]+walkopts+commitopts+commitopts2,_('[OPTION]... [FILE]...'))defamend(ui,repo,*pats,**opts):"""combine a changeset with updates and replace it with a new one Commits a new changeset incorporating both the changes to the given files and all the changes from the current parent changeset into the repository. See :hg:`commit` for details about committing changes. If you don't specify -m, the parent's message will be reused. If you specify --change, amend additionally considers all changesets between the indicated changeset and the working copy parent as updates to be subsumed. This allows you to commit updates manually first. As a special shorthand you can say `--amend .` instead of '--amend p1(p1())', which subsumes your latest commit as an update of its parent. Behind the scenes, Mercurial first commits the update as a regular child of the current parent. Then it creates a new commit on the parent's parents with the updated contents. Then it changes the working copy parent to this new combined changeset. Finally, the old changeset and its update are hidden from :hg:`log` (unless you use --hidden with log). Returns 0 on success, 1 if nothing changed. """# determine updates to subsumechange=opts.get('change')ifchange=='.':change='p1(p1())'old=scmutil.revsingle(repo,change)branch=opts.get('branch')ifbranch:opts.setdefault('extra',{})['branch']=branchlock=repo.lock()try:wlock=repo.wlock()try:ifnotold.phase():raiseutil.Abort(_("can not rewrite immutable changeset %s")%old)# commit current changes as update# code copied from commands.commit to avoid noisy messagesciopts=dict(opts)ciopts.pop('message',None)ciopts.pop('logfile',None)ciopts['message']=opts.get('note')or('amends %s'%old.hex())e=cmdutil.commiteditordefcommitfunc(ui,repo,message,match,opts):returnrepo.commit(message,opts.get('user'),opts.get('date'),match,editor=e)cmdutil.commit(ui,repo,commitfunc,pats,ciopts)# find all changesets to be considered updatescl=repo.changeloghead=repo['.']updatenodes=set(cl.nodesbetween(roots=[old.node()],heads=[head.node()])[0])updatenodes.remove(old.node())ifnotupdatenodesandnot(opts.get('message')oropts.get('logfile')oropts.get('edit')):raiseerror.Abort(_('no updates found'))updates=[repo[n]forninupdatenodes]# perform amendifopts.get('edit'):opts['force_editor']=Truenewid=rewrite(repo,old,updates,head,[old.p1().node(),old.p2().node()],opts)# reroute the working copy parent to the new changesetphases.retractboundary(repo,old.phase(),[newid])repo.dirstate.setparents(newid,node.nullid)finally:wlock.release()finally:lock.release()defcommitwrapper(orig,ui,repo,*arg,**kwargs):obsoleted=kwargs.get('obsolete',[])ifobsoleted:obsoleted=repo.set('%lr',obsoleted)result=orig(ui,repo,*arg,**kwargs)ifnotresult:# commit successednew=repo['-1']foroldinobsoleted:repo.addobsolete(new.node(),old.node())returnresultdefgraftwrapper(orig,ui,repo,*revs,**kwargs):lock=repo.lock()try:ifkwargs.get('old_obsolete'):obsoleted=kwargs.setdefault('obsolete',[])ifkwargs['continue']:obsoleted.extend(repo.opener.read('graftstate').splitlines())else:obsoleted.extend(revs)returncommitwrapper(orig,ui,repo,*revs,**kwargs)finally:lock.release()defextsetup(ui):entry=extensions.wrapcommand(commands.table,'commit',commitwrapper)entry[1].append(('o','obsolete',[],_("this commit obsolet this revision")))entry=extensions.wrapcommand(commands.table,'graft',graftwrapper)entry[1].append(('o','obsolete',[],_("this graft obsolet this revision")))entry[1].append(('O','old-obsolete',False,_("graft result obsolete graft source")))