##// END OF EJS Templates
keyword: do not shadow builtin format (detected by pychecker)
Christian Ebert -
r7417:d3f1d316 default
parent child Browse files
Show More
@@ -1,536 +1,536 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, dispatch, filelog, revlog, extensions
82 82 from mercurial import patch, localrepo, templater, templatefilters, util
83 83 from mercurial.hgweb import webcommands
84 84 from mercurial.node import nullid, hex
85 85 from mercurial.i18n import _
86 86 import re, shutil, tempfile, time
87 87
88 88 commands.optionalrepo += ' kwdemo'
89 89
90 90 # hg commands that do not act on keywords
91 91 nokwcommands = ('add addremove annotate bundle copy export grep incoming init'
92 92 ' log outgoing push rename rollback tip verify'
93 93 ' convert email glog')
94 94
95 95 # hg commands that trigger expansion only when writing to working dir,
96 96 # not when reading filelog, and unexpand when reading from working dir
97 97 restricted = 'merge record resolve qfold qimport qnew qpush qrefresh qrecord'
98 98
99 99 def utcdate(date):
100 100 '''Returns hgdate in cvs-like UTC format.'''
101 101 return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0]))
102 102
103 103 # make keyword tools accessible
104 104 kwtools = {'templater': None, 'hgcmd': '', 'inc': [], 'exc': ['.hg*']}
105 105
106 106
107 107 class kwtemplater(object):
108 108 '''
109 109 Sets up keyword templates, corresponding keyword regex, and
110 110 provides keyword substitution functions.
111 111 '''
112 112 templates = {
113 113 'Revision': '{node|short}',
114 114 'Author': '{author|user}',
115 115 'Date': '{date|utcdate}',
116 116 'RCSFile': '{file|basename},v',
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.matcher = util.matcher(repo.root,
126 126 inc=kwtools['inc'], exc=kwtools['exc'])[1]
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 kwmaps = [(k, templater.parsestring(v, False))
132 132 for (k, v) in kwmaps]
133 133 self.templates = dict(kwmaps)
134 134 escaped = map(re.escape, self.templates.keys())
135 135 kwpat = r'\$(%s)(: [^$\n\r]*? )??\$' % '|'.join(escaped)
136 136 self.re_kw = re.compile(kwpat)
137 137
138 138 templatefilters.filters['utcdate'] = utcdate
139 139 self.ct = cmdutil.changeset_templater(self.ui, self.repo,
140 140 False, '', False)
141 141
142 142 def substitute(self, data, path, ctx, subfunc):
143 143 '''Replaces keywords in data with expanded template.'''
144 144 def kwsub(mobj):
145 145 kw = mobj.group(1)
146 146 self.ct.use_template(self.templates[kw])
147 147 self.ui.pushbuffer()
148 148 self.ct.show(ctx, root=self.repo.root, file=path)
149 149 ekw = templatefilters.firstline(self.ui.popbuffer())
150 150 return '$%s: %s $' % (kw, ekw)
151 151 return subfunc(kwsub, data)
152 152
153 153 def expand(self, path, node, data):
154 154 '''Returns data with keywords expanded.'''
155 155 if not self.restrict and self.matcher(path) and not util.binary(data):
156 156 ctx = self.repo.filectx(path, fileid=node).changectx()
157 157 return self.substitute(data, path, ctx, self.re_kw.sub)
158 158 return data
159 159
160 160 def iskwfile(self, path, flagfunc):
161 161 '''Returns true if path matches [keyword] pattern
162 162 and is not a symbolic link.
163 163 Caveat: localrepository._link fails on Windows.'''
164 164 return self.matcher(path) and not 'l' in flagfunc(path)
165 165
166 166 def overwrite(self, node, expand, files):
167 167 '''Overwrites selected files expanding/shrinking keywords.'''
168 168 ctx = self.repo[node]
169 169 mf = ctx.manifest()
170 170 if node is not None: # commit
171 171 files = [f for f in ctx.files() if f in mf]
172 172 notify = self.ui.debug
173 173 else: # kwexpand/kwshrink
174 174 notify = self.ui.note
175 175 candidates = [f for f in files if self.iskwfile(f, ctx.flags)]
176 176 if candidates:
177 177 self.restrict = True # do not expand when reading
178 178 action = expand and 'expanding' or 'shrinking'
179 179 for f in candidates:
180 180 fp = self.repo.file(f)
181 181 data = fp.read(mf[f])
182 182 if util.binary(data):
183 183 continue
184 184 if expand:
185 185 if node is None:
186 186 ctx = self.repo.filectx(f, fileid=mf[f]).changectx()
187 187 data, found = self.substitute(data, f, ctx,
188 188 self.re_kw.subn)
189 189 else:
190 190 found = self.re_kw.search(data)
191 191 if found:
192 192 notify(_('overwriting %s %s keywords\n') % (f, action))
193 193 self.repo.wwrite(f, data, mf.flags(f))
194 194 self.repo.dirstate.normal(f)
195 195 self.restrict = False
196 196
197 197 def shrinktext(self, text):
198 198 '''Unconditionally removes all keyword substitutions from text.'''
199 199 return self.re_kw.sub(r'$\1$', text)
200 200
201 201 def shrink(self, fname, text):
202 202 '''Returns text with all keyword substitutions removed.'''
203 203 if self.matcher(fname) and not util.binary(text):
204 204 return self.shrinktext(text)
205 205 return text
206 206
207 207 def shrinklines(self, fname, lines):
208 208 '''Returns lines with keyword substitutions removed.'''
209 209 if self.matcher(fname):
210 210 text = ''.join(lines)
211 211 if not util.binary(text):
212 212 return self.shrinktext(text).splitlines(True)
213 213 return lines
214 214
215 215 def wread(self, fname, data):
216 216 '''If in restricted mode returns data read from wdir with
217 217 keyword substitutions removed.'''
218 218 return self.restrict and self.shrink(fname, data) or data
219 219
220 220 class kwfilelog(filelog.filelog):
221 221 '''
222 222 Subclass of filelog to hook into its read, add, cmp methods.
223 223 Keywords are "stored" unexpanded, and processed on reading.
224 224 '''
225 225 def __init__(self, opener, kwt, path):
226 226 super(kwfilelog, self).__init__(opener, path)
227 227 self.kwt = kwt
228 228 self.path = path
229 229
230 230 def read(self, node):
231 231 '''Expands keywords when reading filelog.'''
232 232 data = super(kwfilelog, self).read(node)
233 233 return self.kwt.expand(self.path, node, data)
234 234
235 235 def add(self, text, meta, tr, link, p1=None, p2=None):
236 236 '''Removes keyword substitutions when adding to filelog.'''
237 237 text = self.kwt.shrink(self.path, text)
238 238 return super(kwfilelog, self).add(text, meta, tr, link, p1, p2)
239 239
240 240 def cmp(self, node, text):
241 241 '''Removes keyword substitutions for comparison.'''
242 242 text = self.kwt.shrink(self.path, text)
243 243 if self.renamed(node):
244 244 t2 = super(kwfilelog, self).read(node)
245 245 return t2 != text
246 246 return revlog.revlog.cmp(self, node, text)
247 247
248 248 def _status(ui, repo, kwt, unknown, *pats, **opts):
249 249 '''Bails out if [keyword] configuration is not active.
250 250 Returns status of working directory.'''
251 251 if kwt:
252 252 matcher = cmdutil.match(repo, pats, opts)
253 253 return repo.status(match=matcher, unknown=unknown, clean=True)
254 254 if ui.configitems('keyword'):
255 255 raise util.Abort(_('[keyword] patterns cannot match'))
256 256 raise util.Abort(_('no [keyword] patterns configured'))
257 257
258 258 def _kwfwrite(ui, repo, expand, *pats, **opts):
259 259 '''Selects files and passes them to kwtemplater.overwrite.'''
260 260 if repo.dirstate.parents()[1] != nullid:
261 261 raise util.Abort(_('outstanding uncommitted merge'))
262 262 kwt = kwtools['templater']
263 263 status = _status(ui, repo, kwt, False, *pats, **opts)
264 264 modified, added, removed, deleted = status[:4]
265 265 if modified or added or removed or deleted:
266 266 raise util.Abort(_('outstanding uncommitted changes'))
267 267 wlock = lock = None
268 268 try:
269 269 wlock = repo.wlock()
270 270 lock = repo.lock()
271 271 kwt.overwrite(None, expand, status[6])
272 272 finally:
273 273 del wlock, lock
274 274
275 275
276 276 def demo(ui, repo, *args, **opts):
277 277 '''print [keywordmaps] configuration and an expansion example
278 278
279 279 Show current, custom, or default keyword template maps
280 280 and their expansion.
281 281
282 282 Extend current configuration by specifying maps as arguments
283 283 and optionally by reading from an additional hgrc file.
284 284
285 285 Override current keyword template maps with "default" option.
286 286 '''
287 287 def demostatus(stat):
288 288 ui.status(_('\n\t%s\n') % stat)
289 289
290 290 def demoitems(section, items):
291 291 ui.write('[%s]\n' % section)
292 292 for k, v in items:
293 293 ui.write('%s = %s\n' % (k, v))
294 294
295 295 msg = 'hg keyword config and expansion example'
296 296 kwstatus = 'current'
297 297 fn = 'demo.txt'
298 298 branchname = 'demobranch'
299 299 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
300 300 ui.note(_('creating temporary repo at %s\n') % tmpdir)
301 301 repo = localrepo.localrepository(ui, tmpdir, True)
302 302 ui.setconfig('keyword', fn, '')
303 303 if args or opts.get('rcfile'):
304 304 kwstatus = 'custom'
305 305 if opts.get('rcfile'):
306 306 ui.readconfig(opts.get('rcfile'))
307 307 if opts.get('default'):
308 308 kwstatus = 'default'
309 309 kwmaps = kwtemplater.templates
310 310 if ui.configitems('keywordmaps'):
311 311 # override maps from optional rcfile
312 312 for k, v in kwmaps.iteritems():
313 313 ui.setconfig('keywordmaps', k, v)
314 314 elif args:
315 315 # simulate hgrc parsing
316 316 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
317 317 fp = repo.opener('hgrc', 'w')
318 318 fp.writelines(rcmaps)
319 319 fp.close()
320 320 ui.readconfig(repo.join('hgrc'))
321 321 if not opts.get('default'):
322 322 kwmaps = dict(ui.configitems('keywordmaps')) or kwtemplater.templates
323 323 uisetup(ui)
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 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 format = ui.verbose and ' in %s' % path or ''
352 demostatus('%s keywords expanded%s' % (kwstatus, format))
351 fmt = ui.verbose and ' in %s' % path or ''
352 demostatus('%s keywords expanded%s' % (kwstatus, fmt))
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 kwt = kwtools['templater']
375 375 status = _status(ui, repo, kwt, opts.get('untracked'), *pats, **opts)
376 376 modified, added, removed, deleted, unknown, ignored, clean = status
377 377 files = util.sort(modified + added + clean + unknown)
378 378 wctx = repo[None]
379 379 kwfiles = [f for f in files if kwt.iskwfile(f, wctx.flags)]
380 380 cwd = pats and repo.getcwd() or ''
381 381 kwfstats = not opts.get('ignore') and (('K', kwfiles),) or ()
382 382 if opts.get('all') or opts.get('ignore'):
383 383 kwfstats += (('I', [f for f in files if f not in kwfiles]),)
384 384 for char, filenames in kwfstats:
385 format = (opts.get('all') or ui.verbose) and '%s %%s\n' % char or '%s\n'
385 fmt = (opts.get('all') or ui.verbose) and '%s %%s\n' % char or '%s\n'
386 386 for f in filenames:
387 ui.write(format % repo.pathto(f, cwd))
387 ui.write(fmt % repo.pathto(f, cwd))
388 388
389 389 def shrink(ui, repo, *pats, **opts):
390 390 '''revert expanded keywords in working directory
391 391
392 392 Run before changing/disabling active keywords
393 393 or if you experience problems with "hg import" or "hg merge".
394 394
395 395 kwshrink refuses to run if given files contain local changes.
396 396 '''
397 397 # 3rd argument sets expansion to False
398 398 _kwfwrite(ui, repo, False, *pats, **opts)
399 399
400 400
401 401 def uisetup(ui):
402 402 '''Collects [keyword] config in kwtools.
403 403 Monkeypatches dispatch._parse if needed.'''
404 404
405 405 for pat, opt in ui.configitems('keyword'):
406 406 if opt != 'ignore':
407 407 kwtools['inc'].append(pat)
408 408 else:
409 409 kwtools['exc'].append(pat)
410 410
411 411 if kwtools['inc']:
412 412 def kwdispatch_parse(orig, ui, args):
413 413 '''Monkeypatch dispatch._parse to obtain running hg command.'''
414 414 cmd, func, args, options, cmdoptions = orig(ui, args)
415 415 kwtools['hgcmd'] = cmd
416 416 return cmd, func, args, options, cmdoptions
417 417
418 418 extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse)
419 419
420 420 def reposetup(ui, repo):
421 421 '''Sets up repo as kwrepo for keyword substitution.
422 422 Overrides file method to return kwfilelog instead of filelog
423 423 if file matches user configuration.
424 424 Wraps commit to overwrite configured files with updated
425 425 keyword substitutions.
426 426 Monkeypatches patch and webcommands.'''
427 427
428 428 try:
429 429 if (not repo.local() or not kwtools['inc']
430 430 or kwtools['hgcmd'] in nokwcommands.split()
431 431 or '.hg' in util.splitpath(repo.root)
432 432 or repo._url.startswith('bundle:')):
433 433 return
434 434 except AttributeError:
435 435 pass
436 436
437 437 kwtools['templater'] = kwt = kwtemplater(ui, repo)
438 438
439 439 class kwrepo(repo.__class__):
440 440 def file(self, f):
441 441 if f[0] == '/':
442 442 f = f[1:]
443 443 return kwfilelog(self.sopener, kwt, f)
444 444
445 445 def wread(self, filename):
446 446 data = super(kwrepo, self).wread(filename)
447 447 return kwt.wread(filename, data)
448 448
449 449 def commit(self, files=None, text='', user=None, date=None,
450 450 match=None, force=False, force_editor=False,
451 451 p1=None, p2=None, extra={}, empty_ok=False):
452 452 wlock = lock = None
453 453 _p1 = _p2 = None
454 454 try:
455 455 wlock = self.wlock()
456 456 lock = self.lock()
457 457 # store and postpone commit hooks
458 458 commithooks = {}
459 459 for name, cmd in ui.configitems('hooks'):
460 460 if name.split('.', 1)[0] == 'commit':
461 461 commithooks[name] = cmd
462 462 ui.setconfig('hooks', name, None)
463 463 if commithooks:
464 464 # store parents for commit hook environment
465 465 if p1 is None:
466 466 _p1, _p2 = repo.dirstate.parents()
467 467 else:
468 468 _p1, _p2 = p1, p2 or nullid
469 469 _p1 = hex(_p1)
470 470 if _p2 == nullid:
471 471 _p2 = ''
472 472 else:
473 473 _p2 = hex(_p2)
474 474
475 475 n = super(kwrepo, self).commit(files, text, user, date, match,
476 476 force, force_editor, p1, p2,
477 477 extra, empty_ok)
478 478
479 479 # restore commit hooks
480 480 for name, cmd in commithooks.iteritems():
481 481 ui.setconfig('hooks', name, cmd)
482 482 if n is not None:
483 483 kwt.overwrite(n, True, None)
484 484 repo.hook('commit', node=n, parent1=_p1, parent2=_p2)
485 485 return n
486 486 finally:
487 487 del wlock, lock
488 488
489 489 # monkeypatches
490 490 def kwpatchfile_init(orig, self, ui, fname, opener, missing=False):
491 491 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
492 492 rejects or conflicts due to expanded keywords in working dir.'''
493 493 orig(self, ui, fname, opener, missing)
494 494 # shrink keywords read from working dir
495 495 self.lines = kwt.shrinklines(self.fname, self.lines)
496 496
497 497 def kw_diff(orig, repo, node1=None, node2=None, match=None, changes=None,
498 498 opts=None):
499 499 '''Monkeypatch patch.diff to avoid expansion except when
500 500 comparing against working dir.'''
501 501 if node2 is not None:
502 502 kwt.matcher = util.never
503 503 elif node1 is not None and node1 != repo['.'].node():
504 504 kwt.restrict = True
505 505 return orig(repo, node1, node2, match, changes, opts)
506 506
507 507 def kwweb_skip(orig, web, req, tmpl):
508 508 '''Wraps webcommands.x turning off keyword expansion.'''
509 509 kwt.matcher = util.never
510 510 return orig(web, req, tmpl)
511 511
512 512 repo.__class__ = kwrepo
513 513
514 514 extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init)
515 515 extensions.wrapfunction(patch, 'diff', kw_diff)
516 516 for c in 'annotate changeset rev filediff diff'.split():
517 517 extensions.wrapfunction(webcommands, c, kwweb_skip)
518 518
519 519 cmdtable = {
520 520 'kwdemo':
521 521 (demo,
522 522 [('d', 'default', None, _('show default keyword template maps')),
523 523 ('f', 'rcfile', [], _('read maps from rcfile'))],
524 524 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')),
525 525 'kwexpand': (expand, commands.walkopts,
526 526 _('hg kwexpand [OPTION]... [FILE]...')),
527 527 'kwfiles':
528 528 (files,
529 529 [('a', 'all', None, _('show keyword status flags of all files')),
530 530 ('i', 'ignore', None, _('show files excluded from expansion')),
531 531 ('u', 'untracked', None, _('additionally show untracked files')),
532 532 ] + commands.walkopts,
533 533 _('hg kwfiles [OPTION]... [FILE]...')),
534 534 'kwshrink': (shrink, commands.walkopts,
535 535 _('hg kwshrink [OPTION]... [FILE]...')),
536 536 }
General Comments 0
You need to be logged in to leave comments. Login now