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