# 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>.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.Substitution takes place on every commit and update of the workingrepository.Keyword expansion is based on Mercurial's changeset template mappings.The extension provides an additional UTC-date filter.The user has the choice either to create his own keywords and theirexpansions or to use the CVS-like default ones.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 usernameExpansions spanning more than one line are truncated to their first line.Incremental expansion (like CVS' $Log$) is not supported.Simple setup in hgrc: # enable extension keyword = /full/path/to/keyword.py # or, if script in hgext folder: # hgext.keyword = # filename patterns for expansion are configured in this section # files matching patterns with value 'ignore' are ignored [keyword] **.py = x* = ignore ... # 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} ...'''frommercurial.i18nimportgettextas_# above line for backwards compatibility; can be changed to# from mercurial.i18n import _# some dayfrommercurialimportcontext,filelog,revlogfrommercurialimportcommands,cmdutil,templater,utilfrommercurial.nodeimportbinimportos.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]))defgetmodulename():'''Makes sure pretxncommit-hook can import keyword module regardless of where its located.'''fork,vinsys.modules.iteritems():ifvisNone:continueifnothasattr(v,'__file__'):continueifv.__file__.startswith(__file__):returnkelse:sys.path.insert(0,os.path.abspath(os.path.dirname(__file__)))returnos.path.splitext(os.path.basename(__file__))[0]defkwfmatches(ui,repo,files):'''Selects candidates for keyword substitution configured in keyword section in hgrc.'''inc,exc=[],[]forpat,optinui.configitems('keyword'):ifopt!='ignore':inc.append(pat)else:exc.append(pat)ifnotinc:ui.debug(_('keyword: no filename globs for substitution\n'))return[]kwfmatcher=util.matcher(repo.root,inc=inc,exc=['.hg*']+exc)[1]return[fforfinfilesifkwfmatcher(f)]classkwtemplater(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(ui.configitems('keywordmaps'))ordeftemplatesself.re_kw=re.compile(r'\$(%s)[^$]*?\$'%'|'.join(re.escape(k)forkinself.templates.keys()))templater.common_filters['utcdate']=utcdateself.t=cmdutil.changeset_templater(ui,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)kwsub=templater.firstline(self.ui.popbuffer())return'$%s: %s $'%(kw,kwsub)defreposetup(ui,repo):'''Sets up repo, and filelog especially, as kwrepo and kwfilelog for keyword substitution. This is done for local repos only.'''ifnotrepo.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=path# only init kwtemplater if neededifnotisinstance(repo,int)andkwfmatches(ui,repo,[path]):self.kwt=kwtemplater(ui,repo)else:self.kwt=Nonedefiskwcandidate(self,data):'''Decides whether to act on keywords.'''returnself.kwtisnotNoneandnotutil.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# configure pretxncommit hookui.setconfig('hooks','pretxncommit.keyword','python:%s.pretxnkw'%getmodulename())defpretxnkw(ui,repo,hooktype,**args):'''pretxncommit hook that collects candidates for keyword expansion on commit and expands keywords in working dir.'''cmd,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)