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