hgext/keyword.py
changeset 45942 89a2afe31e82
parent 43506 9f70512ae2cf
child 47711 b492bc018011
equal deleted inserted replaced
45941:346af7687c6f 45942:89a2afe31e82
   156 
   156 
   157 configtable = {}
   157 configtable = {}
   158 configitem = registrar.configitem(configtable)
   158 configitem = registrar.configitem(configtable)
   159 
   159 
   160 configitem(
   160 configitem(
   161     b'keywordset', b'svn', default=False,
   161     b'keywordset',
       
   162     b'svn',
       
   163     default=False,
   162 )
   164 )
   163 # date like in cvs' $Date
   165 # date like in cvs' $Date
   164 @templatefilter(b'utcdate', intype=templateutil.date)
   166 @templatefilter(b'utcdate', intype=templateutil.date)
   165 def utcdate(date):
   167 def utcdate(date):
   166     '''Date. Returns a UTC-date in this format: "2009/08/18 11:00:13".
   168     """Date. Returns a UTC-date in this format: "2009/08/18 11:00:13"."""
   167     '''
       
   168     dateformat = b'%Y/%m/%d %H:%M:%S'
   169     dateformat = b'%Y/%m/%d %H:%M:%S'
   169     return dateutil.datestr((date[0], 0), dateformat)
   170     return dateutil.datestr((date[0], 0), dateformat)
   170 
   171 
   171 
   172 
   172 # date like in svn's $Date
   173 # date like in svn's $Date
   173 @templatefilter(b'svnisodate', intype=templateutil.date)
   174 @templatefilter(b'svnisodate', intype=templateutil.date)
   174 def svnisodate(date):
   175 def svnisodate(date):
   175     '''Date. Returns a date in this format: "2009-08-18 13:00:13
   176     """Date. Returns a date in this format: "2009-08-18 13:00:13
   176     +0200 (Tue, 18 Aug 2009)".
   177     +0200 (Tue, 18 Aug 2009)".
   177     '''
   178     """
   178     return dateutil.datestr(date, b'%Y-%m-%d %H:%M:%S %1%2 (%a, %d %b %Y)')
   179     return dateutil.datestr(date, b'%Y-%m-%d %H:%M:%S %1%2 (%a, %d %b %Y)')
   179 
   180 
   180 
   181 
   181 # date like in svn's $Id
   182 # date like in svn's $Id
   182 @templatefilter(b'svnutcdate', intype=templateutil.date)
   183 @templatefilter(b'svnutcdate', intype=templateutil.date)
   183 def svnutcdate(date):
   184 def svnutcdate(date):
   184     '''Date. Returns a UTC-date in this format: "2009-08-18
   185     """Date. Returns a UTC-date in this format: "2009-08-18
   185     11:00:13Z".
   186     11:00:13Z".
   186     '''
   187     """
   187     dateformat = b'%Y-%m-%d %H:%M:%SZ'
   188     dateformat = b'%Y-%m-%d %H:%M:%SZ'
   188     return dateutil.datestr((date[0], 0), dateformat)
   189     return dateutil.datestr((date[0], 0), dateformat)
   189 
   190 
   190 
   191 
   191 # make keyword tools accessible
   192 # make keyword tools accessible
   219     templates.update(kwsets[ui.configbool(b'keywordset', b'svn')])
   220     templates.update(kwsets[ui.configbool(b'keywordset', b'svn')])
   220     return templates
   221     return templates
   221 
   222 
   222 
   223 
   223 def _shrinktext(text, subfunc):
   224 def _shrinktext(text, subfunc):
   224     '''Helper for keyword expansion removal in text.
   225     """Helper for keyword expansion removal in text.
   225     Depending on subfunc also returns number of substitutions.'''
   226     Depending on subfunc also returns number of substitutions."""
   226     return subfunc(br'$\1$', text)
   227     return subfunc(br'$\1$', text)
   227 
   228 
   228 
   229 
   229 def _preselect(wstatus, changed):
   230 def _preselect(wstatus, changed):
   230     '''Retrieves modified and added files from a working directory state
   231     """Retrieves modified and added files from a working directory state
   231     and returns the subset of each contained in given changed files
   232     and returns the subset of each contained in given changed files
   232     retrieved from a change context.'''
   233     retrieved from a change context."""
   233     modified = [f for f in wstatus.modified if f in changed]
   234     modified = [f for f in wstatus.modified if f in changed]
   234     added = [f for f in wstatus.added if f in changed]
   235     added = [f for f in wstatus.added if f in changed]
   235     return modified, added
   236     return modified, added
   236 
   237 
   237 
   238 
   238 class kwtemplater(object):
   239 class kwtemplater(object):
   239     '''
   240     """
   240     Sets up keyword templates, corresponding keyword regex, and
   241     Sets up keyword templates, corresponding keyword regex, and
   241     provides keyword substitution functions.
   242     provides keyword substitution functions.
   242     '''
   243     """
   243 
   244 
   244     def __init__(self, ui, repo, inc, exc):
   245     def __init__(self, ui, repo, inc, exc):
   245         self.ui = ui
   246         self.ui = ui
   246         self._repo = weakref.ref(repo)
   247         self._repo = weakref.ref(repo)
   247         self.match = match.match(repo.root, b'', [], inc, exc)
   248         self.match = match.match(repo.root, b'', [], inc, exc)
   302             ctx = self.linkctx(path, node)
   303             ctx = self.linkctx(path, node)
   303             return self.substitute(data, path, ctx, self.rekw.sub)
   304             return self.substitute(data, path, ctx, self.rekw.sub)
   304         return data
   305         return data
   305 
   306 
   306     def iskwfile(self, cand, ctx):
   307     def iskwfile(self, cand, ctx):
   307         '''Returns subset of candidates which are configured for keyword
   308         """Returns subset of candidates which are configured for keyword
   308         expansion but are not symbolic links.'''
   309         expansion but are not symbolic links."""
   309         return [f for f in cand if self.match(f) and b'l' not in ctx.flags(f)]
   310         return [f for f in cand if self.match(f) and b'l' not in ctx.flags(f)]
   310 
   311 
   311     def overwrite(self, ctx, candidates, lookup, expand, rekw=False):
   312     def overwrite(self, ctx, candidates, lookup, expand, rekw=False):
   312         '''Overwrites selected files expanding/shrinking keywords.'''
   313         '''Overwrites selected files expanding/shrinking keywords.'''
   313         if self.restrict or lookup or self.postcommit:  # exclude kw_copy
   314         if self.restrict or lookup or self.postcommit:  # exclude kw_copy
   372             if not stringutil.binary(text):
   373             if not stringutil.binary(text):
   373                 return _shrinktext(text, self.rekwexp.sub).splitlines(True)
   374                 return _shrinktext(text, self.rekwexp.sub).splitlines(True)
   374         return lines
   375         return lines
   375 
   376 
   376     def wread(self, fname, data):
   377     def wread(self, fname, data):
   377         '''If in restricted mode returns data read from wdir with
   378         """If in restricted mode returns data read from wdir with
   378         keyword substitutions removed.'''
   379         keyword substitutions removed."""
   379         if self.restrict:
   380         if self.restrict:
   380             return self.shrink(fname, data)
   381             return self.shrink(fname, data)
   381         return data
   382         return data
   382 
   383 
   383 
   384 
   384 class kwfilelog(filelog.filelog):
   385 class kwfilelog(filelog.filelog):
   385     '''
   386     """
   386     Subclass of filelog to hook into its read, add, cmp methods.
   387     Subclass of filelog to hook into its read, add, cmp methods.
   387     Keywords are "stored" unexpanded, and processed on reading.
   388     Keywords are "stored" unexpanded, and processed on reading.
   388     '''
   389     """
   389 
   390 
   390     def __init__(self, opener, kwt, path):
   391     def __init__(self, opener, kwt, path):
   391         super(kwfilelog, self).__init__(opener, path)
   392         super(kwfilelog, self).__init__(opener, path)
   392         self.kwt = kwt
   393         self.kwt = kwt
   393         self.path = path
   394         self.path = path
   409         text = self.kwt.shrink(self.path, text)
   410         text = self.kwt.shrink(self.path, text)
   410         return super(kwfilelog, self).cmp(node, text)
   411         return super(kwfilelog, self).cmp(node, text)
   411 
   412 
   412 
   413 
   413 def _status(ui, repo, wctx, kwt, *pats, **opts):
   414 def _status(ui, repo, wctx, kwt, *pats, **opts):
   414     '''Bails out if [keyword] configuration is not active.
   415     """Bails out if [keyword] configuration is not active.
   415     Returns status of working directory.'''
   416     Returns status of working directory."""
   416     if kwt:
   417     if kwt:
   417         opts = pycompat.byteskwargs(opts)
   418         opts = pycompat.byteskwargs(opts)
   418         return repo.status(
   419         return repo.status(
   419             match=scmutil.match(wctx, pats, opts),
   420             match=scmutil.match(wctx, pats, opts),
   420             clean=True,
   421             clean=True,
   446     ],
   447     ],
   447     _(b'hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...'),
   448     _(b'hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...'),
   448     optionalrepo=True,
   449     optionalrepo=True,
   449 )
   450 )
   450 def demo(ui, repo, *args, **opts):
   451 def demo(ui, repo, *args, **opts):
   451     '''print [keywordmaps] configuration and an expansion example
   452     """print [keywordmaps] configuration and an expansion example
   452 
   453 
   453     Show current, custom, or default keyword template maps and their
   454     Show current, custom, or default keyword template maps and their
   454     expansions.
   455     expansions.
   455 
   456 
   456     Extend the current configuration by specifying maps as arguments
   457     Extend the current configuration by specifying maps as arguments
   457     and using -f/--rcfile to source an external hgrc file.
   458     and using -f/--rcfile to source an external hgrc file.
   458 
   459 
   459     Use -d/--default to disable current configuration.
   460     Use -d/--default to disable current configuration.
   460 
   461 
   461     See :hg:`help templates` for information on templates and filters.
   462     See :hg:`help templates` for information on templates and filters.
   462     '''
   463     """
   463 
   464 
   464     def demoitems(section, items):
   465     def demoitems(section, items):
   465         ui.write(b'[%s]\n' % section)
   466         ui.write(b'[%s]\n' % section)
   466         for k, v in sorted(items):
   467         for k, v in sorted(items):
   467             if isinstance(v, bool):
   468             if isinstance(v, bool):
   545     cmdutil.walkopts,
   546     cmdutil.walkopts,
   546     _(b'hg kwexpand [OPTION]... [FILE]...'),
   547     _(b'hg kwexpand [OPTION]... [FILE]...'),
   547     inferrepo=True,
   548     inferrepo=True,
   548 )
   549 )
   549 def expand(ui, repo, *pats, **opts):
   550 def expand(ui, repo, *pats, **opts):
   550     '''expand keywords in the working directory
   551     """expand keywords in the working directory
   551 
   552 
   552     Run after (re)enabling keyword expansion.
   553     Run after (re)enabling keyword expansion.
   553 
   554 
   554     kwexpand refuses to run if given files contain local changes.
   555     kwexpand refuses to run if given files contain local changes.
   555     '''
   556     """
   556     # 3rd argument sets expansion to True
   557     # 3rd argument sets expansion to True
   557     _kwfwrite(ui, repo, True, *pats, **opts)
   558     _kwfwrite(ui, repo, True, *pats, **opts)
   558 
   559 
   559 
   560 
   560 @command(
   561 @command(
   567     + cmdutil.walkopts,
   568     + cmdutil.walkopts,
   568     _(b'hg kwfiles [OPTION]... [FILE]...'),
   569     _(b'hg kwfiles [OPTION]... [FILE]...'),
   569     inferrepo=True,
   570     inferrepo=True,
   570 )
   571 )
   571 def files(ui, repo, *pats, **opts):
   572 def files(ui, repo, *pats, **opts):
   572     '''show files configured for keyword expansion
   573     """show files configured for keyword expansion
   573 
   574 
   574     List which files in the working directory are matched by the
   575     List which files in the working directory are matched by the
   575     [keyword] configuration patterns.
   576     [keyword] configuration patterns.
   576 
   577 
   577     Useful to prevent inadvertent keyword expansion and to speed up
   578     Useful to prevent inadvertent keyword expansion and to speed up
   586 
   587 
   587       K = keyword expansion candidate
   588       K = keyword expansion candidate
   588       k = keyword expansion candidate (not tracked)
   589       k = keyword expansion candidate (not tracked)
   589       I = ignored
   590       I = ignored
   590       i = ignored (not tracked)
   591       i = ignored (not tracked)
   591     '''
   592     """
   592     kwt = getattr(repo, '_keywordkwt', None)
   593     kwt = getattr(repo, '_keywordkwt', None)
   593     wctx = repo[None]
   594     wctx = repo[None]
   594     status = _status(ui, repo, wctx, kwt, *pats, **opts)
   595     status = _status(ui, repo, wctx, kwt, *pats, **opts)
   595     if pats:
   596     if pats:
   596         cwd = repo.getcwd()
   597         cwd = repo.getcwd()
   632     cmdutil.walkopts,
   633     cmdutil.walkopts,
   633     _(b'hg kwshrink [OPTION]... [FILE]...'),
   634     _(b'hg kwshrink [OPTION]... [FILE]...'),
   634     inferrepo=True,
   635     inferrepo=True,
   635 )
   636 )
   636 def shrink(ui, repo, *pats, **opts):
   637 def shrink(ui, repo, *pats, **opts):
   637     '''revert expanded keywords in the working directory
   638     """revert expanded keywords in the working directory
   638 
   639 
   639     Must be run before changing/disabling active keywords.
   640     Must be run before changing/disabling active keywords.
   640 
   641 
   641     kwshrink refuses to run if given files contain local changes.
   642     kwshrink refuses to run if given files contain local changes.
   642     '''
   643     """
   643     # 3rd argument sets expansion to False
   644     # 3rd argument sets expansion to False
   644     _kwfwrite(ui, repo, False, *pats, **opts)
   645     _kwfwrite(ui, repo, False, *pats, **opts)
   645 
   646 
   646 
   647 
   647 # monkeypatches
   648 # monkeypatches
   648 
   649 
   649 
   650 
   650 def kwpatchfile_init(orig, self, ui, gp, backend, store, eolmode=None):
   651 def kwpatchfile_init(orig, self, ui, gp, backend, store, eolmode=None):
   651     '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
   652     """Monkeypatch/wrap patch.patchfile.__init__ to avoid
   652     rejects or conflicts due to expanded keywords in working dir.'''
   653     rejects or conflicts due to expanded keywords in working dir."""
   653     orig(self, ui, gp, backend, store, eolmode)
   654     orig(self, ui, gp, backend, store, eolmode)
   654     kwt = getattr(getattr(backend, 'repo', None), '_keywordkwt', None)
   655     kwt = getattr(getattr(backend, 'repo', None), '_keywordkwt', None)
   655     if kwt:
   656     if kwt:
   656         # shrink keywords read from working dir
   657         # shrink keywords read from working dir
   657         self.lines = kwt.shrinklines(self.fname, self.lines)
   658         self.lines = kwt.shrinklines(self.fname, self.lines)
   700             kwt.restrict = False
   701             kwt.restrict = False
   701         return newid
   702         return newid
   702 
   703 
   703 
   704 
   704 def kw_copy(orig, ui, repo, pats, opts, rename=False):
   705 def kw_copy(orig, ui, repo, pats, opts, rename=False):
   705     '''Wraps cmdutil.copy so that copy/rename destinations do not
   706     """Wraps cmdutil.copy so that copy/rename destinations do not
   706     contain expanded keywords.
   707     contain expanded keywords.
   707     Note that the source of a regular file destination may also be a
   708     Note that the source of a regular file destination may also be a
   708     symlink:
   709     symlink:
   709     hg cp sym x                -> x is symlink
   710     hg cp sym x                -> x is symlink
   710     cp sym x; hg cp -A sym x   -> x is file (maybe expanded keywords)
   711     cp sym x; hg cp -A sym x   -> x is file (maybe expanded keywords)
   711     For the latter we have to follow the symlink to find out whether its
   712     For the latter we have to follow the symlink to find out whether its
   712     target is configured for expansion and we therefore must unexpand the
   713     target is configured for expansion and we therefore must unexpand the
   713     keywords in the destination.'''
   714     keywords in the destination."""
   714     kwt = getattr(repo, '_keywordkwt', None)
   715     kwt = getattr(repo, '_keywordkwt', None)
   715     if kwt is None:
   716     if kwt is None:
   716         return orig(ui, repo, pats, opts, rename)
   717         return orig(ui, repo, pats, opts, rename)
   717     with repo.wlock():
   718     with repo.wlock():
   718         orig(ui, repo, pats, opts, rename)
   719         orig(ui, repo, pats, opts, rename)
   720             return
   721             return
   721         wctx = repo[None]
   722         wctx = repo[None]
   722         cwd = repo.getcwd()
   723         cwd = repo.getcwd()
   723 
   724 
   724         def haskwsource(dest):
   725         def haskwsource(dest):
   725             '''Returns true if dest is a regular file and configured for
   726             """Returns true if dest is a regular file and configured for
   726             expansion or a symlink which points to a file configured for
   727             expansion or a symlink which points to a file configured for
   727             expansion. '''
   728             expansion."""
   728             source = repo.dirstate.copied(dest)
   729             source = repo.dirstate.copied(dest)
   729             if b'l' in wctx.flags(source):
   730             if b'l' in wctx.flags(source):
   730                 source = pathutil.canonpath(
   731                 source = pathutil.canonpath(
   731                     repo.root, cwd, os.path.realpath(source)
   732                     repo.root, cwd, os.path.realpath(source)
   732                 )
   733                 )
   783         return self._filelog.cmp(self._filenode, fctx.data())
   784         return self._filelog.cmp(self._filenode, fctx.data())
   784     return True
   785     return True
   785 
   786 
   786 
   787 
   787 def uisetup(ui):
   788 def uisetup(ui):
   788     ''' Monkeypatches dispatch._parse to retrieve user command.
   789     """Monkeypatches dispatch._parse to retrieve user command.
   789     Overrides file method to return kwfilelog instead of filelog
   790     Overrides file method to return kwfilelog instead of filelog
   790     if file matches user configuration.
   791     if file matches user configuration.
   791     Wraps commit to overwrite configured files with updated
   792     Wraps commit to overwrite configured files with updated
   792     keyword substitutions.
   793     keyword substitutions.
   793     Monkeypatches patch and webcommands.'''
   794     Monkeypatches patch and webcommands."""
   794 
   795 
   795     def kwdispatch_parse(orig, ui, args):
   796     def kwdispatch_parse(orig, ui, args):
   796         '''Monkeypatch dispatch._parse to obtain running hg command.'''
   797         '''Monkeypatch dispatch._parse to obtain running hg command.'''
   797         cmd, func, args, options, cmdoptions = orig(ui, args)
   798         cmd, func, args, options, cmdoptions = orig(ui, args)
   798         kwtools[b'hgcmd'] = cmd
   799         kwtools[b'hgcmd'] = cmd