80 Expansions spanning more than one line and incremental expansions, |
80 Expansions spanning more than one line and incremental expansions, |
81 like CVS' $Log$, are not supported. A keyword template map "Log = |
81 like CVS' $Log$, are not supported. A keyword template map "Log = |
82 {desc}" expands to the first line of the changeset description. |
82 {desc}" expands to the first line of the changeset description. |
83 ''' |
83 ''' |
84 |
84 |
85 from mercurial import commands, cmdutil, dispatch, filelog, revlog, extensions |
85 from mercurial import commands, cmdutil, dispatch, filelog, extensions |
86 from mercurial import patch, localrepo, templater, templatefilters, util, match |
86 from mercurial import localrepo, match, patch, templatefilters, templater, util |
87 from mercurial.hgweb import webcommands |
87 from mercurial.hgweb import webcommands |
88 from mercurial.i18n import _ |
88 from mercurial.i18n import _ |
89 import re, shutil, tempfile |
89 import re, shutil, tempfile |
90 |
90 |
91 commands.optionalrepo += ' kwdemo' |
91 commands.optionalrepo += ' kwdemo' |
92 |
92 |
93 # hg commands that do not act on keywords |
93 # hg commands that do not act on keywords |
94 nokwcommands = ('add addremove annotate bundle copy export grep incoming init' |
94 nokwcommands = ('add addremove annotate bundle export grep incoming init log' |
95 ' log outgoing push rename tip verify convert email glog') |
95 ' outgoing push tip verify convert email glog') |
96 |
96 |
97 # hg commands that trigger expansion only when writing to working dir, |
97 # hg commands that trigger expansion only when writing to working dir, |
98 # not when reading filelog, and unexpand when reading from working dir |
98 # not when reading filelog, and unexpand when reading from working dir |
99 restricted = 'merge record qrecord resolve transplant' |
99 restricted = 'merge kwexpand kwshrink record qrecord resolve transplant' |
100 |
100 |
101 # commands using dorecord |
|
102 recordcommands = 'record qrecord' |
|
103 # names of extensions using dorecord |
101 # names of extensions using dorecord |
104 recordextensions = 'record' |
102 recordextensions = 'record' |
105 |
103 |
106 # date like in cvs' $Date |
104 # date like in cvs' $Date |
107 utcdate = lambda x: util.datestr((x[0], 0), '%Y/%m/%d %H:%M:%S') |
105 utcdate = lambda x: util.datestr((x[0], 0), '%Y/%m/%d %H:%M:%S') |
147 def __init__(self, ui, repo, inc, exc): |
151 def __init__(self, ui, repo, inc, exc): |
148 self.ui = ui |
152 self.ui = ui |
149 self.repo = repo |
153 self.repo = repo |
150 self.match = match.match(repo.root, '', [], inc, exc) |
154 self.match = match.match(repo.root, '', [], inc, exc) |
151 self.restrict = kwtools['hgcmd'] in restricted.split() |
155 self.restrict = kwtools['hgcmd'] in restricted.split() |
152 self.record = kwtools['hgcmd'] in recordcommands.split() |
156 self.record = False |
153 |
157 |
154 kwmaps = self.ui.configitems('keywordmaps') |
158 kwmaps = self.ui.configitems('keywordmaps') |
155 if kwmaps: # override default templates |
159 if kwmaps: # override default templates |
156 self.templates = dict((k, templater.parsestring(v, False)) |
160 self.templates = dict((k, templater.parsestring(v, False)) |
157 for k, v in kwmaps) |
161 for k, v in kwmaps) |
158 else: |
162 else: |
159 self.templates = _defaultkwmaps(self.ui) |
163 self.templates = _defaultkwmaps(self.ui) |
160 escaped = map(re.escape, self.templates.keys()) |
164 escaped = '|'.join(map(re.escape, self.templates.keys())) |
161 kwpat = r'\$(%s)(: [^$\n\r]*? )??\$' % '|'.join(escaped) |
165 self.re_kw = re.compile(r'\$(%s)\$' % escaped) |
162 self.re_kw = re.compile(kwpat) |
166 self.re_kwexp = re.compile(r'\$(%s): [^$\n\r]*? \$' % escaped) |
163 |
167 |
164 templatefilters.filters.update({'utcdate': utcdate, |
168 templatefilters.filters.update({'utcdate': utcdate, |
165 'svnisodate': svnisodate, |
169 'svnisodate': svnisodate, |
166 'svnutcdate': svnutcdate}) |
170 'svnutcdate': svnutcdate}) |
167 |
171 |
183 if not self.restrict and self.match(path) and not util.binary(data): |
187 if not self.restrict and self.match(path) and not util.binary(data): |
184 ctx = self.repo.filectx(path, fileid=node).changectx() |
188 ctx = self.repo.filectx(path, fileid=node).changectx() |
185 return self.substitute(data, path, ctx, self.re_kw.sub) |
189 return self.substitute(data, path, ctx, self.re_kw.sub) |
186 return data |
190 return data |
187 |
191 |
188 def iskwfile(self, path, flagfunc): |
192 def iskwfile(self, cand, ctx): |
189 '''Returns true if path matches [keyword] pattern |
193 '''Returns subset of candidates which are configured for keyword |
190 and is not a symbolic link. |
194 expansion are not symbolic links.''' |
191 Caveat: localrepository._link fails on Windows.''' |
195 return [f for f in cand if self.match(f) and not 'l' in ctx.flags(f)] |
192 return self.match(path) and not 'l' in flagfunc(path) |
196 |
193 |
197 def overwrite(self, ctx, candidates, lookup, expand, recsubn=None): |
194 def overwrite(self, ctx, candidates, iswctx, expand, cfiles): |
|
195 '''Overwrites selected files expanding/shrinking keywords.''' |
198 '''Overwrites selected files expanding/shrinking keywords.''' |
196 if cfiles is not None: |
199 if self.restrict or lookup: # exclude kw_copy |
197 candidates = [f for f in candidates if f in cfiles] |
200 candidates = self.iskwfile(candidates, ctx) |
198 candidates = [f for f in candidates if self.iskwfile(f, ctx.flags)] |
201 if not candidates: |
199 if candidates: |
202 return |
200 restrict = self.restrict |
203 commit = self.restrict and not lookup |
201 self.restrict = True # do not expand when reading |
204 if self.restrict or expand and lookup: |
202 rollback = kwtools['hgcmd'] == 'rollback' |
|
203 mf = ctx.manifest() |
205 mf = ctx.manifest() |
204 msg = (expand and _('overwriting %s expanding keywords\n') |
206 fctx = ctx |
205 or _('overwriting %s shrinking keywords\n')) |
207 subn = (self.restrict and self.re_kw.subn or |
206 for f in candidates: |
208 recsubn or self.re_kwexp.subn) |
207 if not self.record and not rollback: |
209 msg = (expand and _('overwriting %s expanding keywords\n') |
208 data = self.repo.file(f).read(mf[f]) |
210 or _('overwriting %s shrinking keywords\n')) |
209 else: |
211 for f in candidates: |
210 data = self.repo.wread(f) |
212 if self.restrict: |
211 if util.binary(data): |
213 data = self.repo.file(f).read(mf[f]) |
212 continue |
214 else: |
213 if expand: |
215 data = self.repo.wread(f) |
214 if iswctx: |
216 if util.binary(data): |
215 ctx = self.repo.filectx(f, fileid=mf[f]).changectx() |
217 continue |
216 data, found = self.substitute(data, f, ctx, |
218 if expand: |
217 self.re_kw.subn) |
219 if lookup: |
218 else: |
220 fctx = self.repo.filectx(f, fileid=mf[f]).changectx() |
219 found = self.re_kw.search(data) |
221 data, found = self.substitute(data, f, fctx, subn) |
220 if found: |
222 elif self.restrict: |
221 self.ui.note(msg % f) |
223 found = self.re_kw.search(data) |
222 self.repo.wwrite(f, data, mf.flags(f)) |
224 else: |
223 if iswctx and not rollback: |
225 data, found = _shrinktext(data, subn) |
224 self.repo.dirstate.normal(f) |
226 if found: |
225 elif self.record: |
227 self.ui.note(msg % f) |
226 self.repo.dirstate.normallookup(f) |
228 self.repo.wwrite(f, data, ctx.flags(f)) |
227 self.restrict = restrict |
229 if commit: |
228 |
230 self.repo.dirstate.normal(f) |
229 def shrinktext(self, text): |
231 elif self.record: |
230 '''Unconditionally removes all keyword substitutions from text.''' |
232 self.repo.dirstate.normallookup(f) |
231 return self.re_kw.sub(r'$\1$', text) |
|
232 |
233 |
233 def shrink(self, fname, text): |
234 def shrink(self, fname, text): |
234 '''Returns text with all keyword substitutions removed.''' |
235 '''Returns text with all keyword substitutions removed.''' |
235 if self.match(fname) and not util.binary(text): |
236 if self.match(fname) and not util.binary(text): |
236 return self.shrinktext(text) |
237 return _shrinktext(text, self.re_kwexp.sub) |
237 return text |
238 return text |
238 |
239 |
239 def shrinklines(self, fname, lines): |
240 def shrinklines(self, fname, lines): |
240 '''Returns lines with keyword substitutions removed.''' |
241 '''Returns lines with keyword substitutions removed.''' |
241 if self.match(fname): |
242 if self.match(fname): |
242 text = ''.join(lines) |
243 text = ''.join(lines) |
243 if not util.binary(text): |
244 if not util.binary(text): |
244 return self.shrinktext(text).splitlines(True) |
245 return _shrinktext(text, self.re_kwexp.sub).splitlines(True) |
245 return lines |
246 return lines |
246 |
247 |
247 def wread(self, fname, data): |
248 def wread(self, fname, data): |
248 '''If in restricted mode returns data read from wdir with |
249 '''If in restricted mode returns data read from wdir with |
249 keyword substitutions removed.''' |
250 keyword substitutions removed.''' |
260 self.path = path |
261 self.path = path |
261 |
262 |
262 def read(self, node): |
263 def read(self, node): |
263 '''Expands keywords when reading filelog.''' |
264 '''Expands keywords when reading filelog.''' |
264 data = super(kwfilelog, self).read(node) |
265 data = super(kwfilelog, self).read(node) |
|
266 if self.renamed(node): |
|
267 return data |
265 return self.kwt.expand(self.path, node, data) |
268 return self.kwt.expand(self.path, node, data) |
266 |
269 |
267 def add(self, text, meta, tr, link, p1=None, p2=None): |
270 def add(self, text, meta, tr, link, p1=None, p2=None): |
268 '''Removes keyword substitutions when adding to filelog.''' |
271 '''Removes keyword substitutions when adding to filelog.''' |
269 text = self.kwt.shrink(self.path, text) |
272 text = self.kwt.shrink(self.path, text) |
270 return super(kwfilelog, self).add(text, meta, tr, link, p1, p2) |
273 return super(kwfilelog, self).add(text, meta, tr, link, p1, p2) |
271 |
274 |
272 def cmp(self, node, text): |
275 def cmp(self, node, text): |
273 '''Removes keyword substitutions for comparison.''' |
276 '''Removes keyword substitutions for comparison.''' |
274 text = self.kwt.shrink(self.path, text) |
277 text = self.kwt.shrink(self.path, text) |
275 if self.renamed(node): |
278 return super(kwfilelog, self).cmp(node, text) |
276 t2 = super(kwfilelog, self).read(node) |
|
277 return t2 != text |
|
278 return revlog.revlog.cmp(self, node, text) |
|
279 |
279 |
280 def _status(ui, repo, kwt, *pats, **opts): |
280 def _status(ui, repo, kwt, *pats, **opts): |
281 '''Bails out if [keyword] configuration is not active. |
281 '''Bails out if [keyword] configuration is not active. |
282 Returns status of working directory.''' |
282 Returns status of working directory.''' |
283 if kwt: |
283 if kwt: |
297 try: |
297 try: |
298 status = _status(ui, repo, kwt, *pats, **opts) |
298 status = _status(ui, repo, kwt, *pats, **opts) |
299 modified, added, removed, deleted, unknown, ignored, clean = status |
299 modified, added, removed, deleted, unknown, ignored, clean = status |
300 if modified or added or removed or deleted: |
300 if modified or added or removed or deleted: |
301 raise util.Abort(_('outstanding uncommitted changes')) |
301 raise util.Abort(_('outstanding uncommitted changes')) |
302 kwt.overwrite(wctx, clean, True, expand, None) |
302 kwt.overwrite(wctx, clean, True, expand) |
303 finally: |
303 finally: |
304 wlock.release() |
304 wlock.release() |
305 |
305 |
306 def demo(ui, repo, *args, **opts): |
306 def demo(ui, repo, *args, **opts): |
307 '''print [keywordmaps] configuration and an expansion example |
307 '''print [keywordmaps] configuration and an expansion example |
413 modified, added, removed, deleted, unknown, ignored, clean = status |
413 modified, added, removed, deleted, unknown, ignored, clean = status |
414 files = [] |
414 files = [] |
415 if not opts.get('unknown') or opts.get('all'): |
415 if not opts.get('unknown') or opts.get('all'): |
416 files = sorted(modified + added + clean) |
416 files = sorted(modified + added + clean) |
417 wctx = repo[None] |
417 wctx = repo[None] |
418 kwfiles = [f for f in files if kwt.iskwfile(f, wctx.flags)] |
418 kwfiles = kwt.iskwfile(files, wctx) |
419 kwunknown = [f for f in unknown if kwt.iskwfile(f, wctx.flags)] |
419 kwunknown = kwt.iskwfile(unknown, wctx) |
420 if not opts.get('ignore') or opts.get('all'): |
420 if not opts.get('ignore') or opts.get('all'): |
421 showfiles = kwfiles, kwunknown |
421 showfiles = kwfiles, kwunknown |
422 else: |
422 else: |
423 showfiles = [], [] |
423 showfiles = [], [] |
424 if opts.get('all') or opts.get('ignore'): |
424 if opts.get('all') or opts.get('ignore'): |
500 |
500 |
501 def kwcommitctx(self, ctx, error=False): |
501 def kwcommitctx(self, ctx, error=False): |
502 n = super(kwrepo, self).commitctx(ctx, error) |
502 n = super(kwrepo, self).commitctx(ctx, error) |
503 # no lock needed, only called from repo.commit() which already locks |
503 # no lock needed, only called from repo.commit() which already locks |
504 if not kwt.record: |
504 if not kwt.record: |
|
505 restrict = kwt.restrict |
|
506 kwt.restrict = True |
505 kwt.overwrite(self[n], sorted(ctx.added() + ctx.modified()), |
507 kwt.overwrite(self[n], sorted(ctx.added() + ctx.modified()), |
506 False, True, None) |
508 False, True) |
|
509 kwt.restrict = restrict |
507 return n |
510 return n |
508 |
511 |
509 def rollback(self, dryrun=False): |
512 def rollback(self, dryrun=False): |
510 wlock = repo.wlock() |
513 wlock = repo.wlock() |
511 try: |
514 try: |
512 if not dryrun: |
515 if not dryrun: |
513 cfiles = self['.'].files() |
516 changed = self['.'].files() |
514 ret = super(kwrepo, self).rollback(dryrun) |
517 ret = super(kwrepo, self).rollback(dryrun) |
515 if not dryrun: |
518 if not dryrun: |
516 ctx = self['.'] |
519 ctx = self['.'] |
517 modified, added = super(kwrepo, self).status()[:2] |
520 modified, added = self[None].status()[:2] |
518 kwt.overwrite(ctx, added, True, False, cfiles) |
521 modified = [f for f in modified if f in changed] |
519 kwt.overwrite(ctx, modified, True, True, cfiles) |
522 added = [f for f in added if f in changed] |
|
523 kwt.overwrite(ctx, added, True, False) |
|
524 kwt.overwrite(ctx, modified, True, True) |
520 return ret |
525 return ret |
521 finally: |
526 finally: |
522 wlock.release() |
527 wlock.release() |
523 |
528 |
524 # monkeypatches |
529 # monkeypatches |
539 def kwweb_skip(orig, web, req, tmpl): |
544 def kwweb_skip(orig, web, req, tmpl): |
540 '''Wraps webcommands.x turning off keyword expansion.''' |
545 '''Wraps webcommands.x turning off keyword expansion.''' |
541 kwt.match = util.never |
546 kwt.match = util.never |
542 return orig(web, req, tmpl) |
547 return orig(web, req, tmpl) |
543 |
548 |
|
549 def kw_copy(orig, ui, repo, pats, opts, rename=False): |
|
550 '''Wraps cmdutil.copy so that copy/rename destinations do not |
|
551 contain expanded keywords. |
|
552 Note that the source may also be a symlink as: |
|
553 hg cp sym x -> x is symlink |
|
554 cp sym x; hg cp -A sym x -> x is file (maybe expanded keywords) |
|
555 ''' |
|
556 orig(ui, repo, pats, opts, rename) |
|
557 if opts.get('dry_run'): |
|
558 return |
|
559 wctx = repo[None] |
|
560 candidates = [f for f in repo.dirstate.copies() if |
|
561 kwt.match(repo.dirstate.copied(f)) and |
|
562 not 'l' in wctx.flags(f)] |
|
563 kwt.overwrite(wctx, candidates, False, False) |
|
564 |
544 def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts): |
565 def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts): |
545 '''Wraps record.dorecord expanding keywords after recording.''' |
566 '''Wraps record.dorecord expanding keywords after recording.''' |
546 wlock = repo.wlock() |
567 wlock = repo.wlock() |
547 try: |
568 try: |
548 # record returns 0 even when nothing has changed |
569 # record returns 0 even when nothing has changed |
549 # therefore compare nodes before and after |
570 # therefore compare nodes before and after |
|
571 kwt.record = True |
550 ctx = repo['.'] |
572 ctx = repo['.'] |
|
573 modified, added = repo[None].status()[:2] |
551 ret = orig(ui, repo, commitfunc, *pats, **opts) |
574 ret = orig(ui, repo, commitfunc, *pats, **opts) |
552 recordctx = repo['.'] |
575 recctx = repo['.'] |
553 if ctx != recordctx: |
576 if ctx != recctx: |
554 kwt.overwrite(recordctx, recordctx.files(), |
577 modified = [f for f in modified if f in recctx] |
555 False, True, recordctx) |
578 added = [f for f in added if f in recctx] |
|
579 kwt.restrict = False |
|
580 kwt.overwrite(recctx, modified, False, True, kwt.re_kwexp.subn) |
|
581 kwt.overwrite(recctx, added, False, True, kwt.re_kw.subn) |
|
582 kwt.restrict = True |
556 return ret |
583 return ret |
557 finally: |
584 finally: |
558 wlock.release() |
585 wlock.release() |
559 |
586 |
560 repo.__class__ = kwrepo |
587 repo.__class__ = kwrepo |
561 |
588 |
562 extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init) |
589 extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init) |
563 extensions.wrapfunction(patch, 'diff', kw_diff) |
590 extensions.wrapfunction(patch, 'diff', kw_diff) |
|
591 extensions.wrapfunction(cmdutil, 'copy', kw_copy) |
564 for c in 'annotate changeset rev filediff diff'.split(): |
592 for c in 'annotate changeset rev filediff diff'.split(): |
565 extensions.wrapfunction(webcommands, c, kwweb_skip) |
593 extensions.wrapfunction(webcommands, c, kwweb_skip) |
566 for name in recordextensions.split(): |
594 for name in recordextensions.split(): |
567 try: |
595 try: |
568 record = extensions.find(name) |
596 record = extensions.find(name) |