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