hgkw/keyword.py
branchsolo-extension
changeset 69 4c5d9635b517
parent 68 b285ab731fff
child 71 f7a2a246740c
equal deleted inserted replaced
68:b285ab731fff 69:4c5d9635b517
     8 There are many good reasons why this is not needed in a distributed
     8 There are many good reasons why this is not needed in a distributed
     9 SCM, still it may be useful in very small projects based on single
     9 SCM, still it may be useful in very small projects based on single
    10 files (like LaTeX packages), that are mostly addressed to an audience
    10 files (like LaTeX packages), that are mostly addressed to an audience
    11 not running a version control system.
    11 not running a version control system.
    12 
    12 
    13 Supported keywords are (changeset 000000000000):
    13 Supported $keywords$ are:
    14     $Revision: 000000000000 $
    14     Revision: changeset id
    15     $Author: Your Name <address@example.com> $
    15     Author:   full username
    16     $Date: %a %b %d %H:%M:%S %Y %z $
    16     Date:     %a %b %d %H:%M:%S %Y %z $
    17     $RCSFile: basename,v $
    17     RCSFile:  basename,v
    18     $Source: /path/to/basename,v $
    18     Source:   /path/to/basename,v
    19     $Id: basename,v 000000000000 %Y-%m-%d %H:%M:%S %z shortname $
    19     Id:       basename,v csetid %Y-%m-%d %H:%M:%S %s shortname
    20     $Header: /path/to/basename,v 000000000000 %Y-%m-%d %H:%M:%S %z shortname $
    20     Header:   /path/to/basename,v csetid %Y-%m-%d %H:%M:%S %s shortname
    21 
       
    22 The extension, according to its hackish nature, is a hybrid and consists
       
    23 actually in 2 parts:
       
    24 
       
    25     1. pure extension code (reposetup) that is triggered on checkout and
       
    26        logging of changes.
       
    27     2. a pretxncommit hook (hgrc (5)) that expands keywords immediately
       
    28        at commit time in the working directory.
       
    29 
    21 
    30 Simple setup in hgrc:
    22 Simple setup in hgrc:
    31 
    23 
    32     # enable extension
    24     # enable extension
       
    25     # keyword.py in hgext folder, specify full path otherwise
    33     hgext.keyword =
    26     hgext.keyword =
    34     
    27     
    35     # filename patterns for expansion are configured in this section
    28     # filename patterns for expansion are configured in this section
    36     [keyword]
    29     [keyword]
    37     *.sty = expand
    30     **.py = expand
    38     ...
    31     ...
    39 
       
    40     # set up pretxncommit hook
       
    41     [hooks]
       
    42     pretxncommit =
       
    43     pretxncommit.keyword = python:hgext.keyword.pretxnkw
       
    44 '''
    32 '''
    45 
    33 
       
    34 from mercurial.node import *
    46 from mercurial.i18n import _
    35 from mercurial.i18n import _
    47 from mercurial import context, util
    36 from mercurial import context, filelog, revlog, util
    48 import os.path, re
    37 import os.path, re
    49 
    38 
    50 
    39 
    51 re_kw = re.compile(
    40 re_kw = re.compile(
    52         r'\$(Id|Header|Author|Date|Revision|RCSFile|Source)[^$]*?\$')
    41         r'\$(Id|Header|Author|Date|Revision|RCSFile|Source)[^$]*?\$')
    53 
    42 
    54 
    43 
    55 def kwexpand(matchobj, repo, path, changeid=None, fileid=None, filelog=None):
    44 def kwexpand(matchobj, repo, path, changeid=None, fileid=None, filelog=None):
    56     '''Called by kwfilelog.read and pretxnkw.
    45     '''Called by kwrepo.commit and kwfilelog.read.
    57     Sets supported keywords as local variables and evaluates them to
    46     Sets supported keywords as local variables and evaluates them to
    58     their expansion if matchobj is equal to string representation.'''
    47     their expansion if matchobj is equal to string representation.'''
    59 
       
    60     c = context.filectx(repo, path,
    48     c = context.filectx(repo, path,
    61             changeid=changeid, fileid=fileid, filelog=filelog)
    49             changeid=changeid, fileid=fileid, filelog=filelog)
    62     date = c.date()
    50     date = c.date()
    63 
       
    64     Revision = c.changectx()
    51     Revision = c.changectx()
    65     Author = c.user()
    52     Author = c.user()
    66     RCSFile = os.path.basename(path)+',v'
    53     RCSFile = os.path.basename(path)+',v'
    67     Source = repo.wjoin(path)+',v'
    54     Source = repo.wjoin(path)+',v'
    68     Date = util.datestr(date=date)
    55     Date = util.datestr(date=date)
    69     revdateauth = '%s %s %s' % (Revision,
    56     revdateauth = '%s %s %s' % (Revision,
    70             util.datestr(date=date, format=util.defaultdateformats[0]),
    57             util.datestr(date=date, format=util.defaultdateformats[0]),
    71             util.shortuser(Author))
    58             util.shortuser(Author))
    72     Header = '%s %s' % (Source, revdateauth)
    59     Header = '%s %s' % (Source, revdateauth)
    73     Id = '%s %s' % (RCSFile, revdateauth)
    60     Id = '%s %s' % (RCSFile, revdateauth)
    74 
       
    75     return '$%s: %s $' % (matchobj.group(1), eval(matchobj.group(1)))
    61     return '$%s: %s $' % (matchobj.group(1), eval(matchobj.group(1)))
    76 
    62 
    77 def kwfmatchers(ui, repo):
    63 def kwfmatches(ui, repo, files):
    78     '''Returns filename matchers from ui keyword section.'''
    64     '''Selects candidates for keyword substitution
    79     return [util.matcher(repo.root, '', [pat], [], [])[1]
    65     configured in keyword section in hgrc.'''
       
    66     files = [f for f in files if not f.startswith('.hg')]
       
    67     if not files:
       
    68         return []
       
    69     candidates = []
       
    70     fmatchers = [util.matcher(repo.root, '', [pat], [], [])[1]
    80             for pat, opt in ui.configitems('keyword')
    71             for pat, opt in ui.configitems('keyword')
    81             if opt == 'expand']
    72             if opt == 'expand']
       
    73     for f in files:
       
    74         for mf in fmatchers:
       
    75             if mf(f):
       
    76                 candidates.append(f)
       
    77                 break
       
    78     return candidates
    82 
    79 
    83 
    80 
    84 def reposetup(ui, repo):
    81 def reposetup(ui, repo):
    85     from mercurial import filelog, revlog
       
    86 
    82 
    87     if not repo.local():
    83     if not repo.local():
    88         return
    84         return
    89 
    85 
    90     class kwrepo(repo.__class__):
    86     class kwrepo(repo.__class__):
    91         def file(self, f):
    87         def file(self, f):
    92             if f[0] == '/':
    88             if f[0] == '/':
    93                 f = f[1:]
    89                 f = f[1:]
    94             return filelog.filelog(self.sopener, f, self, self.revlogversion)
    90             return filelog.filelog(self.sopener, f, self, self.revlogversion)
    95 
    91 
       
    92         def commit(self, files=None, text="", user=None, date=None,
       
    93                        match=util.always, force=False, lock=None, wlock=None,
       
    94                        force_editor=False, p1=None, p2=None, extra={}):
       
    95 
       
    96                 commit = []
       
    97                 remove = []
       
    98                 changed = []
       
    99                 use_dirstate = (p1 is None) # not rawcommit
       
   100                 extra = extra.copy()
       
   101 
       
   102                 if use_dirstate:
       
   103                     if files:
       
   104                         for f in files:
       
   105                             s = self.dirstate.state(f)
       
   106                             if s in 'nmai':
       
   107                                 commit.append(f)
       
   108                             elif s == 'r':
       
   109                                 remove.append(f)
       
   110                             else:
       
   111                                 ui.warn(_("%s not tracked!\n") % f)
       
   112                     else:
       
   113                         changes = self.status(match=match)[:5]
       
   114                         modified, added, removed, deleted, unknown = changes
       
   115                         commit = modified + added
       
   116                         remove = removed
       
   117                 else:
       
   118                     commit = files
       
   119 
       
   120                 if use_dirstate:
       
   121                     p1, p2 = self.dirstate.parents()
       
   122                     update_dirstate = True
       
   123                 else:
       
   124                     p1, p2 = p1, p2 or nullid
       
   125                     update_dirstate = (self.dirstate.parents()[0] == p1)
       
   126 
       
   127                 c1 = self.changelog.read(p1)
       
   128                 c2 = self.changelog.read(p2)
       
   129                 m1 = self.manifest.read(c1[0]).copy()
       
   130                 m2 = self.manifest.read(c2[0])
       
   131 
       
   132                 if use_dirstate:
       
   133                     branchname = self.workingctx().branch()
       
   134                     try:
       
   135                         branchname = branchname.decode('UTF-8').encode('UTF-8')
       
   136                     except UnicodeDecodeError:
       
   137                         raise util.Abort(_('branch name not in UTF-8!'))
       
   138                 else:
       
   139                     branchname = ""
       
   140 
       
   141                 if use_dirstate:
       
   142                     oldname = c1[5].get("branch", "") # stored in UTF-8
       
   143                     if not commit and not remove and not force and p2 == nullid and \
       
   144                            branchname == oldname:
       
   145                         ui.status(_("nothing changed\n"))
       
   146                         return None
       
   147 
       
   148                 xp1 = hex(p1)
       
   149                 if p2 == nullid: xp2 = ''
       
   150                 else: xp2 = hex(p2)
       
   151 
       
   152                 self.hook("precommit", throw=True, parent1=xp1, parent2=xp2)
       
   153 
       
   154                 if not wlock:
       
   155                     wlock = self.wlock()
       
   156                 if not lock:
       
   157                     lock = self.lock()
       
   158                 tr = self.transaction()
       
   159 
       
   160                 # check in files
       
   161                 new = {}
       
   162                 linkrev = self.changelog.count()
       
   163                 commit.sort()
       
   164                 is_exec = util.execfunc(self.root, m1.execf)
       
   165                 is_link = util.linkfunc(self.root, m1.linkf)
       
   166                 for f in commit:
       
   167                     ui.note(f + "\n")
       
   168                     try:
       
   169                         new[f] = self.filecommit(f, m1, m2, linkrev, tr, changed)
       
   170                         m1.set(f, is_exec(f), is_link(f))
       
   171                     except OSError:
       
   172                         if use_dirstate:
       
   173                             ui.warn(_("trouble committing %s!\n") % f)
       
   174                             raise
       
   175                         else:
       
   176                             remove.append(f)
       
   177 
       
   178                 # update manifest
       
   179                 m1.update(new)
       
   180                 remove.sort()
       
   181                 removed = []
       
   182 
       
   183                 for f in remove:
       
   184                     if f in m1:
       
   185                         del m1[f]
       
   186                         removed.append(f)
       
   187                 mn = self.manifest.add(m1, tr, linkrev, c1[0], c2[0], (new, removed))
       
   188 
       
   189                 # add changeset
       
   190                 new = new.keys()
       
   191                 new.sort()
       
   192 
       
   193                 user = user or ui.username()
       
   194                 if not text or force_editor:
       
   195                     edittext = []
       
   196                     if text:
       
   197                         edittext.append(text)
       
   198                     edittext.append("")
       
   199                     edittext.append("HG: user: %s" % user)
       
   200                     if p2 != nullid:
       
   201                         edittext.append("HG: branch merge")
       
   202                     edittext.extend(["HG: changed %s" % f for f in changed])
       
   203                     edittext.extend(["HG: removed %s" % f for f in removed])
       
   204                     if not changed and not remove:
       
   205                         edittext.append("HG: no files changed")
       
   206                     edittext.append("")
       
   207                     # run editor in the repository root
       
   208                     olddir = os.getcwd()
       
   209                     os.chdir(self.root)
       
   210                     text = ui.edit("\n".join(edittext), user)
       
   211                     os.chdir(olddir)
       
   212 
       
   213                 lines = [line.rstrip() for line in text.rstrip().splitlines()]
       
   214                 while lines and not lines[0]:
       
   215                     del lines[0]
       
   216                 if not lines:
       
   217                     return None
       
   218                 text = '\n'.join(lines)
       
   219                 if branchname:
       
   220                     extra["branch"] = branchname
       
   221                 n = self.changelog.add(mn, changed + removed, text, tr, p1, p2,
       
   222                                        user, date, extra)
       
   223                 self.hook('pretxncommit', throw=True, node=hex(n), parent1=xp1,
       
   224                           parent2=xp2)
       
   225 
       
   226                 # substitute keywords
       
   227                 for f in kwfmatches(ui, self, changed):
       
   228                     data = self.wfile(f).read()
       
   229                     if not util.binary(data):
       
   230                         data, kwct = re_kw.subn(lambda m:
       
   231                                 kwexpand(m, self, f, changeid=hex(n)),
       
   232                                 data)
       
   233                         if kwct:
       
   234                             ui.debug(_('overwriting %s expanding keywords\n'
       
   235                                 % f))
       
   236                             self.wfile(f, 'w').write(data)
       
   237 
       
   238                 tr.close()
       
   239 
       
   240                 if use_dirstate or update_dirstate:
       
   241                     self.dirstate.setparents(n)
       
   242                     if use_dirstate:
       
   243                         self.dirstate.update(new, "n")
       
   244                         self.dirstate.forget(removed)
       
   245 
       
   246                 self.hook("commit", node=hex(n), parent1=xp1, parent2=xp2)
       
   247                 return n
       
   248 
       
   249     
    96     class kwfilelog(filelog.filelog):
   250     class kwfilelog(filelog.filelog):
    97         def __init__(self, opener, path, repo,
   251         def __init__(self, opener, path, repo,
    98                      defversion=revlog.REVLOG_DEFAULT_VERSION):
   252                      defversion=revlog.REVLOG_DEFAULT_VERSION):
    99             super(kwfilelog, self).__init__(opener, path, defversion)
   253             super(kwfilelog, self).__init__(opener, path, defversion)
   100             self._repo = repo
   254             self._repo = repo
   101             self._path = path
   255             self._path = path
   102 
   256 
   103         def read(self, node):
   257         def read(self, node):
   104             data = super(kwfilelog, self).read(node)
   258             data = super(kwfilelog, self).read(node)
   105             if not self._path.startswith('.hg') and not util.binary(data):
   259             if not util.binary(data) and \
   106                 for mf in kwfmatchers(ui, self._repo):
   260                     kwfmatches(ui, self._repo, [self._path]):
   107                     if mf(self._path):
   261                 ui.debug(_('expanding keywords in %s\n' % self._path))
   108                         ui.debug(_('expanding keywords in %s\n' % self._path))
   262                 return re_kw.sub(lambda m:
   109                         return re_kw.sub(lambda m:
   263                         kwexpand(m, self._repo, self._path,
   110                                 kwexpand(m, self._repo, self._path,
   264                             fileid=node, filelog=self), data)
   111                                     fileid=node, filelog=self),
       
   112                                 data)
       
   113             return data
   265             return data
   114 
   266 
   115         def size(self, rev):
   267         def size(self, rev):
   116             '''Overrides filelog's size() to use kwfilelog.read().'''
   268             '''Overrides filelog's size() to use kwfilelog.read().'''
   117             node = revlog.node(self, rev)
   269             node = revlog.node(self, rev)
   125                 t2 = self.read(node)
   277                 t2 = self.read(node)
   126                 return t2 != text
   278                 return t2 != text
   127 
   279 
   128     filelog.filelog = kwfilelog
   280     filelog.filelog = kwfilelog
   129     repo.__class__ = kwrepo
   281     repo.__class__ = kwrepo
   130 
       
   131 
       
   132 def pretxnkw(ui, repo, hooktype, **args):
       
   133     '''pretxncommit hook that collects candidates for keyword expansion
       
   134     on commit and expands keywords in working dir.'''
       
   135     from mercurial import cmdutil, commands
       
   136     import sys
       
   137 
       
   138     if hooktype != 'pretxncommit':
       
   139         return True
       
   140 
       
   141     cmd, sysargs, globalopts, cmdopts = commands.parse(ui, sys.argv[1:])[1:]
       
   142     if repr(cmd).split()[1] in ('tag', 'import_'):
       
   143         return False
       
   144 
       
   145     files, match, anypats = cmdutil.matchpats(repo, sysargs, cmdopts)
       
   146     modified, added = repo.status(files=files, match=match)[:2]
       
   147     candidates = [f for f in modified + added if not f.startswith('.hg')]
       
   148     if not candidates:
       
   149         return False
       
   150 
       
   151     fmatchers = kwfmatchers(ui, repo)
       
   152     for f in candidates:
       
   153         for mf in fmatchers:
       
   154             if mf(f):
       
   155                 data = repo.wfile(f).read()
       
   156                 if not util.binary(data):
       
   157                     data, kwct = re_kw.subn(lambda m:
       
   158                             kwexpand(m, repo, f, changeid=args['node']),
       
   159                             data)
       
   160                     if kwct:
       
   161                         ui.debug(_('overwriting %s expanding keywords\n' % f))
       
   162                         repo.wfile(f, 'w').write(data)
       
   163                 break