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