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