##// END OF EJS Templates
keyword: suppress keyword expansion for log commands...
Christian Ebert -
r5824:b8e8bd3c default
parent child Browse files
Show More
@@ -1,500 +1,501 b''
1 1 # keyword.py - $Keyword$ expansion for Mercurial
2 2 #
3 3 # Copyright 2007 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 Expansions spanning more than one line and incremental expansions,
73 73 like CVS' $Log$, are not supported. A keyword template map
74 74 "Log = {desc}" expands to the first line of the changeset description.
75 75 '''
76 76
77 77 from mercurial import commands, cmdutil, context, fancyopts, filelog
78 78 from mercurial import patch, localrepo, revlog, templater, util
79 79 from mercurial.node import *
80 80 from mercurial.i18n import _
81 81 import re, shutil, sys, tempfile, time
82 82
83 83 commands.optionalrepo += ' kwdemo'
84 84
85 85 def utcdate(date):
86 86 '''Returns hgdate in cvs-like UTC format.'''
87 87 return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0]))
88 88
89 89 _kwtemplater = None
90 90
91 91 class kwtemplater(object):
92 92 '''
93 93 Sets up keyword templates, corresponding keyword regex, and
94 94 provides keyword substitution functions.
95 95 '''
96 96 templates = {
97 97 'Revision': '{node|short}',
98 98 'Author': '{author|user}',
99 99 'Date': '{date|utcdate}',
100 100 'RCSFile': '{file|basename},v',
101 101 'Source': '{root}/{file},v',
102 102 'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
103 103 'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
104 104 }
105 105
106 106 def __init__(self, ui, repo, inc, exc):
107 107 self.ui = ui
108 108 self.repo = repo
109 109 self.matcher = util.matcher(repo.root, inc=inc, exc=exc)[1]
110 110 self.commitnode = None
111 111 self.path = ''
112 112
113 113 kwmaps = self.ui.configitems('keywordmaps')
114 114 if kwmaps: # override default templates
115 115 kwmaps = [(k, templater.parsestring(v, quoted=False))
116 116 for (k, v) in kwmaps]
117 117 self.templates = dict(kwmaps)
118 118 escaped = map(re.escape, self.templates.keys())
119 119 kwpat = r'\$(%s)(: [^$\n\r]*? )??\$' % '|'.join(escaped)
120 120 self.re_kw = re.compile(kwpat)
121 121
122 122 templater.common_filters['utcdate'] = utcdate
123 123 self.ct = cmdutil.changeset_templater(self.ui, self.repo,
124 124 False, '', False)
125 125
126 126 def substitute(self, node, data, subfunc):
127 127 '''Obtains file's changenode if commit node not given,
128 128 and calls given substitution function.'''
129 129 if self.commitnode:
130 130 fnode = self.commitnode
131 131 else:
132 132 c = context.filectx(self.repo, self.path, fileid=node)
133 133 fnode = c.node()
134 134
135 135 def kwsub(mobj):
136 136 '''Substitutes keyword using corresponding template.'''
137 137 kw = mobj.group(1)
138 138 self.ct.use_template(self.templates[kw])
139 139 self.ui.pushbuffer()
140 140 self.ct.show(changenode=fnode, root=self.repo.root, file=self.path)
141 141 return '$%s: %s $' % (kw, templater.firstline(self.ui.popbuffer()))
142 142
143 143 return subfunc(kwsub, data)
144 144
145 145 def expand(self, node, data):
146 146 '''Returns data with keywords expanded.'''
147 147 if util.binary(data):
148 148 return data
149 149 return self.substitute(node, data, self.re_kw.sub)
150 150
151 151 def process(self, node, data, expand):
152 152 '''Returns a tuple: data, count.
153 153 Count is number of keywords/keyword substitutions, indicates
154 154 to caller whether to act on file containing data.
155 155 Keywords in data are expanded, if templater was initialized.'''
156 156 if util.binary(data):
157 157 return data, None
158 158 if expand:
159 159 return self.substitute(node, data, self.re_kw.subn)
160 160 return data, self.re_kw.search(data)
161 161
162 162 def shrink(self, text):
163 163 '''Returns text with all keyword substitutions removed.'''
164 164 if util.binary(text):
165 165 return text
166 166 return self.re_kw.sub(r'$\1$', text)
167 167
168 168 class kwfilelog(filelog.filelog):
169 169 '''
170 170 Subclass of filelog to hook into its read, add, cmp methods.
171 171 Keywords are "stored" unexpanded, and processed on reading.
172 172 '''
173 173 def __init__(self, opener, path):
174 174 super(kwfilelog, self).__init__(opener, path)
175 175 _kwtemplater.path = path
176 176
177 177 def kwctread(self, node, expand):
178 178 '''Reads expanding and counting keywords
179 179 (only called from kwtemplater.overwrite).'''
180 180 data = super(kwfilelog, self).read(node)
181 181 return _kwtemplater.process(node, data, expand)
182 182
183 183 def read(self, node):
184 184 '''Expands keywords when reading filelog.'''
185 185 data = super(kwfilelog, self).read(node)
186 186 return _kwtemplater.expand(node, data)
187 187
188 188 def add(self, text, meta, tr, link, p1=None, p2=None):
189 189 '''Removes keyword substitutions when adding to filelog.'''
190 190 text = _kwtemplater.shrink(text)
191 191 return super(kwfilelog, self).add(text, meta, tr, link, p1=p1, p2=p2)
192 192
193 193 def cmp(self, node, text):
194 194 '''Removes keyword substitutions for comparison.'''
195 195 text = _kwtemplater.shrink(text)
196 196 if self.renamed(node):
197 197 t2 = super(kwfilelog, self).read(node)
198 198 return t2 != text
199 199 return revlog.revlog.cmp(self, node, text)
200 200
201 201
202 202 # store original patch.patchfile.__init__
203 203 _patchfile_init = patch.patchfile.__init__
204 204
205 205 def _kwpatchfile_init(self, ui, fname, missing=False):
206 206 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
207 207 rejects or conflicts due to expanded keywords in working dir.'''
208 208 _patchfile_init(self, ui, fname, missing=missing)
209 209
210 210 if _kwtemplater.matcher(self.fname):
211 211 # shrink keywords read from working dir
212 212 kwshrunk = _kwtemplater.shrink(''.join(self.lines))
213 213 self.lines = kwshrunk.splitlines(True)
214 214
215 215
216 216 def _iskwfile(f, link):
217 217 return not link(f) and _kwtemplater.matcher(f)
218 218
219 219 def _status(ui, repo, *pats, **opts):
220 220 '''Bails out if [keyword] configuration is not active.
221 221 Returns status of working directory.'''
222 222 if _kwtemplater:
223 223 files, match, anypats = cmdutil.matchpats(repo, pats, opts)
224 224 return repo.status(files=files, match=match, list_clean=True)
225 225 if ui.configitems('keyword'):
226 226 raise util.Abort(_('[keyword] patterns cannot match'))
227 227 raise util.Abort(_('no [keyword] patterns configured'))
228 228
229 229 def _overwrite(ui, repo, node=None, expand=True, files=None):
230 230 '''Overwrites selected files expanding/shrinking keywords.'''
231 231 ctx = repo.changectx(node)
232 232 mf = ctx.manifest()
233 233 if node is not None: # commit
234 234 _kwtemplater.commitnode = node
235 235 files = [f for f in ctx.files() if mf.has_key(f)]
236 236 notify = ui.debug
237 237 else: # kwexpand/kwshrink
238 238 notify = ui.note
239 239 candidates = [f for f in files if _iskwfile(f, mf.linkf)]
240 240 if candidates:
241 241 candidates.sort()
242 242 action = expand and 'expanding' or 'shrinking'
243 243 for f in candidates:
244 244 fp = repo.file(f, kwmatch=True)
245 245 data, kwfound = fp.kwctread(mf[f], expand)
246 246 if kwfound:
247 247 notify(_('overwriting %s %s keywords\n') % (f, action))
248 248 repo.wwrite(f, data, mf.flags(f))
249 249 repo.dirstate.normal(f)
250 250
251 251 def _kwfwrite(ui, repo, expand, *pats, **opts):
252 252 '''Selects files and passes them to _overwrite.'''
253 253 status = _status(ui, repo, *pats, **opts)
254 254 modified, added, removed, deleted, unknown, ignored, clean = status
255 255 if modified or added or removed or deleted:
256 256 raise util.Abort(_('outstanding uncommitted changes in given files'))
257 257 wlock = lock = None
258 258 try:
259 259 wlock = repo.wlock()
260 260 lock = repo.lock()
261 261 _overwrite(ui, repo, expand=expand, files=clean)
262 262 finally:
263 263 del wlock, lock
264 264
265 265
266 266 def demo(ui, repo, *args, **opts):
267 267 '''print [keywordmaps] configuration and an expansion example
268 268
269 269 Show current, custom, or default keyword template maps
270 270 and their expansion.
271 271
272 272 Extend current configuration by specifying maps as arguments
273 273 and optionally by reading from an additional hgrc file.
274 274
275 275 Override current keyword template maps with "default" option.
276 276 '''
277 277 def demostatus(stat):
278 278 ui.status(_('\n\t%s\n') % stat)
279 279
280 280 def demoitems(section, items):
281 281 ui.write('[%s]\n' % section)
282 282 for k, v in items:
283 283 ui.write('%s = %s\n' % (k, v))
284 284
285 285 msg = 'hg keyword config and expansion example'
286 286 kwstatus = 'current'
287 287 fn = 'demo.txt'
288 288 branchname = 'demobranch'
289 289 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
290 290 ui.note(_('creating temporary repo at %s\n') % tmpdir)
291 291 repo = localrepo.localrepository(ui, path=tmpdir, create=True)
292 292 ui.setconfig('keyword', fn, '')
293 293 if args or opts.get('rcfile'):
294 294 kwstatus = 'custom'
295 295 if opts.get('rcfile'):
296 296 ui.readconfig(opts.get('rcfile'))
297 297 if opts.get('default'):
298 298 kwstatus = 'default'
299 299 kwmaps = kwtemplater.templates
300 300 if ui.configitems('keywordmaps'):
301 301 # override maps from optional rcfile
302 302 for k, v in kwmaps.items():
303 303 ui.setconfig('keywordmaps', k, v)
304 304 elif args:
305 305 # simulate hgrc parsing
306 306 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
307 307 fp = repo.opener('hgrc', 'w')
308 308 fp.writelines(rcmaps)
309 309 fp.close()
310 310 ui.readconfig(repo.join('hgrc'))
311 311 if not opts.get('default'):
312 312 kwmaps = dict(ui.configitems('keywordmaps')) or kwtemplater.templates
313 313 reposetup(ui, repo)
314 314 for k, v in ui.configitems('extensions'):
315 315 if k.endswith('keyword'):
316 316 extension = '%s = %s' % (k, v)
317 317 break
318 318 demostatus('config using %s keyword template maps' % kwstatus)
319 319 ui.write('[extensions]\n%s\n' % extension)
320 320 demoitems('keyword', ui.configitems('keyword'))
321 321 demoitems('keywordmaps', kwmaps.items())
322 322 keywords = '$' + '$\n$'.join(kwmaps.keys()) + '$\n'
323 323 repo.wopener(fn, 'w').write(keywords)
324 324 repo.add([fn])
325 325 path = repo.wjoin(fn)
326 326 ui.note(_('\n%s keywords written to %s:\n') % (kwstatus, path))
327 327 ui.note(keywords)
328 328 ui.note('\nhg -R "%s" branch "%s"\n' % (tmpdir, branchname))
329 329 # silence branch command if not verbose
330 330 quiet = ui.quiet
331 331 verbose = ui.verbose
332 332 ui.quiet = not verbose
333 333 commands.branch(ui, repo, branchname)
334 334 ui.quiet = quiet
335 335 for name, cmd in ui.configitems('hooks'):
336 336 if name.split('.', 1)[0].find('commit') > -1:
337 337 repo.ui.setconfig('hooks', name, '')
338 338 ui.note(_('unhooked all commit hooks\n'))
339 339 ui.note('hg -R "%s" ci -m "%s"\n' % (tmpdir, msg))
340 340 repo.commit(text=msg)
341 341 format = ui.verbose and ' in %s' % path or ''
342 342 demostatus('%s keywords expanded%s' % (kwstatus, format))
343 343 ui.write(repo.wread(fn))
344 344 ui.debug(_('\nremoving temporary repo %s\n') % tmpdir)
345 345 shutil.rmtree(tmpdir, ignore_errors=True)
346 346
347 347 def expand(ui, repo, *pats, **opts):
348 348 '''expand keywords in working directory
349 349
350 350 Run after (re)enabling keyword expansion.
351 351
352 352 kwexpand refuses to run if given files contain local changes.
353 353 '''
354 354 # 3rd argument sets expansion to True
355 355 _kwfwrite(ui, repo, True, *pats, **opts)
356 356
357 357 def files(ui, repo, *pats, **opts):
358 358 '''print files currently configured for keyword expansion
359 359
360 360 Crosscheck which files in working directory are potential targets for
361 361 keyword expansion.
362 362 That is, files matched by [keyword] config patterns but not symlinks.
363 363 '''
364 364 status = _status(ui, repo, *pats, **opts)
365 365 modified, added, removed, deleted, unknown, ignored, clean = status
366 366 if opts.get('untracked'):
367 367 files = modified + added + unknown + clean
368 368 else:
369 369 files = modified + added + clean
370 370 files.sort()
371 371 kwfiles = [f for f in files if _iskwfile(f, repo._link)]
372 372 cwd = pats and repo.getcwd() or ''
373 373 kwfstats = not opts.get('ignore') and (('K', kwfiles),) or ()
374 374 if opts.get('all') or opts.get('ignore'):
375 375 kwfstats += (('I', [f for f in files if f not in kwfiles]),)
376 376 for char, filenames in kwfstats:
377 377 format = (opts.get('all') or ui.verbose) and '%s %%s\n' % char or '%s\n'
378 378 for f in filenames:
379 379 ui.write(format % repo.pathto(f, cwd))
380 380
381 381 def shrink(ui, repo, *pats, **opts):
382 382 '''revert expanded keywords in working directory
383 383
384 384 Run before changing/disabling active keywords
385 385 or if you experience problems with "hg import" or "hg merge".
386 386
387 387 kwshrink refuses to run if given files contain local changes.
388 388 '''
389 389 # 3rd argument sets expansion to False
390 390 _kwfwrite(ui, repo, False, *pats, **opts)
391 391
392 392
393 393 def reposetup(ui, repo):
394 394 '''Sets up repo as kwrepo for keyword substitution.
395 395 Overrides file method to return kwfilelog instead of filelog
396 396 if file matches user configuration.
397 397 Wraps commit to overwrite configured files with updated
398 398 keyword substitutions.
399 399 This is done for local repos only, and only if there are
400 400 files configured at all for keyword substitution.'''
401 401
402 402 def kwbailout():
403 403 '''Obtains command via simplified cmdline parsing,
404 404 returns True if keyword expansion not needed.'''
405 405 nokwcommands = ('add', 'addremove', 'bundle', 'clone', 'copy',
406 406 'export', 'grep', 'identify', 'incoming', 'init',
407 'outgoing', 'push', 'remove', 'rename', 'rollback',
407 'log', 'outgoing', 'push', 'remove', 'rename',
408 'rollback', 'tip',
408 409 'convert')
409 410 args = fancyopts.fancyopts(sys.argv[1:], commands.globalopts, {})
410 411 if args:
411 412 aliases, i = cmdutil.findcmd(ui, args[0], commands.table)
412 413 return aliases[0] in nokwcommands
413 414
414 415 if not repo.local() or kwbailout():
415 416 return
416 417
417 418 inc, exc = [], ['.hgtags']
418 419 for pat, opt in ui.configitems('keyword'):
419 420 if opt != 'ignore':
420 421 inc.append(pat)
421 422 else:
422 423 exc.append(pat)
423 424 if not inc:
424 425 return
425 426
426 427 global _kwtemplater
427 428 _kwtemplater = kwtemplater(ui, repo, inc, exc)
428 429
429 430 class kwrepo(repo.__class__):
430 431 def file(self, f, kwmatch=False):
431 432 if f[0] == '/':
432 433 f = f[1:]
433 434 if kwmatch or _kwtemplater.matcher(f):
434 435 return kwfilelog(self.sopener, f)
435 436 return filelog.filelog(self.sopener, f)
436 437
437 438 def commit(self, files=None, text='', user=None, date=None,
438 439 match=util.always, force=False, force_editor=False,
439 440 p1=None, p2=None, extra={}):
440 441 wlock = lock = None
441 442 _p1 = _p2 = None
442 443 try:
443 444 wlock = self.wlock()
444 445 lock = self.lock()
445 446 # store and postpone commit hooks
446 447 commithooks = []
447 448 for name, cmd in ui.configitems('hooks'):
448 449 if name.split('.', 1)[0] == 'commit':
449 450 commithooks.append((name, cmd))
450 451 ui.setconfig('hooks', name, None)
451 452 if commithooks:
452 453 # store parents for commit hook environment
453 454 if p1 is None:
454 455 _p1, _p2 = repo.dirstate.parents()
455 456 else:
456 457 _p1, _p2 = p1, p2 or nullid
457 458 _p1 = hex(_p1)
458 459 if _p2 == nullid:
459 460 _p2 = ''
460 461 else:
461 462 _p2 = hex(_p2)
462 463
463 464 node = super(kwrepo,
464 465 self).commit(files=files, text=text, user=user,
465 466 date=date, match=match, force=force,
466 467 force_editor=force_editor,
467 468 p1=p1, p2=p2, extra=extra)
468 469
469 470 # restore commit hooks
470 471 for name, cmd in commithooks:
471 472 ui.setconfig('hooks', name, cmd)
472 473 if node is not None:
473 474 _overwrite(ui, self, node=node)
474 475 repo.hook('commit', node=node, parent1=_p1, parent2=_p2)
475 476 return node
476 477 finally:
477 478 del wlock, lock
478 479
479 480 repo.__class__ = kwrepo
480 481 patch.patchfile.__init__ = _kwpatchfile_init
481 482
482 483
483 484 cmdtable = {
484 485 'kwdemo':
485 486 (demo,
486 487 [('d', 'default', None, _('show default keyword template maps')),
487 488 ('f', 'rcfile', [], _('read maps from rcfile'))],
488 489 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')),
489 490 'kwexpand': (expand, commands.walkopts,
490 491 _('hg kwexpand [OPTION]... [FILE]...')),
491 492 'kwfiles':
492 493 (files,
493 494 [('a', 'all', None, _('show keyword status flags of all files')),
494 495 ('i', 'ignore', None, _('show files excluded from expansion')),
495 496 ('u', 'untracked', None, _('additionally show untracked files')),
496 497 ] + commands.walkopts,
497 498 _('hg kwfiles [OPTION]... [FILE]...')),
498 499 'kwshrink': (shrink, commands.walkopts,
499 500 _('hg kwshrink [OPTION]... [FILE]...')),
500 501 }
General Comments 0
You need to be logged in to leave comments. Login now