# keyword.py - keyword expansion for Mercurial## Copyright 2007 Christian Ebert <blacktrash@gmx.net>## This software may be used and distributed according to the terms# of the GNU General Public License, incorporated herein by reference.## $Id$## Keyword expansion hack against the grain of a DSCM## There are many good reasons why this is not needed in a distributed# SCM, still it may be useful in very small projects based on single# files (like LaTeX packages), that are mostly addressed to an audience# not running a version control system.## For in-depth discussion refer to# <http://www.selenic.com/mercurial/wiki/index.cgi/KeywordPlan>.## Keyword expansion is based on Mercurial's changeset template mappings.# The extension provides an additional UTC-date filter ({date|utcdate}).## The user has the choice either to create his own keywords and their# expansions or to use the CVS-like default ones.## Expansions spanning more than one line are truncated to their first line.# Incremental expansion (like CVS' $Log$) is not supported.## Binary files are not touched.## Setup in hgrc:## # enable extension# keyword = /full/path/to/keyword.py# # or, if script in hgext folder:# # hgext.keyword ='''keyword expansion in local repositoriesThis extension expands RCS/CVS-like or self-customized keywords inthe text files selected by your configuration.Keywords are only expanded in local repositories and not logged byMercurial internally. The mechanism can be regarded as a conveniencefor the current user and may be turned off anytime.An additional date template filter {date|utcdate} is provided.Caveat: "hg import" might fail if the patches were exported from arepo with a different/no keyword setup, whereas "hg unbundle" issafe.Configuration is done in the [keyword] and [keywordmaps] sections ofhgrc files.Example: [extensions] hgext.keyword = [keyword] # expand keywords in every python file, # except those matching "x*" **.py = x* = ignoreFor [keywordmaps] demonstration run "hg kwdemo".'''frommercurial.i18nimportgettextas_frommercurialimportcommands,fancyopts,templater,utilfrommercurialimportcmdutil,context,filelog,localrepo# findcmd might be in cmdutil or commands# depending on mercurial versionifhasattr(cmdutil,'findcmd'):findcmd=cmdutil.findcmdelse:findcmd=commands.findcmdimportos,re,shutil,sys,tempfile,timedeftemplates={'Revision':'{node|short}','Author':'{author|user}','Date':'{date|utcdate}','RCSFile':'{file|basename},v','Source':'{root}/{file},v','Id':'{file|basename},v {node|short} {date|utcdate} {author|user}','Header':'{root}/{file},v {node|short} {date|utcdate} {author|user}',}nokwcommands=('add','addremove','bundle','clone','copy','export','incoming','outgoing','push','remove','rename','rollback')defutcdate(date):'''Returns hgdate in cvs-like UTC format.'''returntime.strftime('%Y/%m/%d %H:%M:%S',time.gmtime(date[0]))defgetcmd(ui):'''Returns current hg command.'''# commands.parse(ui, sys.argv[1:])[0] breaks "hg diff -r"try:args=fancyopts.fancyopts(sys.argv[1:],commands.globalopts,{})exceptfancyopts.getopt.GetoptError,inst:raisecommands.ParseError(None,inst)ifargs:cmd=args[0]aliases,i=findcmd(ui,cmd)returnaliases[0]classkwtemplater(object):''' Sets up keyword templates, corresponding keyword regex, and provides keyword substitution functions. '''def__init__(self,ui,repo,path='',node=None):self.ui=uiself.repo=repoself.path=pathself.node=nodetemplates=dict(ui.configitems('keywordmaps'))iftemplates:# parse templates here for less overhead in kwsub matchfuncforkintemplates.keys():templates[k]=templater.parsestring(templates[k],quoted=False)self.templates=templatesordeftemplatesescaped=[re.escape(k)forkinself.templates.keys()]self.re_kw=re.compile(r'\$(%s)[^$]*?\$'%'|'.join(escaped))templater.common_filters['utcdate']=utcdatetry:self.t=cmdutil.changeset_templater(ui,repo,False,'',False)exceptTypeError:# depending on hg rev changeset_templater has extra "brinfo" argself.t=cmdutil.changeset_templater(ui,repo,False,None,'',False)defkwsub(self,mobj):'''Substitutes keyword using corresponding template.'''kw=mobj.group(1)self.t.use_template(self.templates[kw])self.ui.pushbuffer()self.t.show(changenode=self.node,root=self.repo.root,file=self.path)keywordsub=templater.firstline(self.ui.popbuffer())return'$%s: %s $'%(kw,keywordsub)defexpand(self,node,data):'''Returns data with expanded keywords.'''ifutil.binary(data):returndatac=context.filectx(self.repo,self.path,fileid=node)self.node=c.node()returnself.re_kw.sub(self.kwsub,data)defshrink(self,text):'''Returns text with all keyword substitutions removed.'''ifutil.binary(text):returntextreturnself.re_kw.sub(r'$\1$',text)defoverwrite(self,candidates,mn):'''Overwrites candidates in working dir expanding keywords.'''files=[]m=self.repo.manifest.read(mn)forfincandidates:data=self.repo.wread(f)ifnotutil.binary(data):self.path=fdata,kwct=self.re_kw.subn(self.kwsub,data)ifkwct:self.ui.debug(_('overwriting %s expanding keywords\n')%f)self.repo.wwrite(f,data,m.flags(f))files.append(f)iffiles:self.repo.dirstate.update(files,'n')classkwfilelog(filelog.filelog):''' Subclass of filelog to hook into its read, add, cmp methods. Keywords are "stored" unexpanded, and expanded on reading. '''def__init__(self,opener,path,kwtemplater):super(kwfilelog,self).__init__(opener,path)self.kwtemplater=kwtemplaterdefread(self,node):'''Substitutes keywords when reading filelog.'''data=super(kwfilelog,self).read(node)returnself.kwtemplater.expand(node,data)defadd(self,text,meta,tr,link,p1=None,p2=None):'''Removes keyword substitutions when adding to filelog.'''text=self.kwtemplater.shrink(text)returnsuper(kwfilelog,self).add(text,meta,tr,link,p1=p1,p2=p2)defcmp(self,node,text):'''Removes keyword substitutions for comparison.'''text=self.kwtemplater.shrink(text)ifself.renamed(node):t2=super(kwfilelog,self).read(node)returnt2!=textreturnsuper(kwfilelog,self).cmp(node,text)defdemo(ui,repo,**opts):'''print [keywordmaps] configuration and an expansion example '''log='hg keyword config and expansion example'fn='demo.txt'tmpdir=tempfile.mkdtemp('','kwdemo.')ifui.verbose:ui.status(_('creating temporary repo at %s\n')%tmpdir)_repo=localrepo.localrepository(ui,path=tmpdir,create=True)_repo.ui.setconfig('keyword',fn,'')ifopts['default']:kwstatus='default'kwmaps=deftemplateselse:kwstatus='current'kwmaps=dict(ui.configitems('keywordmaps'))ordeftemplatesifui.configitems('keywordmaps'):fork,vinkwmaps.items():_repo.ui.setconfig('keywordmaps',k,v)reposetup(_repo.ui,_repo)ui.status(_('config with %s keyword maps:\n')%kwstatus)ui.write('[keyword]\n%s =\n[keywordmaps]\n'%fn)fork,vinkwmaps.items():ui.write('%s = %s\n'%(k,v))path=_repo.wjoin(fn)keywords='$'+'$\n$'.join(kwmaps.keys())+'$\n'_repo.wfile(fn,'w').write(keywords)_repo.add([fn])ifui.verbose:ui.status(_('\n%s keywords written to %s:\n')%(kwstatus,path))ui.write(keywords)ui.status(_("\nhg --repository '%s' commit\n")%tmpdir)_repo.commit(text=log)ifui.verbose:ui.status(_('\n%s keywords expanded in %s:\n')%(kwstatus,path))else:ui.status(_('\n%s keywords expanded:\n')%kwstatus)ui.write(_repo.wread(fn))ui.debug(_('\nremoving temporary repo\n'))shutil.rmtree(tmpdir)defreposetup(ui,repo):'''Sets up repo as kwrepo for keyword substitution. Overrides file method to return kwfilelog instead of filelog if file matches user configuration. Wraps commit to overwrite configured files with updated keyword substitutions. This is done for local repos only, and only if there are files configured at all for keyword substitution.'''ifnotrepo.local()orgetcmd(repo.ui)innokwcommands:returninc,exc=[],['.hg*']forpat,optinrepo.ui.configitems('keyword'):ifopt!='ignore':inc.append(pat)else:exc.append(pat)ifnotinc:returnkwfmatcher=util.matcher(repo.root,inc=inc,exc=exc)[1]ui=repo.uiclasskwrepo(repo.__class__):deffile(self,f):iff[0]=='/':f=f[1:]ifkwfmatcher(f):kwt=kwtemplater(ui,self,path=f)returnkwfilelog(self.sopener,f,kwt)else:returnfilelog.filelog(self.sopener,f)defcommit(self,files=None,text='',user=None,date=None,match=util.always,force=False,lock=None,wlock=None,force_editor=False,p1=None,p2=None,extra={}):wrelease=Falseifnotwlock:wlock=self.wlock()wrelease=Truetry:removed=self.status(node1=p1,node2=p2,files=files,match=match,wlock=wlock)[2]node=super(kwrepo,self).commit(files=files,text=text,user=user,date=date,match=match,force=force,lock=lock,wlock=wlock,force_editor=force_editor,p1=p1,p2=p2,extra=extra)ifnodeisNone:returnnodecl=self.changelog.read(node)candidates=[fforfincl[3]ifkwfmatcher(f)andfnotinremovedandnotos.path.islink(self.wjoin(f))]ifcandidates:kwt=kwtemplater(ui,self,node=node)kwt.overwrite(candidates,cl[0])returnnodefinally:ifwrelease:wlock.release()repo.__class__=kwrepocmdtable={'kwdemo':(demo,[('d','default',None,_('use default keyword maps'))],_('hg kwdemo [-d]')),}