hgkw/keyword.py
branchstable
changeset 418 3d882f14f23a
parent 416 b69dca43ef08
child 419 2f179ea3a9aa
equal deleted inserted replaced
404:45d3ea301c03 418:3d882f14f23a
   101 def utcdate(date):
   101 def utcdate(date):
   102     '''Returns hgdate in cvs-like UTC format.'''
   102     '''Returns hgdate in cvs-like UTC format.'''
   103     return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0]))
   103     return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0]))
   104 
   104 
   105 
   105 
   106 _kwtemplater = _cmd = _cmdoptions = None
   106 # make keyword tools accessible
   107  
   107 kwtools = {'templater': None, 'hgcmd': None}
       
   108 
   108 # store originals of monkeypatches
   109 # store originals of monkeypatches
   109 _patchfile_init = patch.patchfile.__init__
   110 _patchfile_init = patch.patchfile.__init__
   110 _patch_diff = patch.diff
   111 _patch_diff = patch.diff
   111 _dispatch_parse = dispatch._parse
   112 _dispatch_parse = dispatch._parse
   112 
   113 
   113 def _kwpatchfile_init(self, ui, fname, missing=False):
   114 def _kwpatchfile_init(self, ui, fname, missing=False):
   114     '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
   115     '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
   115     rejects or conflicts due to expanded keywords in working dir.'''
   116     rejects or conflicts due to expanded keywords in working dir.'''
   116     _patchfile_init(self, ui, fname, missing=missing)
   117     _patchfile_init(self, ui, fname, missing=missing)
   117     if _kwtemplater.matcher(self.fname):
   118     # shrink keywords read from working dir
   118         # shrink keywords read from working dir
   119     kwt = kwtools['templater']
   119         kwshrunk = _kwtemplater.shrink(''.join(self.lines))
   120     self.lines = kwt.shrinklines(self.fname, self.lines)
   120         self.lines = kwshrunk.splitlines(True)
       
   121 
   121 
   122 def _kw_diff(repo, node1=None, node2=None, files=None, match=util.always,
   122 def _kw_diff(repo, node1=None, node2=None, files=None, match=util.always,
   123              fp=None, changes=None, opts=None):
   123              fp=None, changes=None, opts=None):
   124     # only expand if comparing against working dir
   124     '''Monkeypatch patch.diff to avoid expansion except when
       
   125     comparing against working dir.'''
   125     if node2 is not None:
   126     if node2 is not None:
   126         _kwtemplater.matcher = util.never
   127         kwtools['templater'].matcher = util.never
   127     if node1 is not None and node1 != repo.changectx().node():
   128     elif node1 is not None and node1 != repo.changectx().node():
   128         _kwtemplater.restrict = True
   129         kwtools['templater'].restrict = True
   129     _patch_diff(repo, node1=node1, node2=node2, files=files, match=match,
   130     _patch_diff(repo, node1=node1, node2=node2, files=files, match=match,
   130                 fp=fp, changes=changes, opts=opts)
   131                 fp=fp, changes=changes, opts=opts)
   131 
   132 
   132 def _kwweb_changeset(web, req, tmpl):
   133 def _kwweb_changeset(web, req, tmpl):
   133     '''Wraps webcommands.changeset turning off keyword expansion.'''
   134     '''Wraps webcommands.changeset turning off keyword expansion.'''
   134     _kwtemplater.matcher = util.never
   135     kwtools['templater'].matcher = util.never
   135     return web.changeset(tmpl, web.changectx(req))
   136     return web.changeset(tmpl, web.changectx(req))
   136 
   137 
   137 def _kwweb_filediff(web, req, tmpl):
   138 def _kwweb_filediff(web, req, tmpl):
   138     '''Wraps webcommands.filediff turning off keyword expansion.'''
   139     '''Wraps webcommands.filediff turning off keyword expansion.'''
   139     _kwtemplater.matcher = util.never
   140     kwtools['templater'].matcher = util.never
   140     return web.filediff(tmpl, web.filectx(req))
   141     return web.filediff(tmpl, web.filectx(req))
   141 
   142 
   142 def _kwdispatch_parse(ui, args):
   143 def _kwdispatch_parse(ui, args):
   143     '''Monkeypatch dispatch._parse to obtain running hg command.'''
   144     '''Monkeypatch dispatch._parse to obtain running hg command.'''
   144     global _cmd
   145     cmd, func, args, options, cmdoptions = _dispatch_parse(ui, args)
   145     _cmd, func, args, options, cmdoptions = _dispatch_parse(ui, args)
   146     kwtools['hgcmd'] = cmd
   146     return _cmd, func, args, options, cmdoptions
   147     return cmd, func, args, options, cmdoptions
   147 
   148 
   148 # dispatch._parse is run before reposetup, so wrap it here
   149 # dispatch._parse is run before reposetup, so wrap it here
   149 dispatch._parse = _kwdispatch_parse
   150 dispatch._parse = _kwdispatch_parse
   150 
   151 
   151 
   152 
   162         'Source': '{root}/{file},v',
   163         'Source': '{root}/{file},v',
   163         'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
   164         'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
   164         'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
   165         'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
   165     }
   166     }
   166 
   167 
   167     def __init__(self, ui, repo, inc, exc, hgcmd):
   168     def __init__(self, ui, repo, inc, exc):
   168         self.ui = ui
   169         self.ui = ui
   169         self.repo = repo
   170         self.repo = repo
   170         self.matcher = util.matcher(repo.root, inc=inc, exc=exc)[1]
   171         self.matcher = util.matcher(repo.root, inc=inc, exc=exc)[1]
   171         self.restrict = hgcmd in restricted.split()
   172         self.restrict = kwtools['hgcmd'] in restricted.split()
   172         self.commitnode = None
       
   173         self.path = ''
       
   174 
   173 
   175         kwmaps = self.ui.configitems('keywordmaps')
   174         kwmaps = self.ui.configitems('keywordmaps')
   176         if kwmaps: # override default templates
   175         if kwmaps: # override default templates
   177             kwmaps = [(k, templater.parsestring(v, quoted=False))
   176             kwmaps = [(k, templater.parsestring(v, quoted=False))
   178                       for (k, v) in kwmaps]
   177                       for (k, v) in kwmaps]
   183 
   182 
   184         templatefilters.filters['utcdate'] = utcdate
   183         templatefilters.filters['utcdate'] = utcdate
   185         self.ct = cmdutil.changeset_templater(self.ui, self.repo,
   184         self.ct = cmdutil.changeset_templater(self.ui, self.repo,
   186                                               False, '', False)
   185                                               False, '', False)
   187 
   186 
   188     def substitute(self, node, data, subfunc):
   187     def getnode(self, path, fnode):
   189         '''Obtains file's changenode if commit node not given,
   188         '''Derives changenode from file path and filenode.'''
   190         and calls given substitution function.'''
   189         # used by kwfilelog.read and kwexpand
   191         if self.commitnode:
   190         c = context.filectx(self.repo, path, fileid=fnode)
   192             fnode = self.commitnode
   191         return c.node()
   193         else:
   192 
   194             c = context.filectx(self.repo, self.path, fileid=node)
   193     def substitute(self, data, path, node, subfunc):
   195             fnode = c.node()
   194         '''Replaces keywords in data with expanded template.'''
   196 
       
   197         def kwsub(mobj):
   195         def kwsub(mobj):
   198             '''Substitutes keyword using corresponding template.'''
       
   199             kw = mobj.group(1)
   196             kw = mobj.group(1)
   200             self.ct.use_template(self.templates[kw])
   197             self.ct.use_template(self.templates[kw])
   201             self.ui.pushbuffer()
   198             self.ui.pushbuffer()
   202             self.ct.show(changenode=fnode, root=self.repo.root, file=self.path)
   199             self.ct.show(changenode=node, root=self.repo.root, file=path)
   203             ekw = templatefilters.firstline(self.ui.popbuffer())
   200             ekw = templatefilters.firstline(self.ui.popbuffer())
   204             return '$%s: %s $' % (kw, ekw)
   201             return '$%s: %s $' % (kw, ekw)
   205 
       
   206         return subfunc(kwsub, data)
   202         return subfunc(kwsub, data)
   207 
   203 
   208     def expand(self, node, data):
   204     def expand(self, path, node, data):
   209         '''Returns data with keywords expanded.'''
   205         '''Returns data with keywords expanded.'''
   210         if self.restrict or util.binary(data):
   206         if not self.restrict and self.matcher(path) and not util.binary(data):
   211             return data
   207             changenode = self.getnode(path, node)
   212         return self.substitute(node, data, self.re_kw.sub)
   208             return self.substitute(data, path, changenode, self.re_kw.sub)
   213 
   209         return data
   214     def process(self, node, data, expand):
   210 
   215         '''Returns a tuple: data, count.
   211     def iskwfile(self, path, islink):
   216         Count is number of keywords/keyword substitutions,
   212         '''Returns true if path matches [keyword] pattern
   217         telling caller whether to act on file containing data.'''
   213         and is not a symbolic link.
   218         if util.binary(data):
   214         Caveat: localrepository._link fails on Windows.'''
   219             return data, None
   215         return self.matcher(path) and not islink(path)
   220         if expand:
   216 
   221             return self.substitute(node, data, self.re_kw.subn)
   217     def overwrite(self, node=None, expand=True, files=None):
   222         return data, self.re_kw.search(data)
   218         '''Overwrites selected files expanding/shrinking keywords.'''
   223 
   219         ctx = self.repo.changectx(node)
   224     def shrink(self, text):
   220         mf = ctx.manifest()
       
   221         if node is not None:     # commit
       
   222             files = [f for f in ctx.files() if f in mf]
       
   223             notify = self.ui.debug
       
   224         else:                    # kwexpand/kwshrink
       
   225             notify = self.ui.note
       
   226         candidates = [f for f in files if self.iskwfile(f, mf.linkf)]
       
   227         if candidates:
       
   228             self.restrict = True # do not expand when reading
       
   229             candidates.sort()
       
   230             action = expand and 'expanding' or 'shrinking'
       
   231             for f in candidates:
       
   232                 fp = self.repo.file(f)
       
   233                 data = fp.read(mf[f])
       
   234                 if util.binary(data):
       
   235                     continue
       
   236                 if expand:
       
   237                     changenode = node or self.getnode(f, mf[f])
       
   238                     data, found = self.substitute(data, f, changenode,
       
   239                                                   self.re_kw.subn)
       
   240                 else:
       
   241                     found = self.re_kw.search(data)
       
   242                 if found:
       
   243                     notify(_('overwriting %s %s keywords\n') % (f, action))
       
   244                     self.repo.wwrite(f, data, mf.flags(f))
       
   245                     self.repo.dirstate.normal(f)
       
   246             self.restrict = False
       
   247 
       
   248     def shrinktext(self, text):
       
   249         '''Unconditionally removes all keyword substitutions from text.'''
       
   250         return self.re_kw.sub(r'$\1$', text)
       
   251 
       
   252     def shrink(self, fname, text):
   225         '''Returns text with all keyword substitutions removed.'''
   253         '''Returns text with all keyword substitutions removed.'''
   226         if util.binary(text):
   254         if self.matcher(fname) and not util.binary(text):
   227             return text
   255             return self.shrinktext(text)
   228         return self.re_kw.sub(r'$\1$', text)
   256         return text
       
   257 
       
   258     def shrinklines(self, fname, lines):
       
   259         '''Returns lines with keyword substitutions removed.'''
       
   260         if self.matcher(fname):
       
   261             text = ''.join(lines)
       
   262             if not util.binary(text):
       
   263                 return self.shrinktext(text).splitlines(True)
       
   264         return lines
       
   265 
       
   266     def wread(self, fname, data):
       
   267         '''If in restricted mode returns data read from wdir with
       
   268         keyword substitutions removed.'''
       
   269         return self.restrict and self.shrink(fname, data) or data
   229 
   270 
   230 class kwfilelog(filelog.filelog):
   271 class kwfilelog(filelog.filelog):
   231     '''
   272     '''
   232     Subclass of filelog to hook into its read, add, cmp methods.
   273     Subclass of filelog to hook into its read, add, cmp methods.
   233     Keywords are "stored" unexpanded, and processed on reading.
   274     Keywords are "stored" unexpanded, and processed on reading.
   234     '''
   275     '''
   235     def __init__(self, opener, path):
   276     def __init__(self, opener, path):
   236         super(kwfilelog, self).__init__(opener, path)
   277         super(kwfilelog, self).__init__(opener, path)
   237         _kwtemplater.path = path
   278         self.kwt = kwtools['templater']
   238 
   279         self.path = path
   239     def kwctread(self, node, expand):
       
   240         '''Reads expanding and counting keywords, called from _overwrite.'''
       
   241         data = super(kwfilelog, self).read(node)
       
   242         return _kwtemplater.process(node, data, expand)
       
   243 
   280 
   244     def read(self, node):
   281     def read(self, node):
   245         '''Expands keywords when reading filelog.'''
   282         '''Expands keywords when reading filelog.'''
   246         data = super(kwfilelog, self).read(node)
   283         data = super(kwfilelog, self).read(node)
   247         return _kwtemplater.expand(node, data)
   284         return self.kwt.expand(self.path, node, data)
   248 
   285 
   249     def add(self, text, meta, tr, link, p1=None, p2=None):
   286     def add(self, text, meta, tr, link, p1=None, p2=None):
   250         '''Removes keyword substitutions when adding to filelog.'''
   287         '''Removes keyword substitutions when adding to filelog.'''
   251         text = _kwtemplater.shrink(text)
   288         text = self.kwt.shrink(self.path, text)
   252         return super(kwfilelog, self).add(text, meta, tr, link, p1=p1, p2=p2)
   289         return super(kwfilelog, self).add(text, meta, tr, link, p1=p1, p2=p2)
   253 
   290 
   254     def cmp(self, node, text):
   291     def cmp(self, node, text):
   255         '''Removes keyword substitutions for comparison.'''
   292         '''Removes keyword substitutions for comparison.'''
   256         text = _kwtemplater.shrink(text)
   293         text = self.kwt.shrink(self.path, text)
   257         if self.renamed(node):
   294         if self.renamed(node):
   258             t2 = super(kwfilelog, self).read(node)
   295             t2 = super(kwfilelog, self).read(node)
   259             return t2 != text
   296             return t2 != text
   260         return revlog.revlog.cmp(self, node, text)
   297         return revlog.revlog.cmp(self, node, text)
   261 
   298 
   262 def _iskwfile(f, link):
   299 def _status(ui, repo, kwt, *pats, **opts):
   263     return not link(f) and _kwtemplater.matcher(f)
       
   264 
       
   265 def _status(ui, repo, *pats, **opts):
       
   266     '''Bails out if [keyword] configuration is not active.
   300     '''Bails out if [keyword] configuration is not active.
   267     Returns status of working directory.'''
   301     Returns status of working directory.'''
   268     if _kwtemplater:
   302     if kwt:
   269         files, match, anypats = cmdutil.matchpats(repo, pats, opts)
   303         files, match, anypats = cmdutil.matchpats(repo, pats, opts)
   270         return repo.status(files=files, match=match, list_clean=True)
   304         return repo.status(files=files, match=match, list_clean=True)
   271     if ui.configitems('keyword'):
   305     if ui.configitems('keyword'):
   272         raise util.Abort(_('[keyword] patterns cannot match'))
   306         raise util.Abort(_('[keyword] patterns cannot match'))
   273     raise util.Abort(_('no [keyword] patterns configured'))
   307     raise util.Abort(_('no [keyword] patterns configured'))
   274 
   308 
   275 def _overwrite(ui, repo, node=None, expand=True, files=None):
       
   276     '''Overwrites selected files expanding/shrinking keywords.'''
       
   277     ctx = repo.changectx(node)
       
   278     mf = ctx.manifest()
       
   279     if node is not None:   # commit
       
   280         _kwtemplater.commitnode = node
       
   281         files = [f for f in ctx.files() if f in mf]
       
   282         notify = ui.debug
       
   283     else:                  # kwexpand/kwshrink
       
   284         notify = ui.note
       
   285     candidates = [f for f in files if _iskwfile(f, mf.linkf)]
       
   286     if candidates:
       
   287         candidates.sort()
       
   288         action = expand and 'expanding' or 'shrinking'
       
   289         for f in candidates:
       
   290             fp = repo.file(f, kwmatch=True)
       
   291             data, kwfound = fp.kwctread(mf[f], expand)
       
   292             if kwfound:
       
   293                 notify(_('overwriting %s %s keywords\n') % (f, action))
       
   294                 repo.wwrite(f, data, mf.flags(f))
       
   295                 repo.dirstate.normal(f)
       
   296 
       
   297 def _kwfwrite(ui, repo, expand, *pats, **opts):
   309 def _kwfwrite(ui, repo, expand, *pats, **opts):
   298     '''Selects files and passes them to _overwrite.'''
   310     '''Selects files and passes them to kwtemplater.overwrite.'''
   299     status = _status(ui, repo, *pats, **opts)
   311     kwt = kwtools['templater']
       
   312     status = _status(ui, repo, kwt, *pats, **opts)
   300     modified, added, removed, deleted, unknown, ignored, clean = status
   313     modified, added, removed, deleted, unknown, ignored, clean = status
   301     if modified or added or removed or deleted:
   314     if modified or added or removed or deleted:
   302         raise util.Abort(_('outstanding uncommitted changes in given files'))
   315         raise util.Abort(_('outstanding uncommitted changes in given files'))
   303     wlock = lock = None
   316     wlock = lock = None
   304     try:
   317     try:
   305         wlock = repo.wlock()
   318         wlock = repo.wlock()
   306         lock = repo.lock()
   319         lock = repo.lock()
   307         _overwrite(ui, repo, expand=expand, files=clean)
   320         kwt.overwrite(expand=expand, files=clean)
   308     finally:
   321     finally:
   309         del wlock, lock
   322         del wlock, lock
   310 
   323 
   311 
   324 
   312 def demo(ui, repo, *args, **opts):
   325 def demo(ui, repo, *args, **opts):
   404 
   417 
   405     Crosscheck which files in working directory are potential targets for
   418     Crosscheck which files in working directory are potential targets for
   406     keyword expansion.
   419     keyword expansion.
   407     That is, files matched by [keyword] config patterns but not symlinks.
   420     That is, files matched by [keyword] config patterns but not symlinks.
   408     '''
   421     '''
   409     status = _status(ui, repo, *pats, **opts)
   422     kwt = kwtools['templater']
       
   423     status = _status(ui, repo, kwt, *pats, **opts)
   410     modified, added, removed, deleted, unknown, ignored, clean = status
   424     modified, added, removed, deleted, unknown, ignored, clean = status
   411     files = modified + added + clean
   425     files = modified + added + clean
   412     if opts.get('untracked'):
   426     if opts.get('untracked'):
   413         files += unknown
   427         files += unknown
   414     files.sort()
   428     files.sort()
   415     wctx = repo.workingctx()
   429     wctx = repo.workingctx()
   416     islink = lambda p: 'l' in wctx.fileflags(p)
   430     islink = lambda p: 'l' in wctx.fileflags(p)
   417     kwfiles = [f for f in files if _iskwfile(f, islink)]
   431     kwfiles = [f for f in files if kwt.iskwfile(f, islink)]
   418     cwd = pats and repo.getcwd() or ''
   432     cwd = pats and repo.getcwd() or ''
   419     kwfstats = not opts.get('ignore') and (('K', kwfiles),) or ()
   433     kwfstats = not opts.get('ignore') and (('K', kwfiles),) or ()
   420     if opts.get('all') or opts.get('ignore'):
   434     if opts.get('all') or opts.get('ignore'):
   421         kwfstats += (('I', [f for f in files if f not in kwfiles]),)
   435         kwfstats += (('I', [f for f in files if f not in kwfiles]),)
   422     for char, filenames in kwfstats:
   436     for char, filenames in kwfstats:
   443     Wraps commit to overwrite configured files with updated
   457     Wraps commit to overwrite configured files with updated
   444     keyword substitutions.
   458     keyword substitutions.
   445     This is done for local repos only, and only if there are
   459     This is done for local repos only, and only if there are
   446     files configured at all for keyword substitution.'''
   460     files configured at all for keyword substitution.'''
   447 
   461 
   448     global _kwtemplater
       
   449 
       
   450     try:
   462     try:
   451         if (not repo.local() or _cmd in nokwcommands.split() 
   463         if (not repo.local() or kwtools['hgcmd'] in nokwcommands.split()
   452             or '.hg' in util.splitpath(repo.root)
   464             or '.hg' in util.splitpath(repo.root)
   453             or repo._url.startswith('bundle:')):
   465             or repo._url.startswith('bundle:')):
   454             return
   466             return
   455     except AttributeError:
   467     except AttributeError:
   456         pass
   468         pass
   462         else:
   474         else:
   463             exc.append(pat)
   475             exc.append(pat)
   464     if not inc:
   476     if not inc:
   465         return
   477         return
   466 
   478 
   467     _kwtemplater = kwtemplater(ui, repo, inc, exc, _cmd)
   479     kwtools['templater'] = kwt = kwtemplater(ui, repo, inc, exc)
   468 
   480 
   469     class kwrepo(repo.__class__):
   481     class kwrepo(repo.__class__):
   470         def file(self, f, kwmatch=False):
   482         def file(self, f):
   471             if f[0] == '/':
   483             if f[0] == '/':
   472                 f = f[1:]
   484                 f = f[1:]
   473             if kwmatch or _kwtemplater.matcher(f):
   485             return kwfilelog(self.sopener, f)
   474                 return kwfilelog(self.sopener, f)
       
   475             return filelog.filelog(self.sopener, f)
       
   476 
   486 
   477         def wread(self, filename):
   487         def wread(self, filename):
   478             data = super(kwrepo, self).wread(filename)
   488             data = super(kwrepo, self).wread(filename)
   479             if _kwtemplater.restrict and _kwtemplater.matcher(filename):
   489             return kwt.wread(filename, data)
   480                 return _kwtemplater.shrink(data)
       
   481             return data
       
   482 
   490 
   483         def commit(self, files=None, text='', user=None, date=None,
   491         def commit(self, files=None, text='', user=None, date=None,
   484                    match=util.always, force=False, force_editor=False,
   492                    match=util.always, force=False, force_editor=False,
   485                    p1=None, p2=None, extra={}, empty_ok=False):
   493                    p1=None, p2=None, extra={}, empty_ok=False):
   486             wlock = lock = None
   494             wlock = lock = None
   515 
   523 
   516                 # restore commit hooks
   524                 # restore commit hooks
   517                 for name, cmd in commithooks.iteritems():
   525                 for name, cmd in commithooks.iteritems():
   518                     ui.setconfig('hooks', name, cmd)
   526                     ui.setconfig('hooks', name, cmd)
   519                 if node is not None:
   527                 if node is not None:
   520                     _overwrite(ui, self, node=node)
   528                     kwt.overwrite(node=node)
   521                     repo.hook('commit', node=node, parent1=_p1, parent2=_p2)
   529                     repo.hook('commit', node=node, parent1=_p1, parent2=_p2)
   522                 return node
   530                 return node
   523             finally:
   531             finally:
   524                 del wlock, lock
   532                 del wlock, lock
   525 
   533