##// END OF EJS Templates
keyword: remove deprecated options
Christian Ebert -
r10652:e7f840e4 default
parent child Browse files
Show More
@@ -1,542 +1,537 b''
1 1 # keyword.py - $Keyword$ expansion for Mercurial
2 2 #
3 3 # Copyright 2007-2009 Christian Ebert <blacktrash@gmx.net>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
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
15 15 # audience not running a version control system.
16 16 #
17 17 # For in-depth discussion refer to
18 18 # <http://mercurial.selenic.com/wiki/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 # Files to act upon/ignore are specified in the [keyword] section.
25 25 # Customized keyword template mappings in the [keywordmaps] section.
26 26 #
27 27 # Run "hg help keyword" and "hg kwdemo" to get info on configuration.
28 28
29 29 '''expand keywords in tracked files
30 30
31 31 This extension expands RCS/CVS-like or self-customized $Keywords$ in
32 32 tracked text files selected by your configuration.
33 33
34 34 Keywords are only expanded in local repositories and not stored in the
35 35 change history. The mechanism can be regarded as a convenience for the
36 36 current user or for archive distribution.
37 37
38 38 Configuration is done in the [keyword] and [keywordmaps] sections of
39 39 hgrc files.
40 40
41 41 Example::
42 42
43 43 [keyword]
44 44 # expand keywords in every python file except those matching "x*"
45 45 **.py =
46 46 x* = ignore
47 47
48 48 NOTE: the more specific you are in your filename patterns the less you
49 49 lose speed in huge repositories.
50 50
51 51 For [keywordmaps] template mapping and expansion demonstration and
52 52 control run "hg kwdemo". See "hg help templates" for a list of
53 53 available templates and filters.
54 54
55 55 An additional date template filter {date|utcdate} is provided. It
56 56 returns a date like "2006/09/18 15:13:13".
57 57
58 58 The default template mappings (view with "hg kwdemo -d") can be
59 59 replaced with customized keywords and templates. Again, run "hg
60 60 kwdemo" to control the results of your config changes.
61 61
62 62 Before changing/disabling active keywords, run "hg kwshrink" to avoid
63 63 the risk of inadvertently storing expanded keywords in the change
64 64 history.
65 65
66 66 To force expansion after enabling it, or a configuration change, run
67 67 "hg kwexpand".
68 68
69 69 Also, when committing with the record extension or using mq's qrecord,
70 70 be aware that keywords cannot be updated. Again, run "hg kwexpand" on
71 71 the files in question to update keyword expansions after all changes
72 72 have been checked in.
73 73
74 74 Expansions spanning more than one line and incremental expansions,
75 75 like CVS' $Log$, are not supported. A keyword template map "Log =
76 76 {desc}" expands to the first line of the changeset description.
77 77 '''
78 78
79 79 from mercurial import commands, cmdutil, dispatch, filelog, revlog, extensions
80 80 from mercurial import patch, localrepo, templater, templatefilters, util, match
81 81 from mercurial.hgweb import webcommands
82 82 from mercurial.node import nullid
83 83 from mercurial.i18n import _
84 84 import re, shutil, tempfile
85 85
86 86 commands.optionalrepo += ' kwdemo'
87 87
88 88 # hg commands that do not act on keywords
89 89 nokwcommands = ('add addremove annotate bundle copy export grep incoming init'
90 90 ' log outgoing push rename rollback tip verify'
91 91 ' convert email glog')
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 = ('merge record resolve qfold qimport qnew qpush qrefresh qrecord'
96 96 ' transplant')
97 97
98 98 # provide cvs-like UTC date filter
99 99 utcdate = lambda x: util.datestr((x[0], 0), '%Y/%m/%d %H:%M:%S')
100 100
101 101 # make keyword tools accessible
102 102 kwtools = {'templater': None, 'hgcmd': '', 'inc': [], 'exc': ['.hg*']}
103 103
104 104
105 105 class kwtemplater(object):
106 106 '''
107 107 Sets up keyword templates, corresponding keyword regex, and
108 108 provides keyword substitution functions.
109 109 '''
110 110 templates = {
111 111 'Revision': '{node|short}',
112 112 'Author': '{author|user}',
113 113 'Date': '{date|utcdate}',
114 114 'RCSfile': '{file|basename},v',
115 115 'RCSFile': '{file|basename},v', # kept for backwards compatibility
116 116 # with hg-keyword
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.match = match.match(repo.root, '', [],
126 126 kwtools['inc'], kwtools['exc'])
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 self.templates = dict((k, templater.parsestring(v, False))
132 132 for k, v in kwmaps)
133 133 escaped = map(re.escape, self.templates.keys())
134 134 kwpat = r'\$(%s)(: [^$\n\r]*? )??\$' % '|'.join(escaped)
135 135 self.re_kw = re.compile(kwpat)
136 136
137 137 templatefilters.filters['utcdate'] = utcdate
138 138 self.ct = cmdutil.changeset_templater(self.ui, self.repo,
139 139 False, None, '', False)
140 140
141 141 def substitute(self, data, path, ctx, subfunc):
142 142 '''Replaces keywords in data with expanded template.'''
143 143 def kwsub(mobj):
144 144 kw = mobj.group(1)
145 145 self.ct.use_template(self.templates[kw])
146 146 self.ui.pushbuffer()
147 147 self.ct.show(ctx, root=self.repo.root, file=path)
148 148 ekw = templatefilters.firstline(self.ui.popbuffer())
149 149 return '$%s: %s $' % (kw, ekw)
150 150 return subfunc(kwsub, data)
151 151
152 152 def expand(self, path, node, data):
153 153 '''Returns data with keywords expanded.'''
154 154 if not self.restrict and self.match(path) and not util.binary(data):
155 155 ctx = self.repo.filectx(path, fileid=node).changectx()
156 156 return self.substitute(data, path, ctx, self.re_kw.sub)
157 157 return data
158 158
159 159 def iskwfile(self, path, flagfunc):
160 160 '''Returns true if path matches [keyword] pattern
161 161 and is not a symbolic link.
162 162 Caveat: localrepository._link fails on Windows.'''
163 163 return self.match(path) and not 'l' in flagfunc(path)
164 164
165 165 def overwrite(self, node, expand, files):
166 166 '''Overwrites selected files expanding/shrinking keywords.'''
167 167 ctx = self.repo[node]
168 168 mf = ctx.manifest()
169 169 if node is not None: # commit
170 170 files = [f for f in ctx.files() if f in mf]
171 171 notify = self.ui.debug
172 172 else: # kwexpand/kwshrink
173 173 notify = self.ui.note
174 174 candidates = [f for f in files if self.iskwfile(f, ctx.flags)]
175 175 if candidates:
176 176 self.restrict = True # do not expand when reading
177 177 msg = (expand and _('overwriting %s expanding keywords\n')
178 178 or _('overwriting %s shrinking keywords\n'))
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(msg % f)
193 193 self.repo.wwrite(f, data, mf.flags(f))
194 194 if node is None:
195 195 self.repo.dirstate.normal(f)
196 196 self.restrict = False
197 197
198 198 def shrinktext(self, text):
199 199 '''Unconditionally removes all keyword substitutions from text.'''
200 200 return self.re_kw.sub(r'$\1$', text)
201 201
202 202 def shrink(self, fname, text):
203 203 '''Returns text with all keyword substitutions removed.'''
204 204 if self.match(fname) and not util.binary(text):
205 205 return self.shrinktext(text)
206 206 return text
207 207
208 208 def shrinklines(self, fname, lines):
209 209 '''Returns lines with keyword substitutions removed.'''
210 210 if self.match(fname):
211 211 text = ''.join(lines)
212 212 if not util.binary(text):
213 213 return self.shrinktext(text).splitlines(True)
214 214 return lines
215 215
216 216 def wread(self, fname, data):
217 217 '''If in restricted mode returns data read from wdir with
218 218 keyword substitutions removed.'''
219 219 return self.restrict and self.shrink(fname, data) or data
220 220
221 221 class kwfilelog(filelog.filelog):
222 222 '''
223 223 Subclass of filelog to hook into its read, add, cmp methods.
224 224 Keywords are "stored" unexpanded, and processed on reading.
225 225 '''
226 226 def __init__(self, opener, kwt, path):
227 227 super(kwfilelog, self).__init__(opener, path)
228 228 self.kwt = kwt
229 229 self.path = path
230 230
231 231 def read(self, node):
232 232 '''Expands keywords when reading filelog.'''
233 233 data = super(kwfilelog, self).read(node)
234 234 return self.kwt.expand(self.path, node, data)
235 235
236 236 def add(self, text, meta, tr, link, p1=None, p2=None):
237 237 '''Removes keyword substitutions when adding to filelog.'''
238 238 text = self.kwt.shrink(self.path, text)
239 239 return super(kwfilelog, self).add(text, meta, tr, link, p1, p2)
240 240
241 241 def cmp(self, node, text):
242 242 '''Removes keyword substitutions for comparison.'''
243 243 text = self.kwt.shrink(self.path, text)
244 244 if self.renamed(node):
245 245 t2 = super(kwfilelog, self).read(node)
246 246 return t2 != text
247 247 return revlog.revlog.cmp(self, node, text)
248 248
249 249 def _status(ui, repo, kwt, *pats, **opts):
250 250 '''Bails out if [keyword] configuration is not active.
251 251 Returns status of working directory.'''
252 252 if kwt:
253 unknown = (opts.get('unknown') or opts.get('all')
254 or opts.get('untracked'))
255 253 return repo.status(match=cmdutil.match(repo, pats, opts), clean=True,
256 unknown=unknown)
254 unknown=opts.get('unknown') or opts.get('all'))
257 255 if ui.configitems('keyword'):
258 256 raise util.Abort(_('[keyword] patterns cannot match'))
259 257 raise util.Abort(_('no [keyword] patterns configured'))
260 258
261 259 def _kwfwrite(ui, repo, expand, *pats, **opts):
262 260 '''Selects files and passes them to kwtemplater.overwrite.'''
263 261 if repo.dirstate.parents()[1] != nullid:
264 262 raise util.Abort(_('outstanding uncommitted merge'))
265 263 kwt = kwtools['templater']
266 264 wlock = repo.wlock()
267 265 try:
268 266 status = _status(ui, repo, kwt, *pats, **opts)
269 267 modified, added, removed, deleted, unknown, ignored, clean = status
270 268 if modified or added or removed or deleted:
271 269 raise util.Abort(_('outstanding uncommitted changes'))
272 270 kwt.overwrite(None, expand, clean)
273 271 finally:
274 272 wlock.release()
275 273
276 274 def demo(ui, repo, *args, **opts):
277 275 '''print [keywordmaps] configuration and an expansion example
278 276
279 277 Show current, custom, or default keyword template maps and their
280 278 expansions.
281 279
282 280 Extend the current configuration by specifying maps as arguments
283 281 and using -f/--rcfile to source an external hgrc file.
284 282
285 283 Use -d/--default to disable current configuration.
286 284
287 285 See "hg help templates" for information on templates and filters.
288 286 '''
289 287 def demoitems(section, items):
290 288 ui.write('[%s]\n' % section)
291 289 for k, v in sorted(items):
292 290 ui.write('%s = %s\n' % (k, v))
293 291
294 292 fn = 'demo.txt'
295 293 branchname = 'demobranch'
296 294 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
297 295 ui.note(_('creating temporary repository at %s\n') % tmpdir)
298 296 repo = localrepo.localrepository(ui, tmpdir, True)
299 297 ui.setconfig('keyword', fn, '')
300 298
301 299 uikwmaps = ui.configitems('keywordmaps')
302 300 if args or opts.get('rcfile'):
303 301 ui.status(_('\n\tconfiguration using custom keyword template maps\n'))
304 302 if uikwmaps:
305 303 ui.status(_('\textending current template maps\n'))
306 304 if opts.get('default') or not uikwmaps:
307 305 ui.status(_('\toverriding default template maps\n'))
308 306 if opts.get('rcfile'):
309 307 ui.readconfig(opts.get('rcfile'))
310 308 if args:
311 309 # simulate hgrc parsing
312 310 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
313 311 fp = repo.opener('hgrc', 'w')
314 312 fp.writelines(rcmaps)
315 313 fp.close()
316 314 ui.readconfig(repo.join('hgrc'))
317 315 kwmaps = dict(ui.configitems('keywordmaps'))
318 316 elif opts.get('default'):
319 317 ui.status(_('\n\tconfiguration using default keyword template maps\n'))
320 318 kwmaps = kwtemplater.templates
321 319 if uikwmaps:
322 320 ui.status(_('\tdisabling current template maps\n'))
323 321 for k, v in kwmaps.iteritems():
324 322 ui.setconfig('keywordmaps', k, v)
325 323 else:
326 324 ui.status(_('\n\tconfiguration using current keyword template maps\n'))
327 325 kwmaps = dict(uikwmaps) or kwtemplater.templates
328 326
329 327 uisetup(ui)
330 328 reposetup(ui, repo)
331 329 for k, v in ui.configitems('extensions'):
332 330 if k.endswith('keyword'):
333 331 extension = '%s = %s' % (k, v)
334 332 break
335 333 ui.write('[extensions]\n%s\n' % extension)
336 334 demoitems('keyword', ui.configitems('keyword'))
337 335 demoitems('keywordmaps', kwmaps.iteritems())
338 336 keywords = '$' + '$\n$'.join(sorted(kwmaps.keys())) + '$\n'
339 337 repo.wopener(fn, 'w').write(keywords)
340 338 repo.add([fn])
341 339 path = repo.wjoin(fn)
342 340 ui.note(_('\nkeywords written to %s:\n') % path)
343 341 ui.note(keywords)
344 342 ui.note('\nhg -R "%s" branch "%s"\n' % (tmpdir, branchname))
345 343 # silence branch command if not verbose
346 344 quiet = ui.quiet
347 345 ui.quiet = not ui.verbose
348 346 commands.branch(ui, repo, branchname)
349 347 ui.quiet = quiet
350 348 for name, cmd in ui.configitems('hooks'):
351 349 if name.split('.', 1)[0].find('commit') > -1:
352 350 repo.ui.setconfig('hooks', name, '')
353 351 ui.note(_('unhooked all commit hooks\n'))
354 352 msg = _('hg keyword configuration and expansion example')
355 353 ui.note("hg -R '%s' ci -m '%s'\n" % (tmpdir, msg))
356 354 repo.commit(text=msg)
357 355 ui.status(_('\n\tkeywords expanded\n'))
358 356 ui.write(repo.wread(fn))
359 357 ui.debug('\nremoving temporary repository %s\n' % tmpdir)
360 358 shutil.rmtree(tmpdir, ignore_errors=True)
361 359
362 360 def expand(ui, repo, *pats, **opts):
363 361 '''expand keywords in the working directory
364 362
365 363 Run after (re)enabling keyword expansion.
366 364
367 365 kwexpand refuses to run if given files contain local changes.
368 366 '''
369 367 # 3rd argument sets expansion to True
370 368 _kwfwrite(ui, repo, True, *pats, **opts)
371 369
372 370 def files(ui, repo, *pats, **opts):
373 371 '''show files configured for keyword expansion
374 372
375 373 List which files in the working directory are matched by the
376 374 [keyword] configuration patterns.
377 375
378 376 Useful to prevent inadvertent keyword expansion and to speed up
379 377 execution by including only files that are actual candidates for
380 378 expansion.
381 379
382 380 See "hg help keyword" on how to construct patterns both for
383 381 inclusion and exclusion of files.
384 382
385 383 With -A/--all and -v/--verbose the codes used to show the status
386 384 of files are::
387 385
388 386 K = keyword expansion candidate
389 387 k = keyword expansion candidate (not tracked)
390 388 I = ignored
391 389 i = ignored (not tracked)
392 390 '''
393 391 kwt = kwtools['templater']
394 392 status = _status(ui, repo, kwt, *pats, **opts)
395 393 cwd = pats and repo.getcwd() or ''
396 394 modified, added, removed, deleted, unknown, ignored, clean = status
397 395 files = []
398 if not (opts.get('unknown') or opts.get('untracked')) or opts.get('all'):
396 if not opts.get('unknown') or opts.get('all'):
399 397 files = sorted(modified + added + clean)
400 398 wctx = repo[None]
401 399 kwfiles = [f for f in files if kwt.iskwfile(f, wctx.flags)]
402 400 kwunknown = [f for f in unknown if kwt.iskwfile(f, wctx.flags)]
403 401 if not opts.get('ignore') or opts.get('all'):
404 402 showfiles = kwfiles, kwunknown
405 403 else:
406 404 showfiles = [], []
407 405 if opts.get('all') or opts.get('ignore'):
408 406 showfiles += ([f for f in files if f not in kwfiles],
409 407 [f for f in unknown if f not in kwunknown])
410 408 for char, filenames in zip('KkIi', showfiles):
411 409 fmt = (opts.get('all') or ui.verbose) and '%s %%s\n' % char or '%s\n'
412 410 for f in filenames:
413 411 ui.write(fmt % repo.pathto(f, cwd))
414 412
415 413 def shrink(ui, repo, *pats, **opts):
416 414 '''revert expanded keywords in the working directory
417 415
418 416 Run before changing/disabling active keywords or if you experience
419 417 problems with "hg import" or "hg merge".
420 418
421 419 kwshrink refuses to run if given files contain local changes.
422 420 '''
423 421 # 3rd argument sets expansion to False
424 422 _kwfwrite(ui, repo, False, *pats, **opts)
425 423
426 424
427 425 def uisetup(ui):
428 426 '''Collects [keyword] config in kwtools.
429 427 Monkeypatches dispatch._parse if needed.'''
430 428
431 429 for pat, opt in ui.configitems('keyword'):
432 430 if opt != 'ignore':
433 431 kwtools['inc'].append(pat)
434 432 else:
435 433 kwtools['exc'].append(pat)
436 434
437 435 if kwtools['inc']:
438 436 def kwdispatch_parse(orig, ui, args):
439 437 '''Monkeypatch dispatch._parse to obtain running hg command.'''
440 438 cmd, func, args, options, cmdoptions = orig(ui, args)
441 439 kwtools['hgcmd'] = cmd
442 440 return cmd, func, args, options, cmdoptions
443 441
444 442 extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse)
445 443
446 444 def reposetup(ui, repo):
447 445 '''Sets up repo as kwrepo for keyword substitution.
448 446 Overrides file method to return kwfilelog instead of filelog
449 447 if file matches user configuration.
450 448 Wraps commit to overwrite configured files with updated
451 449 keyword substitutions.
452 450 Monkeypatches patch and webcommands.'''
453 451
454 452 try:
455 453 if (not repo.local() or not kwtools['inc']
456 454 or kwtools['hgcmd'] in nokwcommands.split()
457 455 or '.hg' in util.splitpath(repo.root)
458 456 or repo._url.startswith('bundle:')):
459 457 return
460 458 except AttributeError:
461 459 pass
462 460
463 461 kwtools['templater'] = kwt = kwtemplater(ui, repo)
464 462
465 463 class kwrepo(repo.__class__):
466 464 def file(self, f):
467 465 if f[0] == '/':
468 466 f = f[1:]
469 467 return kwfilelog(self.sopener, kwt, f)
470 468
471 469 def wread(self, filename):
472 470 data = super(kwrepo, self).wread(filename)
473 471 return kwt.wread(filename, data)
474 472
475 473 def commit(self, *args, **opts):
476 474 # use custom commitctx for user commands
477 475 # other extensions can still wrap repo.commitctx directly
478 476 self.commitctx = self.kwcommitctx
479 477 try:
480 478 return super(kwrepo, self).commit(*args, **opts)
481 479 finally:
482 480 del self.commitctx
483 481
484 482 def kwcommitctx(self, ctx, error=False):
485 483 n = super(kwrepo, self).commitctx(ctx, error)
486 484 # no lock needed, only called from repo.commit() which already locks
487 485 kwt.overwrite(n, True, None)
488 486 return n
489 487
490 488 # monkeypatches
491 489 def kwpatchfile_init(orig, self, ui, fname, opener,
492 490 missing=False, eol=None):
493 491 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
494 492 rejects or conflicts due to expanded keywords in working dir.'''
495 493 orig(self, ui, fname, opener, missing, eol)
496 494 # shrink keywords read from working dir
497 495 self.lines = kwt.shrinklines(self.fname, self.lines)
498 496
499 497 def kw_diff(orig, repo, node1=None, node2=None, match=None, changes=None,
500 498 opts=None):
501 499 '''Monkeypatch patch.diff to avoid expansion except when
502 500 comparing against working dir.'''
503 501 if node2 is not None:
504 502 kwt.match = util.never
505 503 elif node1 is not None and node1 != repo['.'].node():
506 504 kwt.restrict = True
507 505 return orig(repo, node1, node2, match, changes, opts)
508 506
509 507 def kwweb_skip(orig, web, req, tmpl):
510 508 '''Wraps webcommands.x turning off keyword expansion.'''
511 509 kwt.match = util.never
512 510 return orig(web, req, tmpl)
513 511
514 512 repo.__class__ = kwrepo
515 513
516 514 extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init)
517 515 if not kwt.restrict:
518 516 extensions.wrapfunction(patch, 'diff', kw_diff)
519 517 for c in 'annotate changeset rev filediff diff'.split():
520 518 extensions.wrapfunction(webcommands, c, kwweb_skip)
521 519
522 520 cmdtable = {
523 521 'kwdemo':
524 522 (demo,
525 523 [('d', 'default', None, _('show default keyword template maps')),
526 524 ('f', 'rcfile', '', _('read maps from rcfile'))],
527 525 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')),
528 526 'kwexpand': (expand, commands.walkopts,
529 527 _('hg kwexpand [OPTION]... [FILE]...')),
530 528 'kwfiles':
531 529 (files,
532 530 [('A', 'all', None, _('show keyword status flags of all files')),
533 531 ('i', 'ignore', None, _('show files excluded from expansion')),
534 532 ('u', 'unknown', None, _('only show unknown (not tracked) files')),
535 ('a', 'all', None,
536 _('show keyword status flags of all files (DEPRECATED)')),
537 ('u', 'untracked', None, _('only show untracked files (DEPRECATED)')),
538 533 ] + commands.walkopts,
539 534 _('hg kwfiles [OPTION]... [FILE]...')),
540 535 'kwshrink': (shrink, commands.walkopts,
541 536 _('hg kwshrink [OPTION]... [FILE]...')),
542 537 }
General Comments 0
You need to be logged in to leave comments. Login now