# keyword.py - keyword expansion for mercurial
# $Id: keyword.py,v c4daf4753ed3 2007-01-08 03:08:24 +0100 blacktrash $
'''keyword expansion hack against the grain of a DSCM
This extension lets you expand RCS/CVS-like keywords in a Mercurial
repository.
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.
Supported $keywords$ are:
Revision: changeset id
Author: full username
Date: %a %b %d %H:%M:%S %Y %z $
RCSFile: basename,v
Source: /path/to/basename,v
Id: basename,v csetid %Y-%m-%d %H:%M:%S %s shortname
Header: /path/to/basename,v csetid %Y-%m-%d %H:%M:%S %s shortname
Simple setup in hgrc:
# enable extension
# keyword.py in hgext folder, specify full path otherwise
hgext.keyword =
# filename patterns for expansion are configured in this section
[keyword]
**.py = expand
...
'''
from mercurial.node import *
from mercurial.i18n import gettext as _
from mercurial import context, filelog, revlog, util
import os.path, re
re_kw = re.compile(
r'\$(Id|Header|Author|Date|Revision|RCSFile|Source)[^$]*?\$')
def kwexpand(matchobj, repo, path, changeid=None, fileid=None, filelog=None):
'''Called by kwrepo.commit and kwfilelog.read.
Sets supported keywords as local variables and evaluates them to
their expansion if matchobj is equal to string representation.'''
c = context.filectx(repo, path,
changeid=changeid, fileid=fileid, filelog=filelog)
date = c.date()
Revision = c.changectx()
Author = c.user()
RCSFile = os.path.basename(path)+',v'
Source = repo.wjoin(path)+',v'
Date = util.datestr(date=date)
revdateauth = '%s %s %s' % (Revision,
util.datestr(date=date, format=util.defaultdateformats[0]),
util.shortuser(Author))
Header = '%s %s' % (Source, revdateauth)
Id = '%s %s' % (RCSFile, revdateauth)
return '$%s: %s $' % (matchobj.group(1), eval(matchobj.group(1)))
def kwfmatches(ui, repo, files):
'''Selects candidates for keyword substitution
configured in keyword section in hgrc.'''
files = [f for f in files if not f.startswith('.hg')]
if not files:
return []
candidates = []
fmatchers = [util.matcher(repo.root, '', [pat], [], [])[1]
for pat, opt in ui.configitems('keyword')
if opt == 'expand']
for f in files:
for mf in fmatchers:
if mf(f):
candidates.append(f)
break
return candidates
def reposetup(ui, repo):
if not repo.local():
return
class kwrepo(repo.__class__):
def file(self, f):
if f[0] == '/':
f = f[1:]
return filelog.filelog(self.sopener, f, self, self.revlogversion)
def commit(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={}):
commit = []
remove = []
changed = []
use_dirstate = (p1 is None) # not rawcommit
extra = extra.copy()
if use_dirstate:
if files:
for f in files:
s = self.dirstate.state(f)
if s in 'nmai':
commit.append(f)
elif s == 'r':
remove.append(f)
else:
ui.warn(_("%s not tracked!\n") % f)
else:
changes = self.status(match=match)[:5]
modified, added, removed, deleted, unknown = changes
commit = modified + added
remove = removed
else:
commit = files
if use_dirstate:
p1, p2 = self.dirstate.parents()
update_dirstate = True
else:
p1, p2 = p1, p2 or nullid
update_dirstate = (self.dirstate.parents()[0] == p1)
c1 = self.changelog.read(p1)
c2 = self.changelog.read(p2)
m1 = self.manifest.read(c1[0]).copy()
m2 = self.manifest.read(c2[0])
if use_dirstate:
branchname = self.workingctx().branch()
try:
branchname = branchname.decode('UTF-8').encode('UTF-8')
except UnicodeDecodeError:
raise util.Abort(_('branch name not in UTF-8!'))
else:
branchname = ""
if use_dirstate:
oldname = c1[5].get("branch", "") # stored in UTF-8
if not commit and not remove and not force and p2 == nullid and \
branchname == oldname:
ui.status(_("nothing changed\n"))
return None
xp1 = hex(p1)
if p2 == nullid: xp2 = ''
else: xp2 = hex(p2)
self.hook("precommit", throw=True, parent1=xp1, parent2=xp2)
if not wlock:
wlock = self.wlock()
if not lock:
lock = self.lock()
tr = self.transaction()
# check in files
new = {}
linkrev = self.changelog.count()
commit.sort()
for f in commit:
ui.note(f + "\n")
try:
new[f] = self.filecommit(f, m1, m2, linkrev, tr, changed)
m1.set(f, util.is_exec(self.wjoin(f), m1.execf(f)))
except IOError:
if use_dirstate:
ui.warn(_("trouble committing %s!\n") % f)
raise
else:
remove.append(f)
# update manifest
m1.update(new)
remove.sort()
for f in remove:
if f in m1:
del m1[f]
mn = self.manifest.add(m1, tr, linkrev, c1[0], c2[0], (new, remove))
# add changeset
new = new.keys()
new.sort()
user = user or ui.username()
if not text or force_editor:
edittext = []
if text:
edittext.append(text)
edittext.append("")
edittext.append("HG: user: %s" % user)
if p2 != nullid:
edittext.append("HG: branch merge")
edittext.extend(["HG: changed %s" % f for f in changed])
edittext.extend(["HG: removed %s" % f for f in remove])
if not changed and not remove:
edittext.append("HG: no files changed")
edittext.append("")
# run editor in the repository root
olddir = os.getcwd()
os.chdir(self.root)
text = ui.edit("\n".join(edittext), user)
os.chdir(olddir)
lines = [line.rstrip() for line in text.rstrip().splitlines()]
while lines and not lines[0]:
del lines[0]
if not lines:
return None
text = '\n'.join(lines)
if branchname:
extra["branch"] = branchname
n = self.changelog.add(mn, changed + remove, text, tr, p1, p2,
user, date, extra)
self.hook('pretxncommit', throw=True, node=hex(n), parent1=xp1,
parent2=xp2)
# substitute keywords
for f in kwfmatches(ui, self, changed):
data = self.wfile(f).read()
if not util.binary(data):
data, kwct = re_kw.subn(lambda m:
kwexpand(m, self, f, changeid=hex(n)), data)
if kwct:
ui.debug(_('overwriting %s expanding keywords\n' % f))
self.wfile(f, 'w').write(data)
tr.close()
if use_dirstate or update_dirstate:
self.dirstate.setparents(n)
if use_dirstate:
self.dirstate.update(new, "n")
self.dirstate.forget(removed)
self.hook("commit", node=hex(n), parent1=xp1, parent2=xp2)
return n
class kwfilelog(filelog.filelog):
def __init__(self, opener, path, repo,
defversion=revlog.REVLOG_DEFAULT_VERSION):
super(kwfilelog, self).__init__(opener, path, defversion)
self._repo = repo
self._path = path
def read(self, node):
data = super(kwfilelog, self).read(node)
if not util.binary(data) and \
kwfmatches(ui, self._repo, [self._path]):
return re_kw.sub(lambda m:
kwexpand(m, self._repo, self._path,
fileid=node, filelog=self), data)
return data
def cmp(self, node, text):
'''Removes keyword substitution for comparison.'''
if not util.binary(text) and \
kwfmatches(ui, self._repo, [self._path]):
text = re_kw.sub(r'$\1$', text)
return super(kwfilelog, self).cmp(node, text)
filelog.filelog = kwfilelog
repo.__class__ = kwrepo