# keyword.py - keyword expansion for mercurial# $Id$'''keyword expansion hack against the grain of a DSCMThis extension lets you expand RCS/CVS-like keywords in a Mercurialrepository.There are many good reasons why this is not needed in a distributedSCM, still it may be useful in very small projects based on singlefiles (like LaTeX packages), that are mostly addressed to an audiencenot running a version control system.For in-depth discussion refer to<http://www.selenic.com/mercurial/wiki/index.cgi/KeywordPlan>.You can either use the default cvs-like keywords or provide yourown in hgrc.Expansions spanning more than one line are truncated to their first line.Incremental expansion (like CVS' $Log$) is not supported.Default $keywords$ and their $keyword: substition $ are: 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 usernameSimple setup in hgrc: # enable extension hgext.keyword = /full/path/to/script # or, if script in hgext folder: # hgext.keyword = # filename patterns for expansion are configured in this section [keyword] **.py = expand ... # in case you prefer your own keyword maps over the cvs-like defaults: [keywordmaps] HGdate = {date|rfc822date} lastlog = {desc} ## same as {desc|firstline} in this context checked in by = {author} ...'''frommercurialimportcmdutil,templater,utilfrommercurial.nodeimport*importos.path,re,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]))templater.common_filters['utcdate']=utcdatedefkwfmatches(ui,repo,files):'''Selects candidates for keyword substitution configured in keyword section in hgrc.'''files=[fforfinfilesifnotf.startswith('.hg')]ifnotfiles:return[]candidates=[]kwfmatchers=[util.matcher(repo.root,'',[pat],[],[])[1]forpat,optinui.configitems('keyword')ifopt=='expand']forfinfiles:formfinkwfmatchers:ifmf(f):candidates.append(f)breakreturncandidatesclasskwtemplater(object):''' Sets up keyword templates, corresponding keyword regex, and provides keyword expansion function. '''def__init__(self,ui,repo):self.ui=uiself.repo=repoself.templates=(dict(self.ui.configitems('keywordmaps'))ordeftemplates)self.re_kw=re.compile(r'\$(%s)[^$]*?\$'%'|'.join(re.escape(k)forkinself.templates.keys()))self.t=cmdutil.changeset_templater(self.ui,self.repo,False,'',False)defexpand(self,mobj,path,node):'''Expands keyword with corresponding template.'''kw=mobj.group(1)template=templater.parsestring(self.templates[kw],quoted=False)self.t.use_template(template)self.ui.pushbuffer()self.t.show(changenode=node,root=self.repo.root,file=path)expansion=templater.firstline(self.ui.popbuffer())return'$%s: %s $'%(kw,expansion)defreposetup(ui,repo):frommercurialimportcontext,filelog,revlogifnotrepo.local():returnclasskwrepo(repo.__class__):deffile(self,f):iff[0]=='/':f=f[1:]returnfilelog.filelog(self.sopener,f,self,self.revlogversion)classkwfilelog(filelog.filelog):''' Superclass over filelog to customize it's read, add, cmp methods. Keywords are "stored" unexpanded, and expanded on reading. '''def__init__(self,opener,path,repo,defversion=revlog.REVLOG_DEFAULT_VERSION):super(kwfilelog,self).__init__(opener,path,defversion)self._repo=repoself._path=pathself.kwt=kwtemplater(ui,self._repo)defiskwcandidate(self,data):'''Decides whether to act on keywords.'''return(kwfmatches(ui,self._repo,[self._path])andnotutil.binary(data))defread(self,node):'''Substitutes keywords when reading filelog.'''data=super(kwfilelog,self).read(node)ifself.iskwcandidate(data):c=context.filectx(self._repo,self._path,fileid=node,filelog=self)returnself.kwt.re_kw.sub(lambdam:self.kwt.expand(m,self._path,c.node()),data)returndatadefadd(self,text,meta,tr,link,p1=None,p2=None):'''Removes keyword substitutions when adding to filelog.'''ifself.iskwcandidate(text):text=self.kwt.re_kw.sub(r'$\1$',text)returnsuper(kwfilelog,self).add(text,meta,tr,link,p1=p1,p2=p2)defcmp(self,node,text):'''Removes keyword substitutions for comparison.'''ifself.iskwcandidate(text):text=self.kwt.re_kw.sub(r'$\1$',text)returnsuper(kwfilelog,self).cmp(node,text)filelog.filelog=kwfilelogrepo.__class__=kwrepo# make pretxncommit hook import kwmodule regardless of where it's locatedfork,vinsys.modules.iteritems():ifvisNone:continueifnothasattr(v,'__file__'):continueifv.__file__.startswith(__file__):mod=kbreakelse:sys.path.insert(0,os.path.abspath(os.path.dirname(__file__)))mod=os.path.splitext(os.path.basename(__file__))[0]ui.setconfig('hooks','pretxncommit.keyword','python:%s.pretxnkw'%mod)delmoddefpretxnkw(ui,repo,hooktype,**args):'''pretxncommit hook that collects candidates for keyword expansion on commit and expands keywords in working dir.'''frommercurial.i18nimportgettextas_# above line for backwards compatibility; can be changed to# from mercurial.i18n import _# some dayfrommercurialimportcommandscmd,sysargs,globalopts,cmdopts=commands.parse(ui,sys.argv[1:])[1:]ifrepr(cmd).split()[1]in('tag','import_'):returnfiles,match,anypats=cmdutil.matchpats(repo,sysargs,cmdopts)modified,added=repo.status(files=files,match=match)[:2]candidates=kwfmatches(ui,repo,modified+added)ifnotcandidates:returnkwt=kwtemplater(ui,repo)node=bin(args['node'])forfincandidates:data=repo.wfile(f).read()ifnotutil.binary(data):data,kwct=kwt.re_kw.subn(lambdam:kwt.expand(m,f,node),data)ifkwct:ui.debug(_('overwriting %s expanding keywords\n'%f))repo.wfile(f,'w').write(data)