eggs/mercurial-1.7.3-py2.6-linux-x86_64.egg/hgext/keyword.py
changeset 69 c6bca38c1cbf
equal deleted inserted replaced
68:5ff1fc726848 69:c6bca38c1cbf
       
     1 # keyword.py - $Keyword$ expansion for Mercurial
       
     2 #
       
     3 # Copyright 2007-2010 Christian Ebert <blacktrash@gmx.net>
       
     4 #
       
     5 # This software may be used and distributed according to the terms of the
       
     6 # GNU General Public License version 2 or any later version.
       
     7 #
       
     8 # $Id$
       
     9 #
       
    10 # Keyword expansion hack against the grain of a DSCM
       
    11 #
       
    12 # There are many good reasons why this is not needed in a distributed
       
    13 # SCM, still it may be useful in very small projects based on single
       
    14 # files (like LaTeX packages), that are mostly addressed to an
       
    15 # audience not running a version control system.
       
    16 #
       
    17 # For in-depth discussion refer to
       
    18 # <http://mercurial.selenic.com/wiki/KeywordPlan>.
       
    19 #
       
    20 # Keyword expansion is based on Mercurial's changeset template mappings.
       
    21 #
       
    22 # Binary files are not touched.
       
    23 #
       
    24 # Files to act upon/ignore are specified in the [keyword] section.
       
    25 # Customized keyword template mappings in the [keywordmaps] section.
       
    26 #
       
    27 # Run "hg help keyword" and "hg kwdemo" to get info on configuration.
       
    28 
       
    29 '''expand keywords in tracked files
       
    30 
       
    31 This extension expands RCS/CVS-like or self-customized $Keywords$ in
       
    32 tracked text files selected by your configuration.
       
    33 
       
    34 Keywords are only expanded in local repositories and not stored in the
       
    35 change history. The mechanism can be regarded as a convenience for the
       
    36 current user or for archive distribution.
       
    37 
       
    38 Keywords expand to the changeset data pertaining to the latest change
       
    39 relative to the working directory parent of each file.
       
    40 
       
    41 Configuration is done in the [keyword], [keywordset] and [keywordmaps]
       
    42 sections of hgrc files.
       
    43 
       
    44 Example::
       
    45 
       
    46     [keyword]
       
    47     # expand keywords in every python file except those matching "x*"
       
    48     **.py =
       
    49     x*    = ignore
       
    50 
       
    51     [keywordset]
       
    52     # prefer svn- over cvs-like default keywordmaps
       
    53     svn = True
       
    54 
       
    55 .. note::
       
    56    The more specific you are in your filename patterns the less you
       
    57    lose speed in huge repositories.
       
    58 
       
    59 For [keywordmaps] template mapping and expansion demonstration and
       
    60 control run :hg:`kwdemo`. See :hg:`help templates` for a list of
       
    61 available templates and filters.
       
    62 
       
    63 Three additional date template filters are provided::
       
    64 
       
    65     utcdate      "2006/09/18 15:13:13"
       
    66     svnutcdate   "2006-09-18 15:13:13Z"
       
    67     svnisodate   "2006-09-18 08:13:13 -700 (Mon, 18 Sep 2006)"
       
    68 
       
    69 The default template mappings (view with :hg:`kwdemo -d`) can be
       
    70 replaced with customized keywords and templates. Again, run
       
    71 :hg:`kwdemo` to control the results of your configuration changes.
       
    72 
       
    73 Before changing/disabling active keywords, run :hg:`kwshrink` to avoid
       
    74 the risk of inadvertently storing expanded keywords in the change
       
    75 history.
       
    76 
       
    77 To force expansion after enabling it, or a configuration change, run
       
    78 :hg:`kwexpand`.
       
    79 
       
    80 Expansions spanning more than one line and incremental expansions,
       
    81 like CVS' $Log$, are not supported. A keyword template map "Log =
       
    82 {desc}" expands to the first line of the changeset description.
       
    83 '''
       
    84 
       
    85 from mercurial import commands, context, cmdutil, dispatch, filelog, extensions
       
    86 from mercurial import localrepo, match, patch, templatefilters, templater, util
       
    87 from mercurial.hgweb import webcommands
       
    88 from mercurial.i18n import _
       
    89 import os, re, shutil, tempfile
       
    90 
       
    91 commands.optionalrepo += ' kwdemo'
       
    92 
       
    93 # hg commands that do not act on keywords
       
    94 nokwcommands = ('add addremove annotate bundle export grep incoming init log'
       
    95                 ' outgoing push tip verify convert email glog')
       
    96 
       
    97 # hg commands that trigger expansion only when writing to working dir,
       
    98 # not when reading filelog, and unexpand when reading from working dir
       
    99 restricted = 'merge kwexpand kwshrink record qrecord resolve transplant'
       
   100 
       
   101 # names of extensions using dorecord
       
   102 recordextensions = 'record'
       
   103 
       
   104 # date like in cvs' $Date
       
   105 utcdate = lambda x: util.datestr((x[0], 0), '%Y/%m/%d %H:%M:%S')
       
   106 # date like in svn's $Date
       
   107 svnisodate = lambda x: util.datestr(x, '%Y-%m-%d %H:%M:%S %1%2 (%a, %d %b %Y)')
       
   108 # date like in svn's $Id
       
   109 svnutcdate = lambda x: util.datestr((x[0], 0), '%Y-%m-%d %H:%M:%SZ')
       
   110 
       
   111 # make keyword tools accessible
       
   112 kwtools = {'templater': None, 'hgcmd': ''}
       
   113 
       
   114 
       
   115 def _defaultkwmaps(ui):
       
   116     '''Returns default keywordmaps according to keywordset configuration.'''
       
   117     templates = {
       
   118         'Revision': '{node|short}',
       
   119         'Author': '{author|user}',
       
   120     }
       
   121     kwsets = ({
       
   122         'Date': '{date|utcdate}',
       
   123         'RCSfile': '{file|basename},v',
       
   124         'RCSFile': '{file|basename},v', # kept for backwards compatibility
       
   125                                         # with hg-keyword
       
   126         'Source': '{root}/{file},v',
       
   127         'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
       
   128         'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
       
   129     }, {
       
   130         'Date': '{date|svnisodate}',
       
   131         'Id': '{file|basename},v {node|short} {date|svnutcdate} {author|user}',
       
   132         'LastChangedRevision': '{node|short}',
       
   133         'LastChangedBy': '{author|user}',
       
   134         'LastChangedDate': '{date|svnisodate}',
       
   135     })
       
   136     templates.update(kwsets[ui.configbool('keywordset', 'svn')])
       
   137     return templates
       
   138 
       
   139 def _shrinktext(text, subfunc):
       
   140     '''Helper for keyword expansion removal in text.
       
   141     Depending on subfunc also returns number of substitutions.'''
       
   142     return subfunc(r'$\1$', text)
       
   143 
       
   144 def _preselect(wstatus, changed):
       
   145     '''Retrieves modfied and added files from a working directory state
       
   146     and returns the subset of each contained in given changed files
       
   147     retrieved from a change context.'''
       
   148     modified, added = wstatus[:2]
       
   149     modified = [f for f in modified if f in changed]
       
   150     added = [f for f in added if f in changed]
       
   151     return modified, added
       
   152 
       
   153 
       
   154 class kwtemplater(object):
       
   155     '''
       
   156     Sets up keyword templates, corresponding keyword regex, and
       
   157     provides keyword substitution functions.
       
   158     '''
       
   159 
       
   160     def __init__(self, ui, repo, inc, exc):
       
   161         self.ui = ui
       
   162         self.repo = repo
       
   163         self.match = match.match(repo.root, '', [], inc, exc)
       
   164         self.restrict = kwtools['hgcmd'] in restricted.split()
       
   165         self.record = False
       
   166 
       
   167         kwmaps = self.ui.configitems('keywordmaps')
       
   168         if kwmaps: # override default templates
       
   169             self.templates = dict((k, templater.parsestring(v, False))
       
   170                                   for k, v in kwmaps)
       
   171         else:
       
   172             self.templates = _defaultkwmaps(self.ui)
       
   173         escaped = '|'.join(map(re.escape, self.templates.keys()))
       
   174         self.re_kw = re.compile(r'\$(%s)\$' % escaped)
       
   175         self.re_kwexp = re.compile(r'\$(%s): [^$\n\r]*? \$' % escaped)
       
   176 
       
   177         templatefilters.filters.update({'utcdate': utcdate,
       
   178                                         'svnisodate': svnisodate,
       
   179                                         'svnutcdate': svnutcdate})
       
   180 
       
   181     def substitute(self, data, path, ctx, subfunc):
       
   182         '''Replaces keywords in data with expanded template.'''
       
   183         def kwsub(mobj):
       
   184             kw = mobj.group(1)
       
   185             ct = cmdutil.changeset_templater(self.ui, self.repo,
       
   186                                              False, None, '', False)
       
   187             ct.use_template(self.templates[kw])
       
   188             self.ui.pushbuffer()
       
   189             ct.show(ctx, root=self.repo.root, file=path)
       
   190             ekw = templatefilters.firstline(self.ui.popbuffer())
       
   191             return '$%s: %s $' % (kw, ekw)
       
   192         return subfunc(kwsub, data)
       
   193 
       
   194     def expand(self, path, node, data):
       
   195         '''Returns data with keywords expanded.'''
       
   196         if not self.restrict and self.match(path) and not util.binary(data):
       
   197             ctx = self.repo.filectx(path, fileid=node).changectx()
       
   198             return self.substitute(data, path, ctx, self.re_kw.sub)
       
   199         return data
       
   200 
       
   201     def iskwfile(self, cand, ctx):
       
   202         '''Returns subset of candidates which are configured for keyword
       
   203         expansion are not symbolic links.'''
       
   204         return [f for f in cand if self.match(f) and not 'l' in ctx.flags(f)]
       
   205 
       
   206     def overwrite(self, ctx, candidates, lookup, expand, rekw=False):
       
   207         '''Overwrites selected files expanding/shrinking keywords.'''
       
   208         if self.restrict or lookup or self.record: # exclude kw_copy
       
   209             candidates = self.iskwfile(candidates, ctx)
       
   210         if not candidates:
       
   211             return
       
   212         kwcmd = self.restrict and lookup # kwexpand/kwshrink
       
   213         if self.restrict or expand and lookup:
       
   214             mf = ctx.manifest()
       
   215         fctx = ctx
       
   216         subn = (self.restrict or rekw) and self.re_kw.subn or self.re_kwexp.subn
       
   217         msg = (expand and _('overwriting %s expanding keywords\n')
       
   218                or _('overwriting %s shrinking keywords\n'))
       
   219         for f in candidates:
       
   220             if self.restrict:
       
   221                 data = self.repo.file(f).read(mf[f])
       
   222             else:
       
   223                 data = self.repo.wread(f)
       
   224             if util.binary(data):
       
   225                 continue
       
   226             if expand:
       
   227                 if lookup:
       
   228                     fctx = self.repo.filectx(f, fileid=mf[f]).changectx()
       
   229                 data, found = self.substitute(data, f, fctx, subn)
       
   230             elif self.restrict:
       
   231                 found = self.re_kw.search(data)
       
   232             else:
       
   233                 data, found = _shrinktext(data, subn)
       
   234             if found:
       
   235                 self.ui.note(msg % f)
       
   236                 self.repo.wwrite(f, data, ctx.flags(f))
       
   237                 if kwcmd:
       
   238                     self.repo.dirstate.normal(f)
       
   239                 elif self.record:
       
   240                     self.repo.dirstate.normallookup(f)
       
   241 
       
   242     def shrink(self, fname, text):
       
   243         '''Returns text with all keyword substitutions removed.'''
       
   244         if self.match(fname) and not util.binary(text):
       
   245             return _shrinktext(text, self.re_kwexp.sub)
       
   246         return text
       
   247 
       
   248     def shrinklines(self, fname, lines):
       
   249         '''Returns lines with keyword substitutions removed.'''
       
   250         if self.match(fname):
       
   251             text = ''.join(lines)
       
   252             if not util.binary(text):
       
   253                 return _shrinktext(text, self.re_kwexp.sub).splitlines(True)
       
   254         return lines
       
   255 
       
   256     def wread(self, fname, data):
       
   257         '''If in restricted mode returns data read from wdir with
       
   258         keyword substitutions removed.'''
       
   259         return self.restrict and self.shrink(fname, data) or data
       
   260 
       
   261 class kwfilelog(filelog.filelog):
       
   262     '''
       
   263     Subclass of filelog to hook into its read, add, cmp methods.
       
   264     Keywords are "stored" unexpanded, and processed on reading.
       
   265     '''
       
   266     def __init__(self, opener, kwt, path):
       
   267         super(kwfilelog, self).__init__(opener, path)
       
   268         self.kwt = kwt
       
   269         self.path = path
       
   270 
       
   271     def read(self, node):
       
   272         '''Expands keywords when reading filelog.'''
       
   273         data = super(kwfilelog, self).read(node)
       
   274         if self.renamed(node):
       
   275             return data
       
   276         return self.kwt.expand(self.path, node, data)
       
   277 
       
   278     def add(self, text, meta, tr, link, p1=None, p2=None):
       
   279         '''Removes keyword substitutions when adding to filelog.'''
       
   280         text = self.kwt.shrink(self.path, text)
       
   281         return super(kwfilelog, self).add(text, meta, tr, link, p1, p2)
       
   282 
       
   283     def cmp(self, node, text):
       
   284         '''Removes keyword substitutions for comparison.'''
       
   285         text = self.kwt.shrink(self.path, text)
       
   286         return super(kwfilelog, self).cmp(node, text)
       
   287 
       
   288 def _status(ui, repo, kwt, *pats, **opts):
       
   289     '''Bails out if [keyword] configuration is not active.
       
   290     Returns status of working directory.'''
       
   291     if kwt:
       
   292         return repo.status(match=cmdutil.match(repo, pats, opts), clean=True,
       
   293                            unknown=opts.get('unknown') or opts.get('all'))
       
   294     if ui.configitems('keyword'):
       
   295         raise util.Abort(_('[keyword] patterns cannot match'))
       
   296     raise util.Abort(_('no [keyword] patterns configured'))
       
   297 
       
   298 def _kwfwrite(ui, repo, expand, *pats, **opts):
       
   299     '''Selects files and passes them to kwtemplater.overwrite.'''
       
   300     wctx = repo[None]
       
   301     if len(wctx.parents()) > 1:
       
   302         raise util.Abort(_('outstanding uncommitted merge'))
       
   303     kwt = kwtools['templater']
       
   304     wlock = repo.wlock()
       
   305     try:
       
   306         status = _status(ui, repo, kwt, *pats, **opts)
       
   307         modified, added, removed, deleted, unknown, ignored, clean = status
       
   308         if modified or added or removed or deleted:
       
   309             raise util.Abort(_('outstanding uncommitted changes'))
       
   310         kwt.overwrite(wctx, clean, True, expand)
       
   311     finally:
       
   312         wlock.release()
       
   313 
       
   314 def demo(ui, repo, *args, **opts):
       
   315     '''print [keywordmaps] configuration and an expansion example
       
   316 
       
   317     Show current, custom, or default keyword template maps and their
       
   318     expansions.
       
   319 
       
   320     Extend the current configuration by specifying maps as arguments
       
   321     and using -f/--rcfile to source an external hgrc file.
       
   322 
       
   323     Use -d/--default to disable current configuration.
       
   324 
       
   325     See :hg:`help templates` for information on templates and filters.
       
   326     '''
       
   327     def demoitems(section, items):
       
   328         ui.write('[%s]\n' % section)
       
   329         for k, v in sorted(items):
       
   330             ui.write('%s = %s\n' % (k, v))
       
   331 
       
   332     fn = 'demo.txt'
       
   333     tmpdir = tempfile.mkdtemp('', 'kwdemo.')
       
   334     ui.note(_('creating temporary repository at %s\n') % tmpdir)
       
   335     repo = localrepo.localrepository(ui, tmpdir, True)
       
   336     ui.setconfig('keyword', fn, '')
       
   337 
       
   338     uikwmaps = ui.configitems('keywordmaps')
       
   339     if args or opts.get('rcfile'):
       
   340         ui.status(_('\n\tconfiguration using custom keyword template maps\n'))
       
   341         if uikwmaps:
       
   342             ui.status(_('\textending current template maps\n'))
       
   343         if opts.get('default') or not uikwmaps:
       
   344             ui.status(_('\toverriding default template maps\n'))
       
   345         if opts.get('rcfile'):
       
   346             ui.readconfig(opts.get('rcfile'))
       
   347         if args:
       
   348             # simulate hgrc parsing
       
   349             rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
       
   350             fp = repo.opener('hgrc', 'w')
       
   351             fp.writelines(rcmaps)
       
   352             fp.close()
       
   353             ui.readconfig(repo.join('hgrc'))
       
   354         kwmaps = dict(ui.configitems('keywordmaps'))
       
   355     elif opts.get('default'):
       
   356         ui.status(_('\n\tconfiguration using default keyword template maps\n'))
       
   357         kwmaps = _defaultkwmaps(ui)
       
   358         if uikwmaps:
       
   359             ui.status(_('\tdisabling current template maps\n'))
       
   360             for k, v in kwmaps.iteritems():
       
   361                 ui.setconfig('keywordmaps', k, v)
       
   362     else:
       
   363         ui.status(_('\n\tconfiguration using current keyword template maps\n'))
       
   364         kwmaps = dict(uikwmaps) or _defaultkwmaps(ui)
       
   365 
       
   366     uisetup(ui)
       
   367     reposetup(ui, repo)
       
   368     ui.write('[extensions]\nkeyword =\n')
       
   369     demoitems('keyword', ui.configitems('keyword'))
       
   370     demoitems('keywordmaps', kwmaps.iteritems())
       
   371     keywords = '$' + '$\n$'.join(sorted(kwmaps.keys())) + '$\n'
       
   372     repo.wopener(fn, 'w').write(keywords)
       
   373     repo[None].add([fn])
       
   374     ui.note(_('\nkeywords written to %s:\n') % fn)
       
   375     ui.note(keywords)
       
   376     repo.dirstate.setbranch('demobranch')
       
   377     for name, cmd in ui.configitems('hooks'):
       
   378         if name.split('.', 1)[0].find('commit') > -1:
       
   379             repo.ui.setconfig('hooks', name, '')
       
   380     msg = _('hg keyword configuration and expansion example')
       
   381     ui.note("hg ci -m '%s'\n" % msg)
       
   382     repo.commit(text=msg)
       
   383     ui.status(_('\n\tkeywords expanded\n'))
       
   384     ui.write(repo.wread(fn))
       
   385     shutil.rmtree(tmpdir, ignore_errors=True)
       
   386 
       
   387 def expand(ui, repo, *pats, **opts):
       
   388     '''expand keywords in the working directory
       
   389 
       
   390     Run after (re)enabling keyword expansion.
       
   391 
       
   392     kwexpand refuses to run if given files contain local changes.
       
   393     '''
       
   394     # 3rd argument sets expansion to True
       
   395     _kwfwrite(ui, repo, True, *pats, **opts)
       
   396 
       
   397 def files(ui, repo, *pats, **opts):
       
   398     '''show files configured for keyword expansion
       
   399 
       
   400     List which files in the working directory are matched by the
       
   401     [keyword] configuration patterns.
       
   402 
       
   403     Useful to prevent inadvertent keyword expansion and to speed up
       
   404     execution by including only files that are actual candidates for
       
   405     expansion.
       
   406 
       
   407     See :hg:`help keyword` on how to construct patterns both for
       
   408     inclusion and exclusion of files.
       
   409 
       
   410     With -A/--all and -v/--verbose the codes used to show the status
       
   411     of files are::
       
   412 
       
   413       K = keyword expansion candidate
       
   414       k = keyword expansion candidate (not tracked)
       
   415       I = ignored
       
   416       i = ignored (not tracked)
       
   417     '''
       
   418     kwt = kwtools['templater']
       
   419     status = _status(ui, repo, kwt, *pats, **opts)
       
   420     cwd = pats and repo.getcwd() or ''
       
   421     modified, added, removed, deleted, unknown, ignored, clean = status
       
   422     files = []
       
   423     if not opts.get('unknown') or opts.get('all'):
       
   424         files = sorted(modified + added + clean)
       
   425     wctx = repo[None]
       
   426     kwfiles = kwt.iskwfile(files, wctx)
       
   427     kwunknown = kwt.iskwfile(unknown, wctx)
       
   428     if not opts.get('ignore') or opts.get('all'):
       
   429         showfiles = kwfiles, kwunknown
       
   430     else:
       
   431         showfiles = [], []
       
   432     if opts.get('all') or opts.get('ignore'):
       
   433         showfiles += ([f for f in files if f not in kwfiles],
       
   434                       [f for f in unknown if f not in kwunknown])
       
   435     for char, filenames in zip('KkIi', showfiles):
       
   436         fmt = (opts.get('all') or ui.verbose) and '%s %%s\n' % char or '%s\n'
       
   437         for f in filenames:
       
   438             ui.write(fmt % repo.pathto(f, cwd))
       
   439 
       
   440 def shrink(ui, repo, *pats, **opts):
       
   441     '''revert expanded keywords in the working directory
       
   442 
       
   443     Run before changing/disabling active keywords or if you experience
       
   444     problems with :hg:`import` or :hg:`merge`.
       
   445 
       
   446     kwshrink refuses to run if given files contain local changes.
       
   447     '''
       
   448     # 3rd argument sets expansion to False
       
   449     _kwfwrite(ui, repo, False, *pats, **opts)
       
   450 
       
   451 
       
   452 def uisetup(ui):
       
   453     ''' Monkeypatches dispatch._parse to retrieve user command.'''
       
   454 
       
   455     def kwdispatch_parse(orig, ui, args):
       
   456         '''Monkeypatch dispatch._parse to obtain running hg command.'''
       
   457         cmd, func, args, options, cmdoptions = orig(ui, args)
       
   458         kwtools['hgcmd'] = cmd
       
   459         return cmd, func, args, options, cmdoptions
       
   460 
       
   461     extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse)
       
   462 
       
   463 def reposetup(ui, repo):
       
   464     '''Sets up repo as kwrepo for keyword substitution.
       
   465     Overrides file method to return kwfilelog instead of filelog
       
   466     if file matches user configuration.
       
   467     Wraps commit to overwrite configured files with updated
       
   468     keyword substitutions.
       
   469     Monkeypatches patch and webcommands.'''
       
   470 
       
   471     try:
       
   472         if (not repo.local() or kwtools['hgcmd'] in nokwcommands.split()
       
   473             or '.hg' in util.splitpath(repo.root)
       
   474             or repo._url.startswith('bundle:')):
       
   475             return
       
   476     except AttributeError:
       
   477         pass
       
   478 
       
   479     inc, exc = [], ['.hg*']
       
   480     for pat, opt in ui.configitems('keyword'):
       
   481         if opt != 'ignore':
       
   482             inc.append(pat)
       
   483         else:
       
   484             exc.append(pat)
       
   485     if not inc:
       
   486         return
       
   487 
       
   488     kwtools['templater'] = kwt = kwtemplater(ui, repo, inc, exc)
       
   489 
       
   490     class kwrepo(repo.__class__):
       
   491         def file(self, f):
       
   492             if f[0] == '/':
       
   493                 f = f[1:]
       
   494             return kwfilelog(self.sopener, kwt, f)
       
   495 
       
   496         def wread(self, filename):
       
   497             data = super(kwrepo, self).wread(filename)
       
   498             return kwt.wread(filename, data)
       
   499 
       
   500         def commit(self, *args, **opts):
       
   501             # use custom commitctx for user commands
       
   502             # other extensions can still wrap repo.commitctx directly
       
   503             self.commitctx = self.kwcommitctx
       
   504             try:
       
   505                 return super(kwrepo, self).commit(*args, **opts)
       
   506             finally:
       
   507                 del self.commitctx
       
   508 
       
   509         def kwcommitctx(self, ctx, error=False):
       
   510             n = super(kwrepo, self).commitctx(ctx, error)
       
   511             # no lock needed, only called from repo.commit() which already locks
       
   512             if not kwt.record:
       
   513                 restrict = kwt.restrict
       
   514                 kwt.restrict = True
       
   515                 kwt.overwrite(self[n], sorted(ctx.added() + ctx.modified()),
       
   516                               False, True)
       
   517                 kwt.restrict = restrict
       
   518             return n
       
   519 
       
   520         def rollback(self, dryrun=False):
       
   521             wlock = self.wlock()
       
   522             try:
       
   523                 if not dryrun:
       
   524                     changed = self['.'].files()
       
   525                 ret = super(kwrepo, self).rollback(dryrun)
       
   526                 if not dryrun:
       
   527                     ctx = self['.']
       
   528                     modified, added = _preselect(self[None].status(), changed)
       
   529                     kwt.overwrite(ctx, modified, True, True)
       
   530                     kwt.overwrite(ctx, added, True, False)
       
   531                 return ret
       
   532             finally:
       
   533                 wlock.release()
       
   534 
       
   535     # monkeypatches
       
   536     def kwpatchfile_init(orig, self, ui, fname, opener,
       
   537                          missing=False, eolmode=None):
       
   538         '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
       
   539         rejects or conflicts due to expanded keywords in working dir.'''
       
   540         orig(self, ui, fname, opener, missing, eolmode)
       
   541         # shrink keywords read from working dir
       
   542         self.lines = kwt.shrinklines(self.fname, self.lines)
       
   543 
       
   544     def kw_diff(orig, repo, node1=None, node2=None, match=None, changes=None,
       
   545                 opts=None, prefix=''):
       
   546         '''Monkeypatch patch.diff to avoid expansion.'''
       
   547         kwt.restrict = True
       
   548         return orig(repo, node1, node2, match, changes, opts, prefix)
       
   549 
       
   550     def kwweb_skip(orig, web, req, tmpl):
       
   551         '''Wraps webcommands.x turning off keyword expansion.'''
       
   552         kwt.match = util.never
       
   553         return orig(web, req, tmpl)
       
   554 
       
   555     def kw_copy(orig, ui, repo, pats, opts, rename=False):
       
   556         '''Wraps cmdutil.copy so that copy/rename destinations do not
       
   557         contain expanded keywords.
       
   558         Note that the source of a regular file destination may also be a
       
   559         symlink:
       
   560         hg cp sym x                -> x is symlink
       
   561         cp sym x; hg cp -A sym x   -> x is file (maybe expanded keywords)
       
   562         For the latter we have to follow the symlink to find out whether its
       
   563         target is configured for expansion and we therefore must unexpand the
       
   564         keywords in the destination.'''
       
   565         orig(ui, repo, pats, opts, rename)
       
   566         if opts.get('dry_run'):
       
   567             return
       
   568         wctx = repo[None]
       
   569         cwd = repo.getcwd()
       
   570 
       
   571         def haskwsource(dest):
       
   572             '''Returns true if dest is a regular file and configured for
       
   573             expansion or a symlink which points to a file configured for
       
   574             expansion. '''
       
   575             source = repo.dirstate.copied(dest)
       
   576             if 'l' in wctx.flags(source):
       
   577                 source = util.canonpath(repo.root, cwd,
       
   578                                         os.path.realpath(source))
       
   579             return kwt.match(source)
       
   580 
       
   581         candidates = [f for f in repo.dirstate.copies() if
       
   582                       not 'l' in wctx.flags(f) and haskwsource(f)]
       
   583         kwt.overwrite(wctx, candidates, False, False)
       
   584 
       
   585     def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts):
       
   586         '''Wraps record.dorecord expanding keywords after recording.'''
       
   587         wlock = repo.wlock()
       
   588         try:
       
   589             # record returns 0 even when nothing has changed
       
   590             # therefore compare nodes before and after
       
   591             kwt.record = True
       
   592             ctx = repo['.']
       
   593             wstatus = repo[None].status()
       
   594             ret = orig(ui, repo, commitfunc, *pats, **opts)
       
   595             recctx = repo['.']
       
   596             if ctx != recctx:
       
   597                 modified, added = _preselect(wstatus, recctx.files())
       
   598                 kwt.restrict = False
       
   599                 kwt.overwrite(recctx, modified, False, True)
       
   600                 kwt.overwrite(recctx, added, False, True, True)
       
   601                 kwt.restrict = True
       
   602             return ret
       
   603         finally:
       
   604             wlock.release()
       
   605 
       
   606     repo.__class__ = kwrepo
       
   607 
       
   608     def kwfilectx_cmp(orig, self, fctx):
       
   609         # keyword affects data size, comparing wdir and filelog size does
       
   610         # not make sense
       
   611         if (fctx._filerev is None and
       
   612             (self._repo._encodefilterpats or
       
   613              kwt.match(fctx.path()) and not 'l' in fctx.flags()) or
       
   614             self.size() == fctx.size()):
       
   615             return self._filelog.cmp(self._filenode, fctx.data())
       
   616         return True
       
   617 
       
   618     extensions.wrapfunction(context.filectx, 'cmp', kwfilectx_cmp)
       
   619     extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init)
       
   620     extensions.wrapfunction(patch, 'diff', kw_diff)
       
   621     extensions.wrapfunction(cmdutil, 'copy', kw_copy)
       
   622     for c in 'annotate changeset rev filediff diff'.split():
       
   623         extensions.wrapfunction(webcommands, c, kwweb_skip)
       
   624     for name in recordextensions.split():
       
   625         try:
       
   626             record = extensions.find(name)
       
   627             extensions.wrapfunction(record, 'dorecord', kw_dorecord)
       
   628         except KeyError:
       
   629             pass
       
   630 
       
   631 cmdtable = {
       
   632     'kwdemo':
       
   633         (demo,
       
   634          [('d', 'default', None, _('show default keyword template maps')),
       
   635           ('f', 'rcfile', '',
       
   636            _('read maps from rcfile'), _('FILE'))],
       
   637          _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')),
       
   638     'kwexpand': (expand, commands.walkopts,
       
   639                  _('hg kwexpand [OPTION]... [FILE]...')),
       
   640     'kwfiles':
       
   641         (files,
       
   642          [('A', 'all', None, _('show keyword status flags of all files')),
       
   643           ('i', 'ignore', None, _('show files excluded from expansion')),
       
   644           ('u', 'unknown', None, _('only show unknown (not tracked) files')),
       
   645          ] + commands.walkopts,
       
   646          _('hg kwfiles [OPTION]... [FILE]...')),
       
   647     'kwshrink': (shrink, commands.walkopts,
       
   648                  _('hg kwshrink [OPTION]... [FILE]...')),
       
   649 }