|
1 # keyword.py - $Keyword$ expansion for Mercurial |
|
2 # |
|
3 # Copyright 2007-2010 Christian Ebert <blacktrash@gmx.net> |
|
4 # |
|
5 # This software may be used and distributed according to the terms of the |
|
6 # GNU General Public License version 2 or any later version. |
|
7 # |
|
8 # $Id$ |
|
9 # |
|
10 # Keyword expansion hack against the grain of a DSCM |
|
11 # |
|
12 # There are many good reasons why this is not needed in a distributed |
|
13 # SCM, still it may be useful in very small projects based on single |
|
14 # files (like LaTeX packages), that are mostly addressed to an |
|
15 # audience not running a version control system. |
|
16 # |
|
17 # For in-depth discussion refer to |
|
18 # <http://mercurial.selenic.com/wiki/KeywordPlan>. |
|
19 # |
|
20 # Keyword expansion is based on Mercurial's changeset template mappings. |
|
21 # |
|
22 # Binary files are not touched. |
|
23 # |
|
24 # Files to act upon/ignore are specified in the [keyword] section. |
|
25 # Customized keyword template mappings in the [keywordmaps] section. |
|
26 # |
|
27 # Run "hg help keyword" and "hg kwdemo" to get info on configuration. |
|
28 |
|
29 '''expand keywords in tracked files |
|
30 |
|
31 This extension expands RCS/CVS-like or self-customized $Keywords$ in |
|
32 tracked text files selected by your configuration. |
|
33 |
|
34 Keywords are only expanded in local repositories and not stored in the |
|
35 change history. The mechanism can be regarded as a convenience for the |
|
36 current user or for archive distribution. |
|
37 |
|
38 Keywords expand to the changeset data pertaining to the latest change |
|
39 relative to the working directory parent of each file. |
|
40 |
|
41 Configuration is done in the [keyword], [keywordset] and [keywordmaps] |
|
42 sections of hgrc files. |
|
43 |
|
44 Example:: |
|
45 |
|
46 [keyword] |
|
47 # expand keywords in every python file except those matching "x*" |
|
48 **.py = |
|
49 x* = ignore |
|
50 |
|
51 [keywordset] |
|
52 # prefer svn- over cvs-like default keywordmaps |
|
53 svn = True |
|
54 |
|
55 .. note:: |
|
56 The more specific you are in your filename patterns the less you |
|
57 lose speed in huge repositories. |
|
58 |
|
59 For [keywordmaps] template mapping and expansion demonstration and |
|
60 control run :hg:`kwdemo`. See :hg:`help templates` for a list of |
|
61 available templates and filters. |
|
62 |
|
63 Three additional date template filters are provided:: |
|
64 |
|
65 utcdate "2006/09/18 15:13:13" |
|
66 svnutcdate "2006-09-18 15:13:13Z" |
|
67 svnisodate "2006-09-18 08:13:13 -700 (Mon, 18 Sep 2006)" |
|
68 |
|
69 The default template mappings (view with :hg:`kwdemo -d`) can be |
|
70 replaced with customized keywords and templates. Again, run |
|
71 :hg:`kwdemo` to control the results of your configuration changes. |
|
72 |
|
73 Before changing/disabling active keywords, run :hg:`kwshrink` to avoid |
|
74 the risk of inadvertently storing expanded keywords in the change |
|
75 history. |
|
76 |
|
77 To force expansion after enabling it, or a configuration change, run |
|
78 :hg:`kwexpand`. |
|
79 |
|
80 Expansions spanning more than one line and incremental expansions, |
|
81 like CVS' $Log$, are not supported. A keyword template map "Log = |
|
82 {desc}" expands to the first line of the changeset description. |
|
83 ''' |
|
84 |
|
85 from mercurial import commands, context, cmdutil, dispatch, filelog, extensions |
|
86 from mercurial import localrepo, match, patch, templatefilters, templater, util |
|
87 from mercurial.hgweb import webcommands |
|
88 from mercurial.i18n import _ |
|
89 import os, re, shutil, tempfile |
|
90 |
|
91 commands.optionalrepo += ' kwdemo' |
|
92 |
|
93 # hg commands that do not act on keywords |
|
94 nokwcommands = ('add addremove annotate bundle export grep incoming init log' |
|
95 ' outgoing push tip verify convert email glog') |
|
96 |
|
97 # hg commands that trigger expansion only when writing to working dir, |
|
98 # not when reading filelog, and unexpand when reading from working dir |
|
99 restricted = 'merge kwexpand kwshrink record qrecord resolve transplant' |
|
100 |
|
101 # names of extensions using dorecord |
|
102 recordextensions = 'record' |
|
103 |
|
104 # date like in cvs' $Date |
|
105 utcdate = lambda x: util.datestr((x[0], 0), '%Y/%m/%d %H:%M:%S') |
|
106 # date like in svn's $Date |
|
107 svnisodate = lambda x: util.datestr(x, '%Y-%m-%d %H:%M:%S %1%2 (%a, %d %b %Y)') |
|
108 # date like in svn's $Id |
|
109 svnutcdate = lambda x: util.datestr((x[0], 0), '%Y-%m-%d %H:%M:%SZ') |
|
110 |
|
111 # make keyword tools accessible |
|
112 kwtools = {'templater': None, 'hgcmd': ''} |
|
113 |
|
114 |
|
115 def _defaultkwmaps(ui): |
|
116 '''Returns default keywordmaps according to keywordset configuration.''' |
|
117 templates = { |
|
118 'Revision': '{node|short}', |
|
119 'Author': '{author|user}', |
|
120 } |
|
121 kwsets = ({ |
|
122 'Date': '{date|utcdate}', |
|
123 'RCSfile': '{file|basename},v', |
|
124 'RCSFile': '{file|basename},v', # kept for backwards compatibility |
|
125 # with hg-keyword |
|
126 'Source': '{root}/{file},v', |
|
127 'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}', |
|
128 'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}', |
|
129 }, { |
|
130 'Date': '{date|svnisodate}', |
|
131 'Id': '{file|basename},v {node|short} {date|svnutcdate} {author|user}', |
|
132 'LastChangedRevision': '{node|short}', |
|
133 'LastChangedBy': '{author|user}', |
|
134 'LastChangedDate': '{date|svnisodate}', |
|
135 }) |
|
136 templates.update(kwsets[ui.configbool('keywordset', 'svn')]) |
|
137 return templates |
|
138 |
|
139 def _shrinktext(text, subfunc): |
|
140 '''Helper for keyword expansion removal in text. |
|
141 Depending on subfunc also returns number of substitutions.''' |
|
142 return subfunc(r'$\1$', text) |
|
143 |
|
144 def _preselect(wstatus, changed): |
|
145 '''Retrieves modfied and added files from a working directory state |
|
146 and returns the subset of each contained in given changed files |
|
147 retrieved from a change context.''' |
|
148 modified, added = wstatus[:2] |
|
149 modified = [f for f in modified if f in changed] |
|
150 added = [f for f in added if f in changed] |
|
151 return modified, added |
|
152 |
|
153 |
|
154 class kwtemplater(object): |
|
155 ''' |
|
156 Sets up keyword templates, corresponding keyword regex, and |
|
157 provides keyword substitution functions. |
|
158 ''' |
|
159 |
|
160 def __init__(self, ui, repo, inc, exc): |
|
161 self.ui = ui |
|
162 self.repo = repo |
|
163 self.match = match.match(repo.root, '', [], inc, exc) |
|
164 self.restrict = kwtools['hgcmd'] in restricted.split() |
|
165 self.record = False |
|
166 |
|
167 kwmaps = self.ui.configitems('keywordmaps') |
|
168 if kwmaps: # override default templates |
|
169 self.templates = dict((k, templater.parsestring(v, False)) |
|
170 for k, v in kwmaps) |
|
171 else: |
|
172 self.templates = _defaultkwmaps(self.ui) |
|
173 escaped = '|'.join(map(re.escape, self.templates.keys())) |
|
174 self.re_kw = re.compile(r'\$(%s)\$' % escaped) |
|
175 self.re_kwexp = re.compile(r'\$(%s): [^$\n\r]*? \$' % escaped) |
|
176 |
|
177 templatefilters.filters.update({'utcdate': utcdate, |
|
178 'svnisodate': svnisodate, |
|
179 'svnutcdate': svnutcdate}) |
|
180 |
|
181 def substitute(self, data, path, ctx, subfunc): |
|
182 '''Replaces keywords in data with expanded template.''' |
|
183 def kwsub(mobj): |
|
184 kw = mobj.group(1) |
|
185 ct = cmdutil.changeset_templater(self.ui, self.repo, |
|
186 False, None, '', False) |
|
187 ct.use_template(self.templates[kw]) |
|
188 self.ui.pushbuffer() |
|
189 ct.show(ctx, root=self.repo.root, file=path) |
|
190 ekw = templatefilters.firstline(self.ui.popbuffer()) |
|
191 return '$%s: %s $' % (kw, ekw) |
|
192 return subfunc(kwsub, data) |
|
193 |
|
194 def expand(self, path, node, data): |
|
195 '''Returns data with keywords expanded.''' |
|
196 if not self.restrict and self.match(path) and not util.binary(data): |
|
197 ctx = self.repo.filectx(path, fileid=node).changectx() |
|
198 return self.substitute(data, path, ctx, self.re_kw.sub) |
|
199 return data |
|
200 |
|
201 def iskwfile(self, cand, ctx): |
|
202 '''Returns subset of candidates which are configured for keyword |
|
203 expansion are not symbolic links.''' |
|
204 return [f for f in cand if self.match(f) and not 'l' in ctx.flags(f)] |
|
205 |
|
206 def overwrite(self, ctx, candidates, lookup, expand, rekw=False): |
|
207 '''Overwrites selected files expanding/shrinking keywords.''' |
|
208 if self.restrict or lookup or self.record: # exclude kw_copy |
|
209 candidates = self.iskwfile(candidates, ctx) |
|
210 if not candidates: |
|
211 return |
|
212 kwcmd = self.restrict and lookup # kwexpand/kwshrink |
|
213 if self.restrict or expand and lookup: |
|
214 mf = ctx.manifest() |
|
215 fctx = ctx |
|
216 subn = (self.restrict or rekw) and self.re_kw.subn or self.re_kwexp.subn |
|
217 msg = (expand and _('overwriting %s expanding keywords\n') |
|
218 or _('overwriting %s shrinking keywords\n')) |
|
219 for f in candidates: |
|
220 if self.restrict: |
|
221 data = self.repo.file(f).read(mf[f]) |
|
222 else: |
|
223 data = self.repo.wread(f) |
|
224 if util.binary(data): |
|
225 continue |
|
226 if expand: |
|
227 if lookup: |
|
228 fctx = self.repo.filectx(f, fileid=mf[f]).changectx() |
|
229 data, found = self.substitute(data, f, fctx, subn) |
|
230 elif self.restrict: |
|
231 found = self.re_kw.search(data) |
|
232 else: |
|
233 data, found = _shrinktext(data, subn) |
|
234 if found: |
|
235 self.ui.note(msg % f) |
|
236 self.repo.wwrite(f, data, ctx.flags(f)) |
|
237 if kwcmd: |
|
238 self.repo.dirstate.normal(f) |
|
239 elif self.record: |
|
240 self.repo.dirstate.normallookup(f) |
|
241 |
|
242 def shrink(self, fname, text): |
|
243 '''Returns text with all keyword substitutions removed.''' |
|
244 if self.match(fname) and not util.binary(text): |
|
245 return _shrinktext(text, self.re_kwexp.sub) |
|
246 return text |
|
247 |
|
248 def shrinklines(self, fname, lines): |
|
249 '''Returns lines with keyword substitutions removed.''' |
|
250 if self.match(fname): |
|
251 text = ''.join(lines) |
|
252 if not util.binary(text): |
|
253 return _shrinktext(text, self.re_kwexp.sub).splitlines(True) |
|
254 return lines |
|
255 |
|
256 def wread(self, fname, data): |
|
257 '''If in restricted mode returns data read from wdir with |
|
258 keyword substitutions removed.''' |
|
259 return self.restrict and self.shrink(fname, data) or data |
|
260 |
|
261 class kwfilelog(filelog.filelog): |
|
262 ''' |
|
263 Subclass of filelog to hook into its read, add, cmp methods. |
|
264 Keywords are "stored" unexpanded, and processed on reading. |
|
265 ''' |
|
266 def __init__(self, opener, kwt, path): |
|
267 super(kwfilelog, self).__init__(opener, path) |
|
268 self.kwt = kwt |
|
269 self.path = path |
|
270 |
|
271 def read(self, node): |
|
272 '''Expands keywords when reading filelog.''' |
|
273 data = super(kwfilelog, self).read(node) |
|
274 if self.renamed(node): |
|
275 return data |
|
276 return self.kwt.expand(self.path, node, data) |
|
277 |
|
278 def add(self, text, meta, tr, link, p1=None, p2=None): |
|
279 '''Removes keyword substitutions when adding to filelog.''' |
|
280 text = self.kwt.shrink(self.path, text) |
|
281 return super(kwfilelog, self).add(text, meta, tr, link, p1, p2) |
|
282 |
|
283 def cmp(self, node, text): |
|
284 '''Removes keyword substitutions for comparison.''' |
|
285 text = self.kwt.shrink(self.path, text) |
|
286 return super(kwfilelog, self).cmp(node, text) |
|
287 |
|
288 def _status(ui, repo, kwt, *pats, **opts): |
|
289 '''Bails out if [keyword] configuration is not active. |
|
290 Returns status of working directory.''' |
|
291 if kwt: |
|
292 return repo.status(match=cmdutil.match(repo, pats, opts), clean=True, |
|
293 unknown=opts.get('unknown') or opts.get('all')) |
|
294 if ui.configitems('keyword'): |
|
295 raise util.Abort(_('[keyword] patterns cannot match')) |
|
296 raise util.Abort(_('no [keyword] patterns configured')) |
|
297 |
|
298 def _kwfwrite(ui, repo, expand, *pats, **opts): |
|
299 '''Selects files and passes them to kwtemplater.overwrite.''' |
|
300 wctx = repo[None] |
|
301 if len(wctx.parents()) > 1: |
|
302 raise util.Abort(_('outstanding uncommitted merge')) |
|
303 kwt = kwtools['templater'] |
|
304 wlock = repo.wlock() |
|
305 try: |
|
306 status = _status(ui, repo, kwt, *pats, **opts) |
|
307 modified, added, removed, deleted, unknown, ignored, clean = status |
|
308 if modified or added or removed or deleted: |
|
309 raise util.Abort(_('outstanding uncommitted changes')) |
|
310 kwt.overwrite(wctx, clean, True, expand) |
|
311 finally: |
|
312 wlock.release() |
|
313 |
|
314 def demo(ui, repo, *args, **opts): |
|
315 '''print [keywordmaps] configuration and an expansion example |
|
316 |
|
317 Show current, custom, or default keyword template maps and their |
|
318 expansions. |
|
319 |
|
320 Extend the current configuration by specifying maps as arguments |
|
321 and using -f/--rcfile to source an external hgrc file. |
|
322 |
|
323 Use -d/--default to disable current configuration. |
|
324 |
|
325 See :hg:`help templates` for information on templates and filters. |
|
326 ''' |
|
327 def demoitems(section, items): |
|
328 ui.write('[%s]\n' % section) |
|
329 for k, v in sorted(items): |
|
330 ui.write('%s = %s\n' % (k, v)) |
|
331 |
|
332 fn = 'demo.txt' |
|
333 tmpdir = tempfile.mkdtemp('', 'kwdemo.') |
|
334 ui.note(_('creating temporary repository at %s\n') % tmpdir) |
|
335 repo = localrepo.localrepository(ui, tmpdir, True) |
|
336 ui.setconfig('keyword', fn, '') |
|
337 |
|
338 uikwmaps = ui.configitems('keywordmaps') |
|
339 if args or opts.get('rcfile'): |
|
340 ui.status(_('\n\tconfiguration using custom keyword template maps\n')) |
|
341 if uikwmaps: |
|
342 ui.status(_('\textending current template maps\n')) |
|
343 if opts.get('default') or not uikwmaps: |
|
344 ui.status(_('\toverriding default template maps\n')) |
|
345 if opts.get('rcfile'): |
|
346 ui.readconfig(opts.get('rcfile')) |
|
347 if args: |
|
348 # simulate hgrc parsing |
|
349 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args] |
|
350 fp = repo.opener('hgrc', 'w') |
|
351 fp.writelines(rcmaps) |
|
352 fp.close() |
|
353 ui.readconfig(repo.join('hgrc')) |
|
354 kwmaps = dict(ui.configitems('keywordmaps')) |
|
355 elif opts.get('default'): |
|
356 ui.status(_('\n\tconfiguration using default keyword template maps\n')) |
|
357 kwmaps = _defaultkwmaps(ui) |
|
358 if uikwmaps: |
|
359 ui.status(_('\tdisabling current template maps\n')) |
|
360 for k, v in kwmaps.iteritems(): |
|
361 ui.setconfig('keywordmaps', k, v) |
|
362 else: |
|
363 ui.status(_('\n\tconfiguration using current keyword template maps\n')) |
|
364 kwmaps = dict(uikwmaps) or _defaultkwmaps(ui) |
|
365 |
|
366 uisetup(ui) |
|
367 reposetup(ui, repo) |
|
368 ui.write('[extensions]\nkeyword =\n') |
|
369 demoitems('keyword', ui.configitems('keyword')) |
|
370 demoitems('keywordmaps', kwmaps.iteritems()) |
|
371 keywords = '$' + '$\n$'.join(sorted(kwmaps.keys())) + '$\n' |
|
372 repo.wopener(fn, 'w').write(keywords) |
|
373 repo[None].add([fn]) |
|
374 ui.note(_('\nkeywords written to %s:\n') % fn) |
|
375 ui.note(keywords) |
|
376 repo.dirstate.setbranch('demobranch') |
|
377 for name, cmd in ui.configitems('hooks'): |
|
378 if name.split('.', 1)[0].find('commit') > -1: |
|
379 repo.ui.setconfig('hooks', name, '') |
|
380 msg = _('hg keyword configuration and expansion example') |
|
381 ui.note("hg ci -m '%s'\n" % msg) |
|
382 repo.commit(text=msg) |
|
383 ui.status(_('\n\tkeywords expanded\n')) |
|
384 ui.write(repo.wread(fn)) |
|
385 shutil.rmtree(tmpdir, ignore_errors=True) |
|
386 |
|
387 def expand(ui, repo, *pats, **opts): |
|
388 '''expand keywords in the working directory |
|
389 |
|
390 Run after (re)enabling keyword expansion. |
|
391 |
|
392 kwexpand refuses to run if given files contain local changes. |
|
393 ''' |
|
394 # 3rd argument sets expansion to True |
|
395 _kwfwrite(ui, repo, True, *pats, **opts) |
|
396 |
|
397 def files(ui, repo, *pats, **opts): |
|
398 '''show files configured for keyword expansion |
|
399 |
|
400 List which files in the working directory are matched by the |
|
401 [keyword] configuration patterns. |
|
402 |
|
403 Useful to prevent inadvertent keyword expansion and to speed up |
|
404 execution by including only files that are actual candidates for |
|
405 expansion. |
|
406 |
|
407 See :hg:`help keyword` on how to construct patterns both for |
|
408 inclusion and exclusion of files. |
|
409 |
|
410 With -A/--all and -v/--verbose the codes used to show the status |
|
411 of files are:: |
|
412 |
|
413 K = keyword expansion candidate |
|
414 k = keyword expansion candidate (not tracked) |
|
415 I = ignored |
|
416 i = ignored (not tracked) |
|
417 ''' |
|
418 kwt = kwtools['templater'] |
|
419 status = _status(ui, repo, kwt, *pats, **opts) |
|
420 cwd = pats and repo.getcwd() or '' |
|
421 modified, added, removed, deleted, unknown, ignored, clean = status |
|
422 files = [] |
|
423 if not opts.get('unknown') or opts.get('all'): |
|
424 files = sorted(modified + added + clean) |
|
425 wctx = repo[None] |
|
426 kwfiles = kwt.iskwfile(files, wctx) |
|
427 kwunknown = kwt.iskwfile(unknown, wctx) |
|
428 if not opts.get('ignore') or opts.get('all'): |
|
429 showfiles = kwfiles, kwunknown |
|
430 else: |
|
431 showfiles = [], [] |
|
432 if opts.get('all') or opts.get('ignore'): |
|
433 showfiles += ([f for f in files if f not in kwfiles], |
|
434 [f for f in unknown if f not in kwunknown]) |
|
435 for char, filenames in zip('KkIi', showfiles): |
|
436 fmt = (opts.get('all') or ui.verbose) and '%s %%s\n' % char or '%s\n' |
|
437 for f in filenames: |
|
438 ui.write(fmt % repo.pathto(f, cwd)) |
|
439 |
|
440 def shrink(ui, repo, *pats, **opts): |
|
441 '''revert expanded keywords in the working directory |
|
442 |
|
443 Run before changing/disabling active keywords or if you experience |
|
444 problems with :hg:`import` or :hg:`merge`. |
|
445 |
|
446 kwshrink refuses to run if given files contain local changes. |
|
447 ''' |
|
448 # 3rd argument sets expansion to False |
|
449 _kwfwrite(ui, repo, False, *pats, **opts) |
|
450 |
|
451 |
|
452 def uisetup(ui): |
|
453 ''' Monkeypatches dispatch._parse to retrieve user command.''' |
|
454 |
|
455 def kwdispatch_parse(orig, ui, args): |
|
456 '''Monkeypatch dispatch._parse to obtain running hg command.''' |
|
457 cmd, func, args, options, cmdoptions = orig(ui, args) |
|
458 kwtools['hgcmd'] = cmd |
|
459 return cmd, func, args, options, cmdoptions |
|
460 |
|
461 extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse) |
|
462 |
|
463 def reposetup(ui, repo): |
|
464 '''Sets up repo as kwrepo for keyword substitution. |
|
465 Overrides file method to return kwfilelog instead of filelog |
|
466 if file matches user configuration. |
|
467 Wraps commit to overwrite configured files with updated |
|
468 keyword substitutions. |
|
469 Monkeypatches patch and webcommands.''' |
|
470 |
|
471 try: |
|
472 if (not repo.local() or kwtools['hgcmd'] in nokwcommands.split() |
|
473 or '.hg' in util.splitpath(repo.root) |
|
474 or repo._url.startswith('bundle:')): |
|
475 return |
|
476 except AttributeError: |
|
477 pass |
|
478 |
|
479 inc, exc = [], ['.hg*'] |
|
480 for pat, opt in ui.configitems('keyword'): |
|
481 if opt != 'ignore': |
|
482 inc.append(pat) |
|
483 else: |
|
484 exc.append(pat) |
|
485 if not inc: |
|
486 return |
|
487 |
|
488 kwtools['templater'] = kwt = kwtemplater(ui, repo, inc, exc) |
|
489 |
|
490 class kwrepo(repo.__class__): |
|
491 def file(self, f): |
|
492 if f[0] == '/': |
|
493 f = f[1:] |
|
494 return kwfilelog(self.sopener, kwt, f) |
|
495 |
|
496 def wread(self, filename): |
|
497 data = super(kwrepo, self).wread(filename) |
|
498 return kwt.wread(filename, data) |
|
499 |
|
500 def commit(self, *args, **opts): |
|
501 # use custom commitctx for user commands |
|
502 # other extensions can still wrap repo.commitctx directly |
|
503 self.commitctx = self.kwcommitctx |
|
504 try: |
|
505 return super(kwrepo, self).commit(*args, **opts) |
|
506 finally: |
|
507 del self.commitctx |
|
508 |
|
509 def kwcommitctx(self, ctx, error=False): |
|
510 n = super(kwrepo, self).commitctx(ctx, error) |
|
511 # no lock needed, only called from repo.commit() which already locks |
|
512 if not kwt.record: |
|
513 restrict = kwt.restrict |
|
514 kwt.restrict = True |
|
515 kwt.overwrite(self[n], sorted(ctx.added() + ctx.modified()), |
|
516 False, True) |
|
517 kwt.restrict = restrict |
|
518 return n |
|
519 |
|
520 def rollback(self, dryrun=False): |
|
521 wlock = self.wlock() |
|
522 try: |
|
523 if not dryrun: |
|
524 changed = self['.'].files() |
|
525 ret = super(kwrepo, self).rollback(dryrun) |
|
526 if not dryrun: |
|
527 ctx = self['.'] |
|
528 modified, added = _preselect(self[None].status(), changed) |
|
529 kwt.overwrite(ctx, modified, True, True) |
|
530 kwt.overwrite(ctx, added, True, False) |
|
531 return ret |
|
532 finally: |
|
533 wlock.release() |
|
534 |
|
535 # monkeypatches |
|
536 def kwpatchfile_init(orig, self, ui, fname, opener, |
|
537 missing=False, eolmode=None): |
|
538 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid |
|
539 rejects or conflicts due to expanded keywords in working dir.''' |
|
540 orig(self, ui, fname, opener, missing, eolmode) |
|
541 # shrink keywords read from working dir |
|
542 self.lines = kwt.shrinklines(self.fname, self.lines) |
|
543 |
|
544 def kw_diff(orig, repo, node1=None, node2=None, match=None, changes=None, |
|
545 opts=None, prefix=''): |
|
546 '''Monkeypatch patch.diff to avoid expansion.''' |
|
547 kwt.restrict = True |
|
548 return orig(repo, node1, node2, match, changes, opts, prefix) |
|
549 |
|
550 def kwweb_skip(orig, web, req, tmpl): |
|
551 '''Wraps webcommands.x turning off keyword expansion.''' |
|
552 kwt.match = util.never |
|
553 return orig(web, req, tmpl) |
|
554 |
|
555 def kw_copy(orig, ui, repo, pats, opts, rename=False): |
|
556 '''Wraps cmdutil.copy so that copy/rename destinations do not |
|
557 contain expanded keywords. |
|
558 Note that the source of a regular file destination may also be a |
|
559 symlink: |
|
560 hg cp sym x -> x is symlink |
|
561 cp sym x; hg cp -A sym x -> x is file (maybe expanded keywords) |
|
562 For the latter we have to follow the symlink to find out whether its |
|
563 target is configured for expansion and we therefore must unexpand the |
|
564 keywords in the destination.''' |
|
565 orig(ui, repo, pats, opts, rename) |
|
566 if opts.get('dry_run'): |
|
567 return |
|
568 wctx = repo[None] |
|
569 cwd = repo.getcwd() |
|
570 |
|
571 def haskwsource(dest): |
|
572 '''Returns true if dest is a regular file and configured for |
|
573 expansion or a symlink which points to a file configured for |
|
574 expansion. ''' |
|
575 source = repo.dirstate.copied(dest) |
|
576 if 'l' in wctx.flags(source): |
|
577 source = util.canonpath(repo.root, cwd, |
|
578 os.path.realpath(source)) |
|
579 return kwt.match(source) |
|
580 |
|
581 candidates = [f for f in repo.dirstate.copies() if |
|
582 not 'l' in wctx.flags(f) and haskwsource(f)] |
|
583 kwt.overwrite(wctx, candidates, False, False) |
|
584 |
|
585 def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts): |
|
586 '''Wraps record.dorecord expanding keywords after recording.''' |
|
587 wlock = repo.wlock() |
|
588 try: |
|
589 # record returns 0 even when nothing has changed |
|
590 # therefore compare nodes before and after |
|
591 kwt.record = True |
|
592 ctx = repo['.'] |
|
593 wstatus = repo[None].status() |
|
594 ret = orig(ui, repo, commitfunc, *pats, **opts) |
|
595 recctx = repo['.'] |
|
596 if ctx != recctx: |
|
597 modified, added = _preselect(wstatus, recctx.files()) |
|
598 kwt.restrict = False |
|
599 kwt.overwrite(recctx, modified, False, True) |
|
600 kwt.overwrite(recctx, added, False, True, True) |
|
601 kwt.restrict = True |
|
602 return ret |
|
603 finally: |
|
604 wlock.release() |
|
605 |
|
606 repo.__class__ = kwrepo |
|
607 |
|
608 def kwfilectx_cmp(orig, self, fctx): |
|
609 # keyword affects data size, comparing wdir and filelog size does |
|
610 # not make sense |
|
611 if (fctx._filerev is None and |
|
612 (self._repo._encodefilterpats or |
|
613 kwt.match(fctx.path()) and not 'l' in fctx.flags()) or |
|
614 self.size() == fctx.size()): |
|
615 return self._filelog.cmp(self._filenode, fctx.data()) |
|
616 return True |
|
617 |
|
618 extensions.wrapfunction(context.filectx, 'cmp', kwfilectx_cmp) |
|
619 extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init) |
|
620 extensions.wrapfunction(patch, 'diff', kw_diff) |
|
621 extensions.wrapfunction(cmdutil, 'copy', kw_copy) |
|
622 for c in 'annotate changeset rev filediff diff'.split(): |
|
623 extensions.wrapfunction(webcommands, c, kwweb_skip) |
|
624 for name in recordextensions.split(): |
|
625 try: |
|
626 record = extensions.find(name) |
|
627 extensions.wrapfunction(record, 'dorecord', kw_dorecord) |
|
628 except KeyError: |
|
629 pass |
|
630 |
|
631 cmdtable = { |
|
632 'kwdemo': |
|
633 (demo, |
|
634 [('d', 'default', None, _('show default keyword template maps')), |
|
635 ('f', 'rcfile', '', |
|
636 _('read maps from rcfile'), _('FILE'))], |
|
637 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')), |
|
638 'kwexpand': (expand, commands.walkopts, |
|
639 _('hg kwexpand [OPTION]... [FILE]...')), |
|
640 'kwfiles': |
|
641 (files, |
|
642 [('A', 'all', None, _('show keyword status flags of all files')), |
|
643 ('i', 'ignore', None, _('show files excluded from expansion')), |
|
644 ('u', 'unknown', None, _('only show unknown (not tracked) files')), |
|
645 ] + commands.walkopts, |
|
646 _('hg kwfiles [OPTION]... [FILE]...')), |
|
647 'kwshrink': (shrink, commands.walkopts, |
|
648 _('hg kwshrink [OPTION]... [FILE]...')), |
|
649 } |