78 Expansions spanning more than one line and incremental expansions, |
78 Expansions spanning more than one line and incremental expansions, |
79 like CVS' $Log$, are not supported. A keyword template map |
79 like CVS' $Log$, are not supported. A keyword template map |
80 "Log = {desc}" expands to the first line of the changeset description. |
80 "Log = {desc}" expands to the first line of the changeset description. |
81 ''' |
81 ''' |
82 |
82 |
83 from mercurial import commands, cmdutil, context, localrepo |
83 from mercurial import commands, cmdutil, context, dispatch, filelog, revlog |
84 from mercurial import patch, revlog, templater, templatefilters, util |
84 from mercurial import patch, localrepo, templater, templatefilters, util |
85 from mercurial.node import * |
85 from mercurial.node import * |
86 from mercurial.hgweb import webcommands |
|
87 from mercurial.i18n import _ |
86 from mercurial.i18n import _ |
88 import mimetypes, re, shutil, tempfile, time |
87 import re, shutil, sys, tempfile, time |
89 |
88 |
90 commands.optionalrepo += ' kwdemo' |
89 commands.optionalrepo += ' kwdemo' |
|
90 |
|
91 # hg commands that do not act on keywords |
|
92 nokwcommands = ('add addremove bundle copy export grep identify incoming init' |
|
93 ' log outgoing push remove rename rollback tip convert') |
|
94 |
|
95 # hg commands that trigger expansion only when writing to working dir, |
|
96 # not when reading filelog, and unexpand when reading from working dir |
|
97 restricted = 'diff1 record qfold qimport qnew qpush qrefresh qrecord' |
91 |
98 |
92 def utcdate(date): |
99 def utcdate(date): |
93 '''Returns hgdate in cvs-like UTC format.''' |
100 '''Returns hgdate in cvs-like UTC format.''' |
94 return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0])) |
101 return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0])) |
|
102 |
|
103 |
|
104 _kwtemplater = None |
95 |
105 |
96 class kwtemplater(object): |
106 class kwtemplater(object): |
97 ''' |
107 ''' |
98 Sets up keyword templates, corresponding keyword regex, and |
108 Sets up keyword templates, corresponding keyword regex, and |
99 provides keyword substitution functions. |
109 provides keyword substitution functions. |
125 |
137 |
126 templatefilters.filters['utcdate'] = utcdate |
138 templatefilters.filters['utcdate'] = utcdate |
127 self.ct = cmdutil.changeset_templater(self.ui, self.repo, |
139 self.ct = cmdutil.changeset_templater(self.ui, self.repo, |
128 False, '', False) |
140 False, '', False) |
129 |
141 |
130 def substitute(self, path, data, node, subfunc): |
142 def substitute(self, node, data, subfunc): |
131 '''Obtains file's changenode if node not given, |
143 '''Obtains file's changenode if commit node not given, |
132 and calls given substitution function.''' |
144 and calls given substitution function.''' |
133 if node is None: |
145 if self.commitnode: |
134 # kwrepo.wwrite except when overwriting on commit |
146 fnode = self.commitnode |
135 try: |
147 else: |
136 fnode = self.ctx.filenode(path) |
148 c = context.filectx(self.repo, self.path, fileid=node) |
137 fl = self.repo.file(path) |
149 fnode = c.node() |
138 c = context.filectx(self.repo, path, fileid=fnode, filelog=fl) |
|
139 node = c.node() |
|
140 except revlog.LookupError: |
|
141 # eg: convert |
|
142 return subfunc == self.re_kw.sub and data or (data, None) |
|
143 |
150 |
144 def kwsub(mobj): |
151 def kwsub(mobj): |
145 '''Substitutes keyword using corresponding template.''' |
152 '''Substitutes keyword using corresponding template.''' |
146 kw = mobj.group(1) |
153 kw = mobj.group(1) |
147 self.ct.use_template(self.templates[kw]) |
154 self.ct.use_template(self.templates[kw]) |
148 self.ui.pushbuffer() |
155 self.ui.pushbuffer() |
149 self.ct.show(changenode=node, root=self.repo.root, file=path) |
156 self.ct.show(changenode=fnode, root=self.repo.root, file=self.path) |
150 ekw = templatefilters.firstline(self.ui.popbuffer()) |
157 ekw = templatefilters.firstline(self.ui.popbuffer()) |
151 return '$%s: %s $' % (kw, ekw) |
158 return '$%s: %s $' % (kw, ekw) |
152 |
159 |
153 return subfunc(kwsub, data) |
160 return subfunc(kwsub, data) |
154 |
161 |
155 def expand(self, path, data, ctx): |
162 def expand(self, node, data): |
156 '''Returns data with keywords expanded.''' |
163 '''Returns data with keywords expanded.''' |
157 if util.binary(data): |
164 if self.restricted or util.binary(data): |
158 return data |
165 return data |
159 if self.ctx is None: |
166 return self.substitute(node, data, self.re_kw.sub) |
160 self.ctx = ctx or self.repo.changectx() |
167 |
161 return self.substitute(path, data, None, self.re_kw.sub) |
168 def process(self, node, data, expand): |
162 |
|
163 def process(self, path, data, expand, ctx, node): |
|
164 '''Returns a tuple: data, count. |
169 '''Returns a tuple: data, count. |
165 Count is number of keywords/keyword substitutions, |
170 Count is number of keywords/keyword substitutions, |
166 telling caller whether to act on file containing data.''' |
171 telling caller whether to act on file containing data.''' |
167 if util.binary(data): |
172 if util.binary(data): |
168 return data, None |
173 return data, None |
169 if expand: |
174 if expand: |
170 self.ctx = ctx |
175 return self.substitute(node, data, self.re_kw.subn) |
171 return self.substitute(path, data, node, self.re_kw.subn) |
176 return data, self.re_kw.search(data) |
172 return self.re_kw.subn(r'$\1$', data) |
177 |
173 |
178 def shrink(self, text): |
174 def shrink(self, data): |
|
175 '''Returns text with all keyword substitutions removed.''' |
179 '''Returns text with all keyword substitutions removed.''' |
176 if util.binary(data): |
180 if util.binary(text): |
177 return data |
181 return text |
178 return self.re_kw.sub(r'$\1$', data) |
182 return self.re_kw.sub(r'$\1$', text) |
|
183 |
|
184 class kwfilelog(filelog.filelog): |
|
185 ''' |
|
186 Subclass of filelog to hook into its read, add, cmp methods. |
|
187 Keywords are "stored" unexpanded, and processed on reading. |
|
188 ''' |
|
189 def __init__(self, opener, path): |
|
190 super(kwfilelog, self).__init__(opener, path) |
|
191 _kwtemplater.path = path |
|
192 |
|
193 def kwctread(self, node, expand): |
|
194 '''Reads expanding and counting keywords, called from _overwrite.''' |
|
195 data = super(kwfilelog, self).read(node) |
|
196 return _kwtemplater.process(node, data, expand) |
|
197 |
|
198 def read(self, node): |
|
199 '''Expands keywords when reading filelog.''' |
|
200 data = super(kwfilelog, self).read(node) |
|
201 return _kwtemplater.expand(node, data) |
|
202 |
|
203 def add(self, text, meta, tr, link, p1=None, p2=None): |
|
204 '''Removes keyword substitutions when adding to filelog.''' |
|
205 text = _kwtemplater.shrink(text) |
|
206 return super(kwfilelog, self).add(text, meta, tr, link, p1=p1, p2=p2) |
|
207 |
|
208 def cmp(self, node, text): |
|
209 '''Removes keyword substitutions for comparison.''' |
|
210 text = _kwtemplater.shrink(text) |
|
211 if self.renamed(node): |
|
212 t2 = super(kwfilelog, self).read(node) |
|
213 return t2 != text |
|
214 return revlog.revlog.cmp(self, node, text) |
|
215 |
179 |
216 |
180 # store original patch.patchfile.__init__ |
217 # store original patch.patchfile.__init__ |
181 _patchfile_init = patch.patchfile.__init__ |
218 _patchfile_init = patch.patchfile.__init__ |
182 |
219 |
183 |
220 def _kwpatchfile_init(self, ui, fname, missing=False): |
184 def _iskwfile(f, link, kwt): |
221 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid |
185 return not link(f) and kwt.matcher(f) |
222 rejects or conflicts due to expanded keywords in working dir.''' |
|
223 _patchfile_init(self, ui, fname, missing=missing) |
|
224 |
|
225 if _kwtemplater.matcher(self.fname): |
|
226 # shrink keywords read from working dir |
|
227 kwshrunk = _kwtemplater.shrink(''.join(self.lines)) |
|
228 self.lines = kwshrunk.splitlines(True) |
|
229 |
|
230 |
|
231 def _iskwfile(f, link): |
|
232 return not link(f) and _kwtemplater.matcher(f) |
186 |
233 |
187 def _status(ui, repo, *pats, **opts): |
234 def _status(ui, repo, *pats, **opts): |
188 '''Bails out if [keyword] configuration is not active. |
235 '''Bails out if [keyword] configuration is not active. |
189 Returns status of working directory.''' |
236 Returns status of working directory.''' |
190 if hasattr(repo, '_kwt'): |
237 if _kwtemplater: |
191 files, match, anypats = cmdutil.matchpats(repo, pats, opts) |
238 files, match, anypats = cmdutil.matchpats(repo, pats, opts) |
192 return repo.status(files=files, match=match, list_clean=True) |
239 return repo.status(files=files, match=match, list_clean=True) |
193 if ui.configitems('keyword'): |
240 if ui.configitems('keyword'): |
194 raise util.Abort(_('[keyword] patterns cannot match')) |
241 raise util.Abort(_('[keyword] patterns cannot match')) |
195 raise util.Abort(_('no [keyword] patterns configured')) |
242 raise util.Abort(_('no [keyword] patterns configured')) |
196 |
243 |
197 def _overwrite(ui, repo, node=None, expand=True, files=None): |
244 def _overwrite(ui, repo, node=None, expand=True, files=None): |
198 '''Overwrites selected files expanding/shrinking keywords.''' |
245 '''Overwrites selected files expanding/shrinking keywords.''' |
199 ctx = repo.changectx(node) |
246 ctx = repo.changectx(node) |
200 mf = ctx.manifest() |
247 mf = ctx.manifest() |
201 if node is not None: |
248 if node is not None: # commit |
202 # commit |
249 _kwtemplater.commitnode = node |
203 files = [f for f in ctx.files() if f in mf] |
250 files = [f for f in ctx.files() if f in mf] |
204 notify = ui.debug |
251 notify = ui.debug |
205 else: |
252 else: # kwexpand/kwshrink |
206 # kwexpand/kwshrink |
|
207 notify = ui.note |
253 notify = ui.note |
208 candidates = [f for f in files if _iskwfile(f, mf.linkf, repo._kwt)] |
254 candidates = [f for f in files if _iskwfile(f, mf.linkf)] |
209 if candidates: |
255 if candidates: |
210 candidates.sort() |
256 candidates.sort() |
211 action = expand and 'expanding' or 'shrinking' |
257 action = expand and 'expanding' or 'shrinking' |
212 for f in candidates: |
258 for f in candidates: |
213 data, kwfound = repo._wreadkwct(f, expand, ctx, node) |
259 fp = repo.file(f, kwmatch=True) |
|
260 data, kwfound = fp.kwctread(mf[f], expand) |
214 if kwfound: |
261 if kwfound: |
215 notify(_('overwriting %s %s keywords\n') % (f, action)) |
262 notify(_('overwriting %s %s keywords\n') % (f, action)) |
216 repo.wwrite(f, data, mf.flags(f), overwrite=True) |
263 repo.wwrite(f, data, mf.flags(f)) |
217 repo.dirstate.normal(f) |
264 repo.dirstate.normal(f) |
218 |
265 |
219 def _kwfwrite(ui, repo, expand, *pats, **opts): |
266 def _kwfwrite(ui, repo, expand, *pats, **opts): |
220 '''Selects files and passes them to _overwrite.''' |
267 '''Selects files and passes them to _overwrite.''' |
221 status = _status(ui, repo, *pats, **opts) |
268 status = _status(ui, repo, *pats, **opts) |
384 # 3rd argument sets expansion to False |
402 # 3rd argument sets expansion to False |
385 _kwfwrite(ui, repo, False, *pats, **opts) |
403 _kwfwrite(ui, repo, False, *pats, **opts) |
386 |
404 |
387 |
405 |
388 def reposetup(ui, repo): |
406 def reposetup(ui, repo): |
389 if not repo.local() or repo.root.endswith('/.hg/patches'): |
407 '''Sets up repo as kwrepo for keyword substitution. |
|
408 Overrides file method to return kwfilelog instead of filelog |
|
409 if file matches user configuration. |
|
410 Wraps commit to overwrite configured files with updated |
|
411 keyword substitutions. |
|
412 This is done for local repos only, and only if there are |
|
413 files configured at all for keyword substitution.''' |
|
414 |
|
415 if not repo.local(): |
390 return |
416 return |
391 |
417 |
392 inc, exc = [], ['.hgtags', '.hg_archival.txt'] |
418 hgcmd, func, args, opts, cmdopts = dispatch._parse(ui, sys.argv[1:]) |
|
419 if hgcmd in nokwcommands.split(): |
|
420 return |
|
421 |
|
422 if hgcmd == 'diff': |
|
423 # only expand if comparing against working dir |
|
424 node1, node2 = cmdutil.revpair(repo, cmdopts.get('rev')) |
|
425 if node2 is not None: |
|
426 return |
|
427 # shrink if rev is not current node |
|
428 if node1 is not None and node1 != repo.changectx().node(): |
|
429 hgcmd = 'diff1' |
|
430 |
|
431 inc, exc = [], ['.hgtags'] |
393 for pat, opt in ui.configitems('keyword'): |
432 for pat, opt in ui.configitems('keyword'): |
394 if opt != 'ignore': |
433 if opt != 'ignore': |
395 inc.append(pat) |
434 inc.append(pat) |
396 else: |
435 else: |
397 exc.append(pat) |
436 exc.append(pat) |
398 if not inc: |
437 if not inc: |
399 return |
438 return |
400 |
439 |
|
440 global _kwtemplater |
|
441 _restricted = hgcmd in restricted.split() |
|
442 _kwtemplater = kwtemplater(ui, repo, inc, exc, _restricted) |
|
443 |
401 class kwrepo(repo.__class__): |
444 class kwrepo(repo.__class__): |
402 def _wreadkwct(self, filename, expand, ctx, node): |
445 def file(self, f, kwmatch=False): |
403 '''Reads filename and returns tuple of data with keywords |
446 if f[0] == '/': |
404 expanded/shrunk and count of keywords (for _overwrite).''' |
447 f = f[1:] |
405 data = super(kwrepo, self).wread(filename) |
448 if kwmatch or _kwtemplater.matcher(f): |
406 return self._kwt.process(filename, data, expand, ctx, node) |
449 return kwfilelog(self.sopener, f) |
|
450 return filelog.filelog(self.sopener, f) |
407 |
451 |
408 def wread(self, filename): |
452 def wread(self, filename): |
409 data = super(kwrepo, self).wread(filename) |
453 data = super(kwrepo, self).wread(filename) |
410 if self._kwt.matcher(filename): |
454 if _restricted and _kwtemplater.matcher(filename): |
411 return self._kwt.shrink(data) |
455 return _kwtemplater.shrink(data) |
412 return data |
456 return data |
413 |
|
414 def wwrite(self, filename, data, flags, overwrite=False): |
|
415 if not overwrite and self._kwt.matcher(filename): |
|
416 data = self._kwt.expand(filename, data, None) |
|
417 super(kwrepo, self).wwrite(filename, data, flags) |
|
418 |
|
419 def wwritedata(self, filename, data): |
|
420 if self._kwt.matcher(filename): |
|
421 data = self._kwt.expand(filename, data, None) |
|
422 return super(kwrepo, self).wwritedata(filename, data) |
|
423 |
457 |
424 def commit(self, files=None, text='', user=None, date=None, |
458 def commit(self, files=None, text='', user=None, date=None, |
425 match=util.always, force=False, force_editor=False, |
459 match=util.always, force=False, force_editor=False, |
426 p1=None, p2=None, extra={}, empty_ok=False): |
460 p1=None, p2=None, extra={}, empty_ok=False): |
427 wlock = lock = None |
461 wlock = lock = None |
462 repo.hook('commit', node=node, parent1=_p1, parent2=_p2) |
496 repo.hook('commit', node=node, parent1=_p1, parent2=_p2) |
463 return node |
497 return node |
464 finally: |
498 finally: |
465 del wlock, lock |
499 del wlock, lock |
466 |
500 |
467 kwt = kwrepo._kwt = kwtemplater(ui, repo, inc, exc) |
|
468 |
|
469 def kwpatchfile_init(self, ui, fname, missing=False): |
|
470 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid |
|
471 rejects or conflicts due to expanded keywords in working dir.''' |
|
472 _patchfile_init(self, ui, fname, missing=missing) |
|
473 |
|
474 if kwt.matcher(self.fname): |
|
475 # shrink keywords read from working dir |
|
476 kwshrunk = kwt.shrink(''.join(self.lines)) |
|
477 self.lines = kwshrunk.splitlines(True) |
|
478 |
|
479 def kwweb_rawfile(web, req, tmpl): |
|
480 '''Monkeypatch webcommands.rawfile so it expands keywords.''' |
|
481 path = web.cleanpath(req.form.get('file', [''])[0]) |
|
482 if not path: |
|
483 content = web.manifest(tmpl, web.changectx(req), path) |
|
484 req.respond(webcommands.HTTP_OK, web.ctype) |
|
485 return content |
|
486 try: |
|
487 fctx = web.filectx(req) |
|
488 except revlog.LookupError: |
|
489 content = web.manifest(tmpl, web.changectx(req), path) |
|
490 req.respond(webcommands.HTTP_OK, web.ctype) |
|
491 return content |
|
492 path = fctx.path() |
|
493 text = fctx.data() |
|
494 if kwt.matcher(path): |
|
495 text = kwt.expand(path, text, web.changectx(req)) |
|
496 mt = mimetypes.guess_type(path)[0] |
|
497 if mt is None or util.binary(text): |
|
498 mt = mt or 'application/octet-stream' |
|
499 req.respond(webcommands.HTTP_OK, mt, path, len(text)) |
|
500 return [text] |
|
501 |
|
502 repo.__class__ = kwrepo |
501 repo.__class__ = kwrepo |
503 patch.patchfile.__init__ = kwpatchfile_init |
502 patch.patchfile.__init__ = _kwpatchfile_init |
504 webcommands.rawfile = kwweb_rawfile |
|
505 |
503 |
506 |
504 |
507 cmdtable = { |
505 cmdtable = { |
508 'kwcat': |
|
509 (cat, commands.table['cat'][1], |
|
510 _('hg kwcat [OPTION]... FILE...')), |
|
511 'kwdemo': |
506 'kwdemo': |
512 (demo, |
507 (demo, |
513 [('d', 'default', None, _('show default keyword template maps')), |
508 [('d', 'default', None, _('show default keyword template maps')), |
514 ('f', 'rcfile', [], _('read maps from rcfile'))], |
509 ('f', 'rcfile', [], _('read maps from rcfile'))], |
515 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')), |
510 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')), |