##// END OF EJS Templates
keyword: monkeypatch dispatch._parse to avoid redundant run...
Christian Ebert -
r6052:75425961 default
parent child Browse files
Show More
@@ -1,524 +1,531 b''
1 1 # keyword.py - $Keyword$ expansion for Mercurial
2 2 #
3 3 # Copyright 2007, 2008 Christian Ebert <blacktrash@gmx.net>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
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 audience
15 15 # not running a version control system.
16 16 #
17 17 # For in-depth discussion refer to
18 18 # <http://www.selenic.com/mercurial/wiki/index.cgi/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 # Setup in hgrc:
25 25 #
26 26 # [extensions]
27 27 # # enable extension
28 28 # hgext.keyword =
29 29 #
30 30 # Files to act upon/ignore are specified in the [keyword] section.
31 31 # Customized keyword template mappings in the [keywordmaps] section.
32 32 #
33 33 # Run "hg help keyword" and "hg kwdemo" to get info on configuration.
34 34
35 35 '''keyword expansion in local repositories
36 36
37 37 This extension expands RCS/CVS-like or self-customized $Keywords$
38 38 in tracked text files selected by your configuration.
39 39
40 40 Keywords are only expanded in local repositories and not stored in
41 41 the change history. The mechanism can be regarded as a convenience
42 42 for the current user or for archive distribution.
43 43
44 44 Configuration is done in the [keyword] and [keywordmaps] sections
45 45 of hgrc files.
46 46
47 47 Example:
48 48
49 49 [keyword]
50 50 # expand keywords in every python file except those matching "x*"
51 51 **.py =
52 52 x* = ignore
53 53
54 54 Note: the more specific you are in your filename patterns
55 55 the less you lose speed in huge repos.
56 56
57 57 For [keywordmaps] template mapping and expansion demonstration and
58 58 control run "hg kwdemo".
59 59
60 60 An additional date template filter {date|utcdate} is provided.
61 61
62 62 The default template mappings (view with "hg kwdemo -d") can be replaced
63 63 with customized keywords and templates.
64 64 Again, run "hg kwdemo" to control the results of your config changes.
65 65
66 66 Before changing/disabling active keywords, run "hg kwshrink" to avoid
67 67 the risk of inadvertedly storing expanded keywords in the change history.
68 68
69 69 To force expansion after enabling it, or a configuration change, run
70 70 "hg kwexpand".
71 71
72 72 Also, when committing with the record extension or using mq's qrecord, be aware
73 73 that keywords cannot be updated. Again, run "hg kwexpand" on the files in
74 74 question to update keyword expansions after all changes have been checked in.
75 75
76 76 Expansions spanning more than one line and incremental expansions,
77 77 like CVS' $Log$, are not supported. A keyword template map
78 78 "Log = {desc}" expands to the first line of the changeset description.
79 79 '''
80 80
81 81 from mercurial import commands, cmdutil, context, dispatch, filelog, revlog
82 82 from mercurial import patch, localrepo, templater, templatefilters, util
83 83 from mercurial.node import *
84 84 from mercurial.i18n import _
85 import re, shutil, sys, tempfile, time
85 import re, shutil, tempfile, time
86 86
87 87 commands.optionalrepo += ' kwdemo'
88 88
89 89 # hg commands that do not act on keywords
90 90 nokwcommands = ('add addremove bundle copy export grep identify incoming init'
91 91 ' log outgoing push remove rename rollback tip convert email')
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 = 'diff1 record qfold qimport qnew qpush qrefresh qrecord'
96 96
97 97 def utcdate(date):
98 98 '''Returns hgdate in cvs-like UTC format.'''
99 99 return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0]))
100 100
101 101
102 _kwtemplater = None
102 _kwtemplater, _cmd, _cmdoptions = None, None, None
103
104 # store originals of monkeypatches
105 _patchfile_init = patch.patchfile.__init__
106 _dispatch_parse = dispatch._parse
107
108 def _kwpatchfile_init(self, ui, fname, missing=False):
109 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
110 rejects or conflicts due to expanded keywords in working dir.'''
111 _patchfile_init(self, ui, fname, missing=missing)
112 if _kwtemplater.matcher(self.fname):
113 # shrink keywords read from working dir
114 kwshrunk = _kwtemplater.shrink(''.join(self.lines))
115 self.lines = kwshrunk.splitlines(True)
116
117 def _kwdispatch_parse(ui, args):
118 '''Monkeypatch dispatch._parse to obtain
119 current command and command options (global _cmd, _cmdoptions).'''
120 global _cmd, _cmdoptions
121 _cmd, func, args, options, _cmdoptions = _dispatch_parse(ui, args)
122 return _cmd, func, args, options, _cmdoptions
123
124 dispatch._parse = _kwdispatch_parse
125
103 126
104 127 class kwtemplater(object):
105 128 '''
106 129 Sets up keyword templates, corresponding keyword regex, and
107 130 provides keyword substitution functions.
108 131 '''
109 132 templates = {
110 133 'Revision': '{node|short}',
111 134 'Author': '{author|user}',
112 135 'Date': '{date|utcdate}',
113 136 'RCSFile': '{file|basename},v',
114 137 'Source': '{root}/{file},v',
115 138 'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
116 139 'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
117 140 }
118 141
119 142 def __init__(self, ui, repo, inc, exc, restrict):
120 143 self.ui = ui
121 144 self.repo = repo
122 145 self.matcher = util.matcher(repo.root, inc=inc, exc=exc)[1]
123 146 self.restrict = restrict
124 147 self.commitnode = None
125 148 self.path = ''
126 149
127 150 kwmaps = self.ui.configitems('keywordmaps')
128 151 if kwmaps: # override default templates
129 152 kwmaps = [(k, templater.parsestring(v, quoted=False))
130 153 for (k, v) in kwmaps]
131 154 self.templates = dict(kwmaps)
132 155 escaped = map(re.escape, self.templates.keys())
133 156 kwpat = r'\$(%s)(: [^$\n\r]*? )??\$' % '|'.join(escaped)
134 157 self.re_kw = re.compile(kwpat)
135 158
136 159 templatefilters.filters['utcdate'] = utcdate
137 160 self.ct = cmdutil.changeset_templater(self.ui, self.repo,
138 161 False, '', False)
139 162
140 163 def substitute(self, node, data, subfunc):
141 164 '''Obtains file's changenode if commit node not given,
142 165 and calls given substitution function.'''
143 166 if self.commitnode:
144 167 fnode = self.commitnode
145 168 else:
146 169 c = context.filectx(self.repo, self.path, fileid=node)
147 170 fnode = c.node()
148 171
149 172 def kwsub(mobj):
150 173 '''Substitutes keyword using corresponding template.'''
151 174 kw = mobj.group(1)
152 175 self.ct.use_template(self.templates[kw])
153 176 self.ui.pushbuffer()
154 177 self.ct.show(changenode=fnode, root=self.repo.root, file=self.path)
155 178 ekw = templatefilters.firstline(self.ui.popbuffer())
156 179 return '$%s: %s $' % (kw, ekw)
157 180
158 181 return subfunc(kwsub, data)
159 182
160 183 def expand(self, node, data):
161 184 '''Returns data with keywords expanded.'''
162 185 if self.restrict or util.binary(data):
163 186 return data
164 187 return self.substitute(node, data, self.re_kw.sub)
165 188
166 189 def process(self, node, data, expand):
167 190 '''Returns a tuple: data, count.
168 191 Count is number of keywords/keyword substitutions,
169 192 telling caller whether to act on file containing data.'''
170 193 if util.binary(data):
171 194 return data, None
172 195 if expand:
173 196 return self.substitute(node, data, self.re_kw.subn)
174 197 return data, self.re_kw.search(data)
175 198
176 199 def shrink(self, text):
177 200 '''Returns text with all keyword substitutions removed.'''
178 201 if util.binary(text):
179 202 return text
180 203 return self.re_kw.sub(r'$\1$', text)
181 204
182 205 class kwfilelog(filelog.filelog):
183 206 '''
184 207 Subclass of filelog to hook into its read, add, cmp methods.
185 208 Keywords are "stored" unexpanded, and processed on reading.
186 209 '''
187 210 def __init__(self, opener, path):
188 211 super(kwfilelog, self).__init__(opener, path)
189 212 _kwtemplater.path = path
190 213
191 214 def kwctread(self, node, expand):
192 215 '''Reads expanding and counting keywords, called from _overwrite.'''
193 216 data = super(kwfilelog, self).read(node)
194 217 return _kwtemplater.process(node, data, expand)
195 218
196 219 def read(self, node):
197 220 '''Expands keywords when reading filelog.'''
198 221 data = super(kwfilelog, self).read(node)
199 222 return _kwtemplater.expand(node, data)
200 223
201 224 def add(self, text, meta, tr, link, p1=None, p2=None):
202 225 '''Removes keyword substitutions when adding to filelog.'''
203 226 text = _kwtemplater.shrink(text)
204 227 return super(kwfilelog, self).add(text, meta, tr, link, p1=p1, p2=p2)
205 228
206 229 def cmp(self, node, text):
207 230 '''Removes keyword substitutions for comparison.'''
208 231 text = _kwtemplater.shrink(text)
209 232 if self.renamed(node):
210 233 t2 = super(kwfilelog, self).read(node)
211 234 return t2 != text
212 235 return revlog.revlog.cmp(self, node, text)
213 236
214
215 # store original patch.patchfile.__init__
216 _patchfile_init = patch.patchfile.__init__
217
218 def _kwpatchfile_init(self, ui, fname, missing=False):
219 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
220 rejects or conflicts due to expanded keywords in working dir.'''
221 _patchfile_init(self, ui, fname, missing=missing)
222
223 if _kwtemplater.matcher(self.fname):
224 # shrink keywords read from working dir
225 kwshrunk = _kwtemplater.shrink(''.join(self.lines))
226 self.lines = kwshrunk.splitlines(True)
227
228
229 237 def _iskwfile(f, link):
230 238 return not link(f) and _kwtemplater.matcher(f)
231 239
232 240 def _status(ui, repo, *pats, **opts):
233 241 '''Bails out if [keyword] configuration is not active.
234 242 Returns status of working directory.'''
235 243 if _kwtemplater:
236 244 files, match, anypats = cmdutil.matchpats(repo, pats, opts)
237 245 return repo.status(files=files, match=match, list_clean=True)
238 246 if ui.configitems('keyword'):
239 247 raise util.Abort(_('[keyword] patterns cannot match'))
240 248 raise util.Abort(_('no [keyword] patterns configured'))
241 249
242 250 def _overwrite(ui, repo, node=None, expand=True, files=None):
243 251 '''Overwrites selected files expanding/shrinking keywords.'''
244 252 ctx = repo.changectx(node)
245 253 mf = ctx.manifest()
246 254 if node is not None: # commit
247 255 _kwtemplater.commitnode = node
248 256 files = [f for f in ctx.files() if f in mf]
249 257 notify = ui.debug
250 258 else: # kwexpand/kwshrink
251 259 notify = ui.note
252 260 candidates = [f for f in files if _iskwfile(f, mf.linkf)]
253 261 if candidates:
254 262 candidates.sort()
255 263 action = expand and 'expanding' or 'shrinking'
256 264 for f in candidates:
257 265 fp = repo.file(f, kwmatch=True)
258 266 data, kwfound = fp.kwctread(mf[f], expand)
259 267 if kwfound:
260 268 notify(_('overwriting %s %s keywords\n') % (f, action))
261 269 repo.wwrite(f, data, mf.flags(f))
262 270 repo.dirstate.normal(f)
263 271
264 272 def _kwfwrite(ui, repo, expand, *pats, **opts):
265 273 '''Selects files and passes them to _overwrite.'''
266 274 status = _status(ui, repo, *pats, **opts)
267 275 modified, added, removed, deleted, unknown, ignored, clean = status
268 276 if modified or added or removed or deleted:
269 277 raise util.Abort(_('outstanding uncommitted changes in given files'))
270 278 wlock = lock = None
271 279 try:
272 280 wlock = repo.wlock()
273 281 lock = repo.lock()
274 282 _overwrite(ui, repo, expand=expand, files=clean)
275 283 finally:
276 284 del wlock, lock
277 285
278 286
279 287 def demo(ui, repo, *args, **opts):
280 288 '''print [keywordmaps] configuration and an expansion example
281 289
282 290 Show current, custom, or default keyword template maps
283 291 and their expansion.
284 292
285 293 Extend current configuration by specifying maps as arguments
286 294 and optionally by reading from an additional hgrc file.
287 295
288 296 Override current keyword template maps with "default" option.
289 297 '''
290 298 def demostatus(stat):
291 299 ui.status(_('\n\t%s\n') % stat)
292 300
293 301 def demoitems(section, items):
294 302 ui.write('[%s]\n' % section)
295 303 for k, v in items:
296 304 ui.write('%s = %s\n' % (k, v))
297 305
298 306 msg = 'hg keyword config and expansion example'
299 307 kwstatus = 'current'
300 308 fn = 'demo.txt'
301 309 branchname = 'demobranch'
302 310 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
303 311 ui.note(_('creating temporary repo at %s\n') % tmpdir)
304 312 repo = localrepo.localrepository(ui, path=tmpdir, create=True)
305 313 ui.setconfig('keyword', fn, '')
306 314 if args or opts.get('rcfile'):
307 315 kwstatus = 'custom'
308 316 if opts.get('rcfile'):
309 317 ui.readconfig(opts.get('rcfile'))
310 318 if opts.get('default'):
311 319 kwstatus = 'default'
312 320 kwmaps = kwtemplater.templates
313 321 if ui.configitems('keywordmaps'):
314 322 # override maps from optional rcfile
315 323 for k, v in kwmaps.iteritems():
316 324 ui.setconfig('keywordmaps', k, v)
317 325 elif args:
318 326 # simulate hgrc parsing
319 327 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
320 328 fp = repo.opener('hgrc', 'w')
321 329 fp.writelines(rcmaps)
322 330 fp.close()
323 331 ui.readconfig(repo.join('hgrc'))
324 332 if not opts.get('default'):
325 333 kwmaps = dict(ui.configitems('keywordmaps')) or kwtemplater.templates
326 334 reposetup(ui, repo)
327 335 for k, v in ui.configitems('extensions'):
328 336 if k.endswith('keyword'):
329 337 extension = '%s = %s' % (k, v)
330 338 break
331 339 demostatus('config using %s keyword template maps' % kwstatus)
332 340 ui.write('[extensions]\n%s\n' % extension)
333 341 demoitems('keyword', ui.configitems('keyword'))
334 342 demoitems('keywordmaps', kwmaps.iteritems())
335 343 keywords = '$' + '$\n$'.join(kwmaps.keys()) + '$\n'
336 344 repo.wopener(fn, 'w').write(keywords)
337 345 repo.add([fn])
338 346 path = repo.wjoin(fn)
339 347 ui.note(_('\n%s keywords written to %s:\n') % (kwstatus, path))
340 348 ui.note(keywords)
341 349 ui.note('\nhg -R "%s" branch "%s"\n' % (tmpdir, branchname))
342 350 # silence branch command if not verbose
343 351 quiet = ui.quiet
344 352 ui.quiet = not ui.verbose
345 353 commands.branch(ui, repo, branchname)
346 354 ui.quiet = quiet
347 355 for name, cmd in ui.configitems('hooks'):
348 356 if name.split('.', 1)[0].find('commit') > -1:
349 357 repo.ui.setconfig('hooks', name, '')
350 358 ui.note(_('unhooked all commit hooks\n'))
351 359 ui.note('hg -R "%s" ci -m "%s"\n' % (tmpdir, msg))
352 360 repo.commit(text=msg)
353 361 format = ui.verbose and ' in %s' % path or ''
354 362 demostatus('%s keywords expanded%s' % (kwstatus, format))
355 363 ui.write(repo.wread(fn))
356 364 ui.debug(_('\nremoving temporary repo %s\n') % tmpdir)
357 365 shutil.rmtree(tmpdir, ignore_errors=True)
358 366
359 367 def expand(ui, repo, *pats, **opts):
360 368 '''expand keywords in working directory
361 369
362 370 Run after (re)enabling keyword expansion.
363 371
364 372 kwexpand refuses to run if given files contain local changes.
365 373 '''
366 374 # 3rd argument sets expansion to True
367 375 _kwfwrite(ui, repo, True, *pats, **opts)
368 376
369 377 def files(ui, repo, *pats, **opts):
370 378 '''print files currently configured for keyword expansion
371 379
372 380 Crosscheck which files in working directory are potential targets for
373 381 keyword expansion.
374 382 That is, files matched by [keyword] config patterns but not symlinks.
375 383 '''
376 384 status = _status(ui, repo, *pats, **opts)
377 385 modified, added, removed, deleted, unknown, ignored, clean = status
378 386 files = modified + added + clean
379 387 if opts.get('untracked'):
380 388 files += unknown
381 389 files.sort()
382 390 kwfiles = [f for f in files if _iskwfile(f, repo._link)]
383 391 cwd = pats and repo.getcwd() or ''
384 392 kwfstats = not opts.get('ignore') and (('K', kwfiles),) or ()
385 393 if opts.get('all') or opts.get('ignore'):
386 394 kwfstats += (('I', [f for f in files if f not in kwfiles]),)
387 395 for char, filenames in kwfstats:
388 396 format = (opts.get('all') or ui.verbose) and '%s %%s\n' % char or '%s\n'
389 397 for f in filenames:
390 398 ui.write(format % repo.pathto(f, cwd))
391 399
392 400 def shrink(ui, repo, *pats, **opts):
393 401 '''revert expanded keywords in working directory
394 402
395 403 Run before changing/disabling active keywords
396 404 or if you experience problems with "hg import" or "hg merge".
397 405
398 406 kwshrink refuses to run if given files contain local changes.
399 407 '''
400 408 # 3rd argument sets expansion to False
401 409 _kwfwrite(ui, repo, False, *pats, **opts)
402 410
403 411
404 412 def reposetup(ui, repo):
405 413 '''Sets up repo as kwrepo for keyword substitution.
406 414 Overrides file method to return kwfilelog instead of filelog
407 415 if file matches user configuration.
408 416 Wraps commit to overwrite configured files with updated
409 417 keyword substitutions.
410 418 This is done for local repos only, and only if there are
411 419 files configured at all for keyword substitution.'''
412 420
421 global _kwtemplater
422 hgcmd, hgcmdopts = _cmd, _cmdoptions
423
413 424 try:
414 if (not repo.local() or '.hg' in repo.root.split('/')
425 if (not repo.local() or hgcmd in nokwcommands.split()
426 or '.hg' in repo.root.split('/')
415 427 or repo._url.startswith('bundle:')):
416 428 return
417 429 except AttributeError:
418 430 pass
419 431
420 hgcmd, func, args, opts, cmdopts = dispatch._parse(ui, sys.argv[1:])
421 if hgcmd in nokwcommands.split():
422 return
423
424 if hgcmd == 'diff':
425 # only expand if comparing against working dir
426 node1, node2 = cmdutil.revpair(repo, cmdopts.get('rev'))
427 if node2 is not None:
428 return
429 # shrink if rev is not current node
430 if node1 is not None and node1 != repo.changectx().node():
431 hgcmd = 'diff1'
432
433 432 inc, exc = [], ['.hg*']
434 433 for pat, opt in ui.configitems('keyword'):
435 434 if opt != 'ignore':
436 435 inc.append(pat)
437 436 else:
438 437 exc.append(pat)
439 438 if not inc:
440 439 return
441 440
442 global _kwtemplater
441 if hgcmd == 'diff':
442 # only expand if comparing against working dir
443 node1, node2 = cmdutil.revpair(repo, hgcmdopts.get('rev'))
444 if node2 is not None:
445 return
446 # shrink if rev is not current node
447 if node1 is not None and node1 != repo.changectx().node():
448 hgcmd = 'diff1'
449
443 450 restrict = hgcmd in restricted.split()
444 451 _kwtemplater = kwtemplater(ui, repo, inc, exc, restrict)
445 452
446 453 class kwrepo(repo.__class__):
447 454 def file(self, f, kwmatch=False):
448 455 if f[0] == '/':
449 456 f = f[1:]
450 457 if kwmatch or _kwtemplater.matcher(f):
451 458 return kwfilelog(self.sopener, f)
452 459 return filelog.filelog(self.sopener, f)
453 460
454 461 def wread(self, filename):
455 462 data = super(kwrepo, self).wread(filename)
456 463 if restrict and _kwtemplater.matcher(filename):
457 464 return _kwtemplater.shrink(data)
458 465 return data
459 466
460 467 def commit(self, files=None, text='', user=None, date=None,
461 468 match=util.always, force=False, force_editor=False,
462 469 p1=None, p2=None, extra={}, empty_ok=False):
463 470 wlock = lock = None
464 471 _p1 = _p2 = None
465 472 try:
466 473 wlock = self.wlock()
467 474 lock = self.lock()
468 475 # store and postpone commit hooks
469 476 commithooks = {}
470 477 for name, cmd in ui.configitems('hooks'):
471 478 if name.split('.', 1)[0] == 'commit':
472 479 commithooks[name] = cmd
473 480 ui.setconfig('hooks', name, None)
474 481 if commithooks:
475 482 # store parents for commit hook environment
476 483 if p1 is None:
477 484 _p1, _p2 = repo.dirstate.parents()
478 485 else:
479 486 _p1, _p2 = p1, p2 or nullid
480 487 _p1 = hex(_p1)
481 488 if _p2 == nullid:
482 489 _p2 = ''
483 490 else:
484 491 _p2 = hex(_p2)
485 492
486 493 node = super(kwrepo,
487 494 self).commit(files=files, text=text, user=user,
488 495 date=date, match=match, force=force,
489 496 force_editor=force_editor,
490 497 p1=p1, p2=p2, extra=extra,
491 498 empty_ok=empty_ok)
492 499
493 500 # restore commit hooks
494 501 for name, cmd in commithooks.iteritems():
495 502 ui.setconfig('hooks', name, cmd)
496 503 if node is not None:
497 504 _overwrite(ui, self, node=node)
498 505 repo.hook('commit', node=node, parent1=_p1, parent2=_p2)
499 506 return node
500 507 finally:
501 508 del wlock, lock
502 509
503 510 repo.__class__ = kwrepo
504 511 patch.patchfile.__init__ = _kwpatchfile_init
505 512
506 513
507 514 cmdtable = {
508 515 'kwdemo':
509 516 (demo,
510 517 [('d', 'default', None, _('show default keyword template maps')),
511 518 ('f', 'rcfile', [], _('read maps from rcfile'))],
512 519 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')),
513 520 'kwexpand': (expand, commands.walkopts,
514 521 _('hg kwexpand [OPTION]... [FILE]...')),
515 522 'kwfiles':
516 523 (files,
517 524 [('a', 'all', None, _('show keyword status flags of all files')),
518 525 ('i', 'ignore', None, _('show files excluded from expansion')),
519 526 ('u', 'untracked', None, _('additionally show untracked files')),
520 527 ] + commands.walkopts,
521 528 _('hg kwfiles [OPTION]... [FILE]...')),
522 529 'kwshrink': (shrink, commands.walkopts,
523 530 _('hg kwshrink [OPTION]... [FILE]...')),
524 531 }
General Comments 0
You need to be logged in to leave comments. Login now