hgkw/keyword.py
branchkwmap-templates
changeset 95 9e4cbe64fb4f
parent 93 9f70f953dd3b
child 97 9353e7ce6d9b
equal deleted inserted replaced
93:9f70f953dd3b 95:9e4cbe64fb4f
     9 There are many good reasons why this is not needed in a distributed
     9 There are many good reasons why this is not needed in a distributed
    10 SCM, still it may be useful in very small projects based on single
    10 SCM, still it may be useful in very small projects based on single
    11 files (like LaTeX packages), that are mostly addressed to an audience
    11 files (like LaTeX packages), that are mostly addressed to an audience
    12 not running a version control system.
    12 not running a version control system.
    13 
    13 
    14 Supported $keywords$ and their $keyword: substition $ are:
    14 You can either use the default cvs-like keywords or provide your
       
    15 own in hgrc.
       
    16 
       
    17 Default $keywords$ and their $keyword: substition $ are:
    15     Revision: changeset id
    18     Revision: changeset id
    16     Author:   short username
    19     Author:   username
    17     Date:     %Y/%m/%d %H:%M:%S [UTC]
    20     Date:     %Y/%m/%d %H:%M:%S [UTC]
    18     RCSFile:  basename,v
    21     RCSFile:  basename,v
    19     Source:   /path/to/basename,v
    22     Source:   /path/to/basename,v
    20     Id:       basename,v csetid %Y/%m/%d %H:%M:%S shortname
    23     Id:       basename,v csetid %Y/%m/%d %H:%M:%S username
    21     Header:   /path/to/basename,v csetid %Y/%m/%d %H:%M:%S shortname
    24     Header:   /path/to/basename,v csetid %Y/%m/%d %H:%M:%S username
    22 
    25 
    23 Simple setup in hgrc:
    26 Simple setup in hgrc:
    24 
    27 
    25     # enable extension
    28     # enable extension
    26     hgext.keyword =
    29     hgext.keyword = /full/path/to/script
    27     # or, if script not in hgext folder:
    30     # or, if script in hgext folder:
    28     # hgext.keyword = /full/path/to/script
    31     # hgext.keyword =
    29     
    32     
    30     # filename patterns for expansion are configured in this section
    33     # filename patterns for expansion are configured in this section
    31     [keyword]
    34     [keyword]
    32     **.py = expand
    35     **.py = expand
    33     ...
    36     ...
       
    37     # in case you prefer you own keyword maps over the cvs-like defaults:
       
    38     [keywordmaps]
       
    39     hgdate = {date|rfc822date}
       
    40     lastlog = {desc}
       
    41     checked in by = {author}
       
    42     ...
    34 '''
    43 '''
    35 
    44 
    36 from mercurial import context, util
    45 from mercurial import cmdutil, templater, util
    37 import os.path, re, sys, time
    46 import os.path, re, sys, time
    38 
    47 
    39 re_kw = re.compile(
    48 deftemplates = {
    40         r'\$(Id|Header|Author|Date|Revision|RCSFile|Source)[^$]*?\$')
    49         'Revision': '{node|short}',
    41 
    50         'Author': '{author|user}',
    42 def kwexpand(mobj, kwfctx):
    51         'Date': '{date|utcdate}',
    43     '''Called by kwfilelog.read and pretxnkw.
    52         'RCSFile': '{file|basename},v',
    44     Expands keywords according to file context.'''
    53         'Source': '{root}/{file},v',
    45     return '$%s: %s $' % (mobj.group(1), kwfctx.expand(mobj.group(1)))
    54         'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
       
    55         'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
       
    56         }
       
    57 
       
    58 def utcdate(date):
       
    59     '''Returns hgdate in cvs-like UTC format.'''
       
    60     return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0]))
       
    61 
       
    62 templater.common_filters['utcdate'] = utcdate
    46 
    63 
    47 def kwfmatches(ui, repo, files):
    64 def kwfmatches(ui, repo, files):
    48     '''Selects candidates for keyword substitution
    65     '''Selects candidates for keyword substitution
    49     configured in keyword section in hgrc.'''
    66     configured in keyword section in hgrc.'''
    50     files = [f for f in files if not f.startswith('.hg')]
    67     files = [f for f in files if not f.startswith('.hg')]
    58             if mf(f):
    75             if mf(f):
    59                 candidates.append(f)
    76                 candidates.append(f)
    60                 break
    77                 break
    61     return candidates
    78     return candidates
    62 
    79 
    63 def utcdate(date):
    80 
    64     '''Returns hgdate in cvs-like UTC format.'''
    81 class kwtemplater(object):
    65     return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0]))
       
    66 
       
    67 
       
    68 class kwfilectx(context.filectx):
       
    69     '''
    82     '''
    70     Provides keyword expansion functions based on file context.
    83     Sets up keyword templates, corresponding keyword regex, and
       
    84     provides keyword expansion function.
    71     '''
    85     '''
    72     def __init__(self, repo, path, changeid=None, fileid=None, filelog=None):
    86     def __init__(self, ui, repo, node=None):
    73         context.filectx.__init__(self, repo, path, changeid, fileid, filelog)
    87         self.ui = ui
    74     def Revision(self):
    88         self.repo = repo
    75         return str(self.changectx())
    89         self.node = node 
    76     def Author(self):
    90         templates = {}
    77         return util.shortuser(self.user())
    91         for k, v in self.ui.configitems('keywordmaps'):
    78     def Date(self):
    92             templates[k] = v
    79         return utcdate(self.date())
    93         self.templates = templates or deftemplates
    80     def RCSFile(self):
    94         self.re_kw = re.compile(r'\$(%s)[^$]*?\$' %
    81         return os.path.basename(self._path)+',v'
    95                 '|'.join(re.escape(k) for k in self.templates.keys()))
    82     def Source(self):
    96         self.t = cmdutil.changeset_templater(self.ui, self.repo,
    83         return self._repo.wjoin(self._path)+',v'
    97                 False, '', False)
    84     def Header(self):
    98 
    85         return ' '.join(
    99     def expand(self, mobj, path):
    86                 [self.Source(), self.Revision(), self.Date(), self.Author()])
   100         kw = mobj.group(1)
    87     def Id(self):
   101         template = templater.parsestring(self.templates[kw], quoted=False)
    88         return ' '.join(
   102         self.t.use_template(template)
    89                 [self.RCSFile(), self.Revision(), self.Date(), self.Author()])
   103         self.ui.pushbuffer()
    90     def expand(self, kw):
   104         self.t.show(changenode=self.node,
    91         '''Called from kwexpand, evaluates keyword.'''
   105                 changes=self.repo.changelog.read(self.node),
    92         return eval('self.%s()' % kw)
   106                 root=self.repo.root, file=path)
       
   107         return '$%s: %s $' % (kw, self.ui.popbuffer())
    93 
   108 
    94 
   109 
    95 def reposetup(ui, repo):
   110 def reposetup(ui, repo):
    96     from mercurial import filelog, revlog
   111     from mercurial import context, filelog, revlog
    97 
       
    98     if not repo.local():
   112     if not repo.local():
    99         return
   113         return
   100 
   114 
   101     class kwrepo(repo.__class__):
   115     class kwrepo(repo.__class__):
   102         def file(self, f):
   116         def file(self, f):
   112         def __init__(self, opener, path, repo,
   126         def __init__(self, opener, path, repo,
   113                      defversion=revlog.REVLOG_DEFAULT_VERSION):
   127                      defversion=revlog.REVLOG_DEFAULT_VERSION):
   114             super(kwfilelog, self).__init__(opener, path, defversion)
   128             super(kwfilelog, self).__init__(opener, path, defversion)
   115             self._repo = repo
   129             self._repo = repo
   116             self._path = path
   130             self._path = path
       
   131             self.kwt = kwtemplater(ui, self._repo)
   117 
   132 
   118         def iskwcandidate(self, data):
   133         def iskwcandidate(self, data):
   119             '''Decides whether to act on keywords.'''
   134             '''Decides whether to act on keywords.'''
   120             return (kwfmatches(ui, self._repo, [self._path])
   135             return (kwfmatches(ui, self._repo, [self._path])
   121                     and not util.binary(data))
   136                     and not util.binary(data))
   122 
   137 
   123         def read(self, node):
   138         def read(self, node):
   124             '''Substitutes keywords when reading filelog.'''
   139             '''Substitutes keywords when reading filelog.'''
   125             data = super(kwfilelog, self).read(node)
   140             data = super(kwfilelog, self).read(node)
   126             if self.iskwcandidate(data):
   141             if self.iskwcandidate(data):
   127                 kwfctx = kwfilectx(self._repo, self._path,
   142                 c = context.filectx(self._repo, self._path,
   128                             fileid=node, filelog=self)
   143                          fileid=node, filelog=self)
   129                 return re_kw.sub(lambda m: kwexpand(m, kwfctx), data)
   144                 self.kwt.node = c.node()
       
   145                 return self.kwt.re_kw.sub(lambda m:
       
   146                         self.kwt.expand(m, self._path), data)
   130             return data
   147             return data
   131 
   148 
   132         def add(self, text, meta, tr, link, p1=None, p2=None):
   149         def add(self, text, meta, tr, link, p1=None, p2=None):
   133             '''Removes keyword substitutions when adding to filelog.'''
   150             '''Removes keyword substitutions when adding to filelog.'''
   134             if self.iskwcandidate(text):
   151             if self.iskwcandidate(text):
   135                 text = re_kw.sub(r'$\1$', text)
   152                 text = self.kwt.re_kw.sub(r'$\1$', text)
   136             return super(kwfilelog, self).add(text,
   153             return super(kwfilelog, self).add(text,
   137                     meta, tr, link, p1=p1, p2=p2)
   154                     meta, tr, link, p1=p1, p2=p2)
   138 
   155 
   139         def cmp(self, node, text):
   156         def cmp(self, node, text):
   140             '''Removes keyword substitutions for comparison.'''
   157             '''Removes keyword substitutions for comparison.'''
   141             if self.iskwcandidate(text):
   158             if self.iskwcandidate(text):
   142                 text = re_kw.sub(r'$\1$', text)
   159                 text = self.kwt.re_kw.sub(r'$\1$', text)
   143             return super(kwfilelog, self).cmp(node, text)
   160             return super(kwfilelog, self).cmp(node, text)
   144 
   161 
   145     filelog.filelog = kwfilelog
   162     filelog.filelog = kwfilelog
   146     repo.__class__ = kwrepo
   163     repo.__class__ = kwrepo
   147     # make pretxncommit hook import kwmodule regardless of where it's located
   164     # make pretxncommit hook import kwmodule regardless of where it's located
   165     on commit and expands keywords in working dir.'''
   182     on commit and expands keywords in working dir.'''
   166     from mercurial.i18n import gettext as _
   183     from mercurial.i18n import gettext as _
   167     # above line for backwards compatibility; can be changed to
   184     # above line for backwards compatibility; can be changed to
   168     #   from mercurial.i18n import _
   185     #   from mercurial.i18n import _
   169     # some day
   186     # some day
   170     from mercurial import cmdutil, commands
   187     from mercurial import commands
   171 
   188 
   172     if hooktype != 'pretxncommit':
   189     if hooktype != 'pretxncommit':
   173         return True
   190         return True
   174 
   191 
   175     cmd, sysargs, globalopts, cmdopts = commands.parse(ui, sys.argv[1:])[1:]
   192     cmd, sysargs, globalopts, cmdopts = commands.parse(ui, sys.argv[1:])[1:]
   176     if repr(cmd).split()[1] in ('tag', 'import_'):
   193     if repr(cmd).split()[1] in ('tag', 'import_'):
   177         return False
   194         return False
   178 
   195 
   179     files, match, anypats = cmdutil.matchpats(repo, sysargs, cmdopts)
   196     files, match, anypats = cmdutil.matchpats(repo, sysargs, cmdopts)
   180     modified, added = repo.status(files=files, match=match)[:2]
   197     modified, added = repo.status(files=files, match=match)[:2]
   181 
   198     candidates = kwfmatches(ui, repo, modified+added)
   182     for f in kwfmatches(ui, repo, modified+added):
   199     if not candidates:
       
   200         return False
       
   201 
       
   202     kwt = kwtemplater(ui, repo, node=repo.changelog.tip())
       
   203     for f in candidates:
   183         data = repo.wfile(f).read()
   204         data = repo.wfile(f).read()
   184         if not util.binary(data):
   205         if not util.binary(data):
   185             kwfctx = kwfilectx(repo, f, changeid=args['node'])
   206             data, kwct = kwt.re_kw.subn(lambda m: kwt.expand(m, f), data)
   186             data, kwct = re_kw.subn(lambda m: kwexpand(m, kwfctx), data)
       
   187             if kwct:
   207             if kwct:
   188                 ui.debug(_('overwriting %s expanding keywords\n' % f))
   208                 ui.debug(_('overwriting %s expanding keywords\n' % f))
   189                 repo.wfile(f, 'w').write(data)
   209                 repo.wfile(f, 'w').write(data)