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, dispatch, filelog |
83 from mercurial import commands, cmdutil, context, localrepo |
84 from mercurial import patch, localrepo, revlog, templater, util |
84 from mercurial import patch, revlog, templater, util |
85 from mercurial.node import * |
85 from mercurial.node import * |
86 from mercurial.i18n import _ |
86 from mercurial.i18n import _ |
87 import re, shutil, sys, tempfile, time |
87 import re, shutil, tempfile, time |
88 |
88 |
89 commands.optionalrepo += ' kwdemo' |
89 commands.optionalrepo += ' kwdemo' |
90 |
|
91 # handle for external callers |
|
92 externalcall = None, None, {} |
|
93 |
|
94 def externalcmdhook(hgcmd, *args, **opts): |
|
95 '''Hook for external callers to pass hg commands to keyword. |
|
96 |
|
97 Caveat: hgcmd, args, opts are not checked for validity. |
|
98 This is the responsibility of the caller. |
|
99 |
|
100 hgmcd can be either the hg function object, eg diff or patch, |
|
101 or its string represenation, eg 'diff' or 'patch'.''' |
|
102 global externalcall |
|
103 if not isinstance(hgcmd, str): |
|
104 hgcmd = hgcmd.__name__.split('.')[-1] |
|
105 externalcall = hgcmd, args, opts |
|
106 |
|
107 # hg commands that trigger expansion only when writing to working dir, |
|
108 # not when reading filelog, and unexpand when reading from working dir |
|
109 restricted = ('diff1', 'record', |
|
110 'qfold', 'qimport', 'qnew', 'qpush', 'qrefresh', 'qrecord') |
|
111 |
90 |
112 def utcdate(date): |
91 def utcdate(date): |
113 '''Returns hgdate in cvs-like UTC format.''' |
92 '''Returns hgdate in cvs-like UTC format.''' |
114 return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0])) |
93 return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0])) |
115 |
|
116 |
94 |
117 _kwtemplater = None |
95 _kwtemplater = None |
118 |
96 |
119 class kwtemplater(object): |
97 class kwtemplater(object): |
120 ''' |
98 ''' |
129 'Source': '{root}/{file},v', |
107 'Source': '{root}/{file},v', |
130 'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}', |
108 'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}', |
131 'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}', |
109 'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}', |
132 } |
110 } |
133 |
111 |
134 def __init__(self, ui, repo, inc, exc, hgcmd): |
112 def __init__(self, ui, repo, inc, exc): |
135 self.ui = ui |
113 self.ui = ui |
136 self.repo = repo |
114 self.repo = repo |
137 self.matcher = util.matcher(repo.root, inc=inc, exc=exc)[1] |
115 self.matcher = util.matcher(repo.root, inc=inc, exc=exc)[1] |
138 self.hgcmd = hgcmd |
116 self.ctx = None |
139 self.commitnode = None |
|
140 self.path = '' |
|
141 |
117 |
142 kwmaps = self.ui.configitems('keywordmaps') |
118 kwmaps = self.ui.configitems('keywordmaps') |
143 if kwmaps: # override default templates |
119 if kwmaps: # override default templates |
144 kwmaps = [(k, templater.parsestring(v, quoted=False)) |
120 kwmaps = [(k, templater.parsestring(v, quoted=False)) |
145 for (k, v) in kwmaps] |
121 for (k, v) in kwmaps] |
150 |
126 |
151 templater.common_filters['utcdate'] = utcdate |
127 templater.common_filters['utcdate'] = utcdate |
152 self.ct = cmdutil.changeset_templater(self.ui, self.repo, |
128 self.ct = cmdutil.changeset_templater(self.ui, self.repo, |
153 False, '', False) |
129 False, '', False) |
154 |
130 |
155 def substitute(self, node, data, subfunc): |
131 def substitute(self, path, data, node, subfunc): |
156 '''Obtains file's changenode if commit node not given, |
132 '''Obtains file's changenode if node not given, |
157 and calls given substitution function.''' |
133 and calls given substitution function.''' |
158 if self.commitnode: |
134 if node is None: |
159 fnode = self.commitnode |
135 # kwrepo.wwrite except when overwriting on commit |
160 else: |
136 if self.ctx is None: |
161 c = context.filectx(self.repo, self.path, fileid=node) |
137 self.ctx = self.repo.changectx() |
162 fnode = c.node() |
138 fnode = self.ctx.filenode(path) |
|
139 fl = self.repo.file(path) |
|
140 c = context.filectx(self.repo, path, fileid=fnode, filelog=fl) |
|
141 node = c.node() |
163 |
142 |
164 def kwsub(mobj): |
143 def kwsub(mobj): |
165 '''Substitutes keyword using corresponding template.''' |
144 '''Substitutes keyword using corresponding template.''' |
166 kw = mobj.group(1) |
145 kw = mobj.group(1) |
167 self.ct.use_template(self.templates[kw]) |
146 self.ct.use_template(self.templates[kw]) |
168 self.ui.pushbuffer() |
147 self.ui.pushbuffer() |
169 self.ct.show(changenode=fnode, root=self.repo.root, file=self.path) |
148 self.ct.show(changenode=node, root=self.repo.root, file=path) |
170 return '$%s: %s $' % (kw, templater.firstline(self.ui.popbuffer())) |
149 return '$%s: %s $' % (kw, templater.firstline(self.ui.popbuffer())) |
171 |
150 |
172 return subfunc(kwsub, data) |
151 return subfunc(kwsub, data) |
173 |
152 |
174 def expand(self, node, data): |
153 def expand(self, path, data): |
175 '''Returns data with keywords expanded.''' |
154 '''Returns data with keywords expanded.''' |
176 if util.binary(data) or self.hgcmd in restricted: |
155 if util.binary(data): |
177 return data |
156 return data |
178 return self.substitute(node, data, self.re_kw.sub) |
157 return self.substitute(path, data, None, self.re_kw.sub) |
179 |
158 |
180 def process(self, node, data, expand): |
159 def process(self, path, data, expand, ctx, node): |
181 '''Returns a tuple: data, count. |
160 '''Returns a tuple: data, count. |
182 Count is number of keywords/keyword substitutions, |
161 Count is number of keywords/keyword substitutions, |
183 telling caller whether to act on file containing data.''' |
162 telling caller whether to act on file containing data.''' |
184 if util.binary(data): |
163 if util.binary(data): |
185 return data, None |
164 return data, None |
186 if expand: |
165 if expand: |
187 return self.substitute(node, data, self.re_kw.subn) |
166 self.ctx = ctx |
188 return data, self.re_kw.search(data) |
167 return self.substitute(path, data, node, self.re_kw.subn) |
189 |
168 return self.re_kw.subn(r'$\1$', data) |
190 def shrink(self, text): |
169 |
|
170 def shrink(self, data): |
191 '''Returns text with all keyword substitutions removed.''' |
171 '''Returns text with all keyword substitutions removed.''' |
192 if util.binary(text): |
172 if util.binary(data): |
193 return text |
173 return data |
194 return self.re_kw.sub(r'$\1$', text) |
174 return self.re_kw.sub(r'$\1$', data) |
195 |
|
196 class kwfilelog(filelog.filelog): |
|
197 ''' |
|
198 Subclass of filelog to hook into its read, add, cmp methods. |
|
199 Keywords are "stored" unexpanded, and processed on reading. |
|
200 ''' |
|
201 def __init__(self, opener, path): |
|
202 super(kwfilelog, self).__init__(opener, path) |
|
203 _kwtemplater.path = path |
|
204 |
|
205 def kwctread(self, node, expand): |
|
206 '''Reads expanding and counting keywords, called from _overwrite.''' |
|
207 data = super(kwfilelog, self).read(node) |
|
208 return _kwtemplater.process(node, data, expand) |
|
209 |
|
210 def read(self, node): |
|
211 '''Expands keywords when reading filelog.''' |
|
212 data = super(kwfilelog, self).read(node) |
|
213 return _kwtemplater.expand(node, data) |
|
214 |
|
215 def add(self, text, meta, tr, link, p1=None, p2=None): |
|
216 '''Removes keyword substitutions when adding to filelog.''' |
|
217 text = _kwtemplater.shrink(text) |
|
218 return super(kwfilelog, self).add(text, meta, tr, link, p1=p1, p2=p2) |
|
219 |
|
220 def cmp(self, node, text): |
|
221 '''Removes keyword substitutions for comparison.''' |
|
222 text = _kwtemplater.shrink(text) |
|
223 if self.renamed(node): |
|
224 t2 = super(kwfilelog, self).read(node) |
|
225 return t2 != text |
|
226 return revlog.revlog.cmp(self, node, text) |
|
227 |
175 |
228 |
176 |
229 # store original patch.patchfile.__init__ |
177 # store original patch.patchfile.__init__ |
230 _patchfile_init = patch.patchfile.__init__ |
178 _patchfile_init = patch.patchfile.__init__ |
231 |
179 |
255 |
203 |
256 def _overwrite(ui, repo, node=None, expand=True, files=None): |
204 def _overwrite(ui, repo, node=None, expand=True, files=None): |
257 '''Overwrites selected files expanding/shrinking keywords.''' |
205 '''Overwrites selected files expanding/shrinking keywords.''' |
258 ctx = repo.changectx(node) |
206 ctx = repo.changectx(node) |
259 mf = ctx.manifest() |
207 mf = ctx.manifest() |
260 if node is not None: # commit |
208 if node is not None: |
261 _kwtemplater.commitnode = node |
209 # commit |
262 files = [f for f in ctx.files() if f in mf] |
210 files = [f for f in ctx.files() if f in mf] |
263 notify = ui.debug |
211 notify = ui.debug |
264 else: # kwexpand/kwshrink |
212 else: |
|
213 # kwexpand/kwshrink |
265 notify = ui.note |
214 notify = ui.note |
266 candidates = [f for f in files if _iskwfile(f, mf.linkf)] |
215 candidates = [f for f in files if _iskwfile(f, mf.linkf)] |
267 if candidates: |
216 if candidates: |
268 candidates.sort() |
217 candidates.sort() |
269 action = expand and 'expanding' or 'shrinking' |
218 action = expand and 'expanding' or 'shrinking' |
270 for f in candidates: |
219 for f in candidates: |
271 fp = repo.file(f, kwmatch=True) |
220 data, kwfound = repo._wreadkwct(f, expand, ctx, node) |
272 data, kwfound = fp.kwctread(mf[f], expand) |
|
273 if kwfound: |
221 if kwfound: |
274 notify(_('overwriting %s %s keywords\n') % (f, action)) |
222 notify(_('overwriting %s %s keywords\n') % (f, action)) |
275 repo.wwrite(f, data, mf.flags(f)) |
223 repo.wwrite(f, data, mf.flags(f), overwrite=True) |
276 repo.dirstate.normal(f) |
224 repo.dirstate.normal(f) |
277 |
225 |
278 def _kwfwrite(ui, repo, expand, *pats, **opts): |
226 def _kwfwrite(ui, repo, expand, *pats, **opts): |
279 '''Selects files and passes them to _overwrite.''' |
227 '''Selects files and passes them to _overwrite.''' |
280 status = _status(ui, repo, *pats, **opts) |
228 status = _status(ui, repo, *pats, **opts) |
364 ui.note(_('unhooked all commit hooks\n')) |
312 ui.note(_('unhooked all commit hooks\n')) |
365 ui.note('hg -R "%s" ci -m "%s"\n' % (tmpdir, msg)) |
313 ui.note('hg -R "%s" ci -m "%s"\n' % (tmpdir, msg)) |
366 repo.commit(text=msg) |
314 repo.commit(text=msg) |
367 format = ui.verbose and ' in %s' % path or '' |
315 format = ui.verbose and ' in %s' % path or '' |
368 demostatus('%s keywords expanded%s' % (kwstatus, format)) |
316 demostatus('%s keywords expanded%s' % (kwstatus, format)) |
369 ui.write(repo.wread(fn)) |
317 ui.write(repo.wopener(fn).read()) |
370 ui.debug(_('\nremoving temporary repo %s\n') % tmpdir) |
318 ui.debug(_('\nremoving temporary repo %s\n') % tmpdir) |
371 shutil.rmtree(tmpdir, ignore_errors=True) |
319 shutil.rmtree(tmpdir, ignore_errors=True) |
372 |
320 |
373 def expand(ui, repo, *pats, **opts): |
321 def expand(ui, repo, *pats, **opts): |
374 '''expand keywords in working directory |
322 '''expand keywords in working directory |
414 # 3rd argument sets expansion to False |
362 # 3rd argument sets expansion to False |
415 _kwfwrite(ui, repo, False, *pats, **opts) |
363 _kwfwrite(ui, repo, False, *pats, **opts) |
416 |
364 |
417 |
365 |
418 def reposetup(ui, repo): |
366 def reposetup(ui, repo): |
419 '''Sets up repo as kwrepo for keyword substitution. |
|
420 Overrides file method to return kwfilelog instead of filelog |
|
421 if file matches user configuration. |
|
422 Wraps commit to overwrite configured files with updated |
|
423 keyword substitutions. |
|
424 This is done for local repos only, and only if there are |
|
425 files configured at all for keyword substitution.''' |
|
426 |
|
427 if not repo.local(): |
367 if not repo.local(): |
428 return |
368 return |
429 |
369 |
430 nokwcommands = ('add', 'addremove', 'bundle', 'clone', 'copy', |
370 inc, exc = [], ['.hgtags', '.hg_archival.txt'] |
431 'export', 'grep', 'identify', 'incoming', 'init', |
|
432 'log', 'outgoing', 'push', 'remove', 'rename', |
|
433 'rollback', 'tip', |
|
434 'convert') |
|
435 try: |
|
436 hgcmd, func, args, opts, cmdopts = dispatch._parse(ui, sys.argv[1:]) |
|
437 except (cmdutil.UnknownCommand, dispatch.ParseError): |
|
438 # must be an external caller, otherwise Exception would have been |
|
439 # raised at core command line parsing |
|
440 hgcmd, args, cmdopts = externalcall |
|
441 if hgcmd is None: |
|
442 # not an "official" hg command as from command line |
|
443 return |
|
444 if hgcmd in nokwcommands: |
|
445 return |
|
446 |
|
447 if hgcmd == 'diff': |
|
448 # only expand if comparing against working dir |
|
449 node1, node2 = cmdutil.revpair(repo, cmdopts.get('rev')) |
|
450 if node2 is not None: |
|
451 return |
|
452 # shrink if rev is not current node |
|
453 if node1 is not None and node1 != repo.changectx().node(): |
|
454 hgcmd = 'diff1' |
|
455 |
|
456 inc, exc = [], ['.hgtags'] |
|
457 for pat, opt in ui.configitems('keyword'): |
371 for pat, opt in ui.configitems('keyword'): |
458 if opt != 'ignore': |
372 if opt != 'ignore': |
459 inc.append(pat) |
373 inc.append(pat) |
460 else: |
374 else: |
461 exc.append(pat) |
375 exc.append(pat) |
462 if not inc: |
376 if not inc: |
463 return |
377 return |
464 |
378 |
465 global _kwtemplater |
379 global _kwtemplater |
466 _kwtemplater = kwtemplater(ui, repo, inc, exc, hgcmd) |
380 _kwtemplater = kwtemplater(ui, repo, inc, exc) |
467 |
381 |
468 class kwrepo(repo.__class__): |
382 class kwrepo(repo.__class__): |
469 def file(self, f, kwmatch=False): |
383 def _wreadkwct(self, filename, expand, ctx, node): |
470 if f[0] == '/': |
384 '''Reads filename and returns tuple of data with keywords |
471 f = f[1:] |
385 expanded/shrunk and count of keywords (for _overwrite).''' |
472 if kwmatch or _kwtemplater.matcher(f): |
386 data = super(kwrepo, self).wread(filename) |
473 return kwfilelog(self.sopener, f) |
387 return _kwtemplater.process(filename, data, expand, ctx, node) |
474 return filelog.filelog(self.sopener, f) |
|
475 |
388 |
476 def wread(self, filename): |
389 def wread(self, filename): |
477 data = super(kwrepo, self).wread(filename) |
390 data = super(kwrepo, self).wread(filename) |
478 if hgcmd in restricted and _kwtemplater.matcher(filename): |
391 if _kwtemplater.matcher(filename): |
479 return _kwtemplater.shrink(data) |
392 return _kwtemplater.shrink(data) |
480 return data |
393 return data |
|
394 |
|
395 def wwrite(self, filename, data, flags, overwrite=False): |
|
396 if not overwrite and _kwtemplater.matcher(filename): |
|
397 data = _kwtemplater.expand(filename, data) |
|
398 super(kwrepo, self).wwrite(filename, data, flags) |
|
399 |
|
400 def wwritedata(self, filename, data): |
|
401 if _kwtemplater.matcher(filename): |
|
402 data = _kwtemplater.expand(filename, data) |
|
403 return super(kwrepo, self).wwritedata(filename, data) |
481 |
404 |
482 def commit(self, files=None, text='', user=None, date=None, |
405 def commit(self, files=None, text='', user=None, date=None, |
483 match=util.always, force=False, force_editor=False, |
406 match=util.always, force=False, force_editor=False, |
484 p1=None, p2=None, extra={}): |
407 p1=None, p2=None, extra={}): |
485 wlock = lock = None |
408 wlock = lock = None |