# 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.The exansion works in 2 modes: 1) working mode: substitution takes place on every commit and update of the working repository. 2) archive mode: substitution is only triggered by "hg archive".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: [keyword] # filename patterns for expansion are configured in this section **.py = ## expand keywords in all python files x* = ignore ## but ignore files matching "x*" ** = archive ## keywords in all textfiles are expanded ## when creating a distribution y* = noarchive ## keywords in files matching "y*" are not expanded ## on archive creation ... [keywordmaps] # custom hg template maps _replace_ the CVS-like default ones HGdate = {date|rfc822date} lastlog = {desc} ## same as {desc|firstline} in this context checked in by = {author} ...If no [keywordmaps] are configured the extension falls back on thefollowing defaults: Revision: changeset id Author: username Date: %Y/%m/%d %H:%M:%S ## [UTC] RCSFile: basename,v Source: /path/to/basename,v Id: basename,v csetid %Y/%m/%d %H:%M:%S username Header: /path/to/basename,v csetid %Y/%m/%d %H:%M:%S username'''try:frommercurial.demandloadimport*# stablefrommercurial.i18nimportgettextas_demandload(globals(),'mercurial:commands,fancyopts,templater,util')demandload(globals(),'mercurial:cmdutil,context,filelog,revlog')demandload(globals(),'re sys time')exceptImportError:# demandimportfrommercurial.i18nimport_frommercurialimportcommands,fancyopts,templater,utilfrommercurialimportcmdutil,context,filelog,revlogimportre,sys,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}',}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=commands.findcmd(ui,cmd)returnaliases[0]classkwtemplater(object):''' Sets up keyword templates, corresponding keyword regex, and provides keyword substitution functions. '''def__init__(self,ui,repo):self.ui=uiself.repo=repotemplates=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=templatesordeftemplatesself.re_kw=re.compile(r'\$(%s)[^$]*?\$'%'|'.join(re.escape(k)forkinself.templates.keys()))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,path,node):'''Substitutes keyword using corresponding template.'''kw=mobj.group(1)self.t.use_template(self.templates[kw])self.ui.pushbuffer()self.t.show(changenode=node,root=self.repo.root,file=path)keywordsub=templater.firstline(self.ui.popbuffer())return'$%s: %s $'%(kw,keywordsub)defexpand(self,path,node,filelog,data):'''Returns data with expanded keywords.'''ifutil.binary(data):returndatac=context.filectx(self.repo,path,fileid=node,filelog=filelog)cnode=c.node()returnself.re_kw.sub(lambdam:self.kwsub(m,path,cnode),data)defshrink(self,text):'''Returns text with all keyword substitutions removed.'''ifutil.binary(text):returntextreturnself.re_kw.sub(r'$\1$',text)defoverwrite(self,candidates,node):'''Overwrites candidates in working dir expanding keywords.'''files=[]forfincandidates:data=self.repo.wfile(f).read()ifnotutil.binary(data):data,kwct=self.re_kw.subn(lambdam:self.kwsub(m,f,node),data)ifkwct:self.ui.debug(_('overwriting %s expanding keywords\n')%f)self.repo.wfile(f,'w').write(data)files.append(f)iffiles:self.repo.dirstate.update(files,'n')classkwfilelog(filelog.filelog):''' Superclass over filelog to customize its read, add, cmp methods. Keywords are "stored" unexpanded, and expanded on reading. '''def__init__(self,opener,path,kwtemplater,defversion=revlog.REVLOG_DEFAULT_VERSION):super(kwfilelog,self).__init__(opener,path,defversion)self.path=pathself.kwtemplater=kwtemplaterdefread(self,node):'''Substitutes keywords when reading filelog.'''data=super(kwfilelog,self).read(node)returnself.kwtemplater.expand(self.path,node,self,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)returnsuper(kwfilelog,self).cmp(node,text)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():returnarchivemode=(getcmd(repo.ui)=='archive')inc,exc,archive,noarchive=[],['.hg*'],[],['.hg*']forpat,optinrepo.ui.configitems('keyword'):ifopt=='archive':archive.append(pat)elifopt=='noarchive':noarchive.append(pat)elifopt=='ignore':exc.append(pat)else:inc.append(pat)ifarchivemode:inc,exc=archive,noarchiveifnotinc:returnrepo.kwfmatcher=util.matcher(repo.root,inc=inc,exc=exc)[1]classkwrepo(repo.__class__):deffile(self,f):iff[0]=='/':f=f[1:]# only use kwfilelog when neededifself.kwfmatcher(f):kwt=kwtemplater(self.ui,self)returnkwfilelog(self.sopener,f,kwt,self.revlogversion)else:returnfilelog.filelog(self.sopener,f,self.revlogversion)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={}):'''Wraps commit, expanding keywords of committed and configured files in working directory.'''wrelease=Falseifnotwlock:wlock=self.wlock()wrelease=Truetry:removed=self.status()[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:returnnodecandidates=self.changelog.read(node)[3]candidates=[fforfincandidatesiffnotinremovedandnotself._link(f)andself.kwfmatcher(f)]ifnotcandidates:returnnodekwt=kwtemplater(self.ui,self)kwt.overwrite(candidates,node)returnnodefinally:ifwrelease:wlock.release()repo.__class__=kwrepo