##// END OF EJS Templates
templates: document missing keywords or filters...
Patrick Mezard -
r13592:ad2ee188 default
parent child Browse files
Show More
@@ -1,682 +1,685
1 1 # keyword.py - $Keyword$ expansion for Mercurial
2 2 #
3 3 # Copyright 2007-2010 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 Keywords expand to the changeset data pertaining to the latest change
39 39 relative to the working directory parent of each file.
40 40
41 41 Configuration is done in the [keyword], [keywordset] and [keywordmaps]
42 42 sections of hgrc files.
43 43
44 44 Example::
45 45
46 46 [keyword]
47 47 # expand keywords in every python file except those matching "x*"
48 48 **.py =
49 49 x* = ignore
50 50
51 51 [keywordset]
52 52 # prefer svn- over cvs-like default keywordmaps
53 53 svn = True
54 54
55 55 .. note::
56 56 The more specific you are in your filename patterns the less you
57 57 lose speed in huge repositories.
58 58
59 59 For [keywordmaps] template mapping and expansion demonstration and
60 60 control run :hg:`kwdemo`. See :hg:`help templates` for a list of
61 61 available templates and filters.
62 62
63 63 Three additional date template filters are provided::
64 64
65 65 utcdate "2006/09/18 15:13:13"
66 66 svnutcdate "2006-09-18 15:13:13Z"
67 67 svnisodate "2006-09-18 08:13:13 -700 (Mon, 18 Sep 2006)"
68 68
69 69 The default template mappings (view with :hg:`kwdemo -d`) can be
70 70 replaced with customized keywords and templates. Again, run
71 71 :hg:`kwdemo` to control the results of your configuration changes.
72 72
73 73 Before changing/disabling active keywords, you must run :hg:`kwshrink`
74 74 to avoid storing expanded keywords in the change history.
75 75
76 76 To force expansion after enabling it, or a configuration change, run
77 77 :hg:`kwexpand`.
78 78
79 79 Expansions spanning more than one line and incremental expansions,
80 80 like CVS' $Log$, are not supported. A keyword template map "Log =
81 81 {desc}" expands to the first line of the changeset description.
82 82 '''
83 83
84 84 from mercurial import commands, context, cmdutil, dispatch, filelog, extensions
85 85 from mercurial import localrepo, match, patch, templatefilters, templater, util
86 86 from mercurial.hgweb import webcommands
87 87 from mercurial.i18n import _
88 88 import os, re, shutil, tempfile
89 89
90 90 commands.optionalrepo += ' kwdemo'
91 91
92 92 # hg commands that do not act on keywords
93 93 nokwcommands = ('add addremove annotate bundle export grep incoming init log'
94 94 ' outgoing push tip verify convert email glog')
95 95
96 96 # hg commands that trigger expansion only when writing to working dir,
97 97 # not when reading filelog, and unexpand when reading from working dir
98 98 restricted = 'merge kwexpand kwshrink record qrecord resolve transplant'
99 99
100 100 # names of extensions using dorecord
101 101 recordextensions = 'record'
102 102
103 103 colortable = {
104 104 'kwfiles.enabled': 'green bold',
105 105 'kwfiles.deleted': 'cyan bold underline',
106 106 'kwfiles.enabledunknown': 'green',
107 107 'kwfiles.ignored': 'bold',
108 108 'kwfiles.ignoredunknown': 'none'
109 109 }
110 110
111 111 # date like in cvs' $Date
112 utcdate = lambda x: util.datestr((x[0], 0), '%Y/%m/%d %H:%M:%S')
112 def utcdate(text):
113 return util.datestr((text[0], 0), '%Y/%m/%d %H:%M:%S')
113 114 # date like in svn's $Date
114 svnisodate = lambda x: util.datestr(x, '%Y-%m-%d %H:%M:%S %1%2 (%a, %d %b %Y)')
115 def svnisodate(text):
116 return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2 (%a, %d %b %Y)')
115 117 # date like in svn's $Id
116 svnutcdate = lambda x: util.datestr((x[0], 0), '%Y-%m-%d %H:%M:%SZ')
118 def svnutcdate(text):
119 return util.datestr((text[0], 0), '%Y-%m-%d %H:%M:%SZ')
117 120
118 121 # make keyword tools accessible
119 122 kwtools = {'templater': None, 'hgcmd': ''}
120 123
121 124 def _defaultkwmaps(ui):
122 125 '''Returns default keywordmaps according to keywordset configuration.'''
123 126 templates = {
124 127 'Revision': '{node|short}',
125 128 'Author': '{author|user}',
126 129 }
127 130 kwsets = ({
128 131 'Date': '{date|utcdate}',
129 132 'RCSfile': '{file|basename},v',
130 133 'RCSFile': '{file|basename},v', # kept for backwards compatibility
131 134 # with hg-keyword
132 135 'Source': '{root}/{file},v',
133 136 'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
134 137 'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
135 138 }, {
136 139 'Date': '{date|svnisodate}',
137 140 'Id': '{file|basename},v {node|short} {date|svnutcdate} {author|user}',
138 141 'LastChangedRevision': '{node|short}',
139 142 'LastChangedBy': '{author|user}',
140 143 'LastChangedDate': '{date|svnisodate}',
141 144 })
142 145 templates.update(kwsets[ui.configbool('keywordset', 'svn')])
143 146 return templates
144 147
145 148 def _shrinktext(text, subfunc):
146 149 '''Helper for keyword expansion removal in text.
147 150 Depending on subfunc also returns number of substitutions.'''
148 151 return subfunc(r'$\1$', text)
149 152
150 153 def _preselect(wstatus, changed):
151 154 '''Retrieves modfied and added files from a working directory state
152 155 and returns the subset of each contained in given changed files
153 156 retrieved from a change context.'''
154 157 modified, added = wstatus[:2]
155 158 modified = [f for f in modified if f in changed]
156 159 added = [f for f in added if f in changed]
157 160 return modified, added
158 161
159 162
160 163 class kwtemplater(object):
161 164 '''
162 165 Sets up keyword templates, corresponding keyword regex, and
163 166 provides keyword substitution functions.
164 167 '''
165 168
166 169 def __init__(self, ui, repo, inc, exc):
167 170 self.ui = ui
168 171 self.repo = repo
169 172 self.match = match.match(repo.root, '', [], inc, exc)
170 173 self.restrict = kwtools['hgcmd'] in restricted.split()
171 174 self.record = False
172 175
173 176 kwmaps = self.ui.configitems('keywordmaps')
174 177 if kwmaps: # override default templates
175 178 self.templates = dict((k, templater.parsestring(v, False))
176 179 for k, v in kwmaps)
177 180 else:
178 181 self.templates = _defaultkwmaps(self.ui)
179 182 templatefilters.filters.update({'utcdate': utcdate,
180 183 'svnisodate': svnisodate,
181 184 'svnutcdate': svnutcdate})
182 185
183 186 @util.propertycache
184 187 def escape(self):
185 188 '''Returns bar-separated and escaped keywords.'''
186 189 return '|'.join(map(re.escape, self.templates.keys()))
187 190
188 191 @util.propertycache
189 192 def rekw(self):
190 193 '''Returns regex for unexpanded keywords.'''
191 194 return re.compile(r'\$(%s)\$' % self.escape)
192 195
193 196 @util.propertycache
194 197 def rekwexp(self):
195 198 '''Returns regex for expanded keywords.'''
196 199 return re.compile(r'\$(%s): [^$\n\r]*? \$' % self.escape)
197 200
198 201 def substitute(self, data, path, ctx, subfunc):
199 202 '''Replaces keywords in data with expanded template.'''
200 203 def kwsub(mobj):
201 204 kw = mobj.group(1)
202 205 ct = cmdutil.changeset_templater(self.ui, self.repo,
203 206 False, None, '', False)
204 207 ct.use_template(self.templates[kw])
205 208 self.ui.pushbuffer()
206 209 ct.show(ctx, root=self.repo.root, file=path)
207 210 ekw = templatefilters.firstline(self.ui.popbuffer())
208 211 return '$%s: %s $' % (kw, ekw)
209 212 return subfunc(kwsub, data)
210 213
211 214 def linkctx(self, path, fileid):
212 215 '''Similar to filelog.linkrev, but returns a changectx.'''
213 216 return self.repo.filectx(path, fileid=fileid).changectx()
214 217
215 218 def expand(self, path, node, data):
216 219 '''Returns data with keywords expanded.'''
217 220 if not self.restrict and self.match(path) and not util.binary(data):
218 221 ctx = self.linkctx(path, node)
219 222 return self.substitute(data, path, ctx, self.rekw.sub)
220 223 return data
221 224
222 225 def iskwfile(self, cand, ctx):
223 226 '''Returns subset of candidates which are configured for keyword
224 227 expansion are not symbolic links.'''
225 228 return [f for f in cand if self.match(f) and not 'l' in ctx.flags(f)]
226 229
227 230 def overwrite(self, ctx, candidates, lookup, expand, rekw=False):
228 231 '''Overwrites selected files expanding/shrinking keywords.'''
229 232 if self.restrict or lookup or self.record: # exclude kw_copy
230 233 candidates = self.iskwfile(candidates, ctx)
231 234 if not candidates:
232 235 return
233 236 kwcmd = self.restrict and lookup # kwexpand/kwshrink
234 237 if self.restrict or expand and lookup:
235 238 mf = ctx.manifest()
236 239 lctx = ctx
237 240 re_kw = (self.restrict or rekw) and self.rekw or self.rekwexp
238 241 msg = (expand and _('overwriting %s expanding keywords\n')
239 242 or _('overwriting %s shrinking keywords\n'))
240 243 for f in candidates:
241 244 if self.restrict:
242 245 data = self.repo.file(f).read(mf[f])
243 246 else:
244 247 data = self.repo.wread(f)
245 248 if util.binary(data):
246 249 continue
247 250 if expand:
248 251 if lookup:
249 252 lctx = self.linkctx(f, mf[f])
250 253 data, found = self.substitute(data, f, lctx, re_kw.subn)
251 254 elif self.restrict:
252 255 found = re_kw.search(data)
253 256 else:
254 257 data, found = _shrinktext(data, re_kw.subn)
255 258 if found:
256 259 self.ui.note(msg % f)
257 260 self.repo.wwrite(f, data, ctx.flags(f))
258 261 if kwcmd:
259 262 self.repo.dirstate.normal(f)
260 263 elif self.record:
261 264 self.repo.dirstate.normallookup(f)
262 265
263 266 def shrink(self, fname, text):
264 267 '''Returns text with all keyword substitutions removed.'''
265 268 if self.match(fname) and not util.binary(text):
266 269 return _shrinktext(text, self.rekwexp.sub)
267 270 return text
268 271
269 272 def shrinklines(self, fname, lines):
270 273 '''Returns lines with keyword substitutions removed.'''
271 274 if self.match(fname):
272 275 text = ''.join(lines)
273 276 if not util.binary(text):
274 277 return _shrinktext(text, self.rekwexp.sub).splitlines(True)
275 278 return lines
276 279
277 280 def wread(self, fname, data):
278 281 '''If in restricted mode returns data read from wdir with
279 282 keyword substitutions removed.'''
280 283 return self.restrict and self.shrink(fname, data) or data
281 284
282 285 class kwfilelog(filelog.filelog):
283 286 '''
284 287 Subclass of filelog to hook into its read, add, cmp methods.
285 288 Keywords are "stored" unexpanded, and processed on reading.
286 289 '''
287 290 def __init__(self, opener, kwt, path):
288 291 super(kwfilelog, self).__init__(opener, path)
289 292 self.kwt = kwt
290 293 self.path = path
291 294
292 295 def read(self, node):
293 296 '''Expands keywords when reading filelog.'''
294 297 data = super(kwfilelog, self).read(node)
295 298 if self.renamed(node):
296 299 return data
297 300 return self.kwt.expand(self.path, node, data)
298 301
299 302 def add(self, text, meta, tr, link, p1=None, p2=None):
300 303 '''Removes keyword substitutions when adding to filelog.'''
301 304 text = self.kwt.shrink(self.path, text)
302 305 return super(kwfilelog, self).add(text, meta, tr, link, p1, p2)
303 306
304 307 def cmp(self, node, text):
305 308 '''Removes keyword substitutions for comparison.'''
306 309 text = self.kwt.shrink(self.path, text)
307 310 return super(kwfilelog, self).cmp(node, text)
308 311
309 312 def _status(ui, repo, kwt, *pats, **opts):
310 313 '''Bails out if [keyword] configuration is not active.
311 314 Returns status of working directory.'''
312 315 if kwt:
313 316 return repo.status(match=cmdutil.match(repo, pats, opts), clean=True,
314 317 unknown=opts.get('unknown') or opts.get('all'))
315 318 if ui.configitems('keyword'):
316 319 raise util.Abort(_('[keyword] patterns cannot match'))
317 320 raise util.Abort(_('no [keyword] patterns configured'))
318 321
319 322 def _kwfwrite(ui, repo, expand, *pats, **opts):
320 323 '''Selects files and passes them to kwtemplater.overwrite.'''
321 324 wctx = repo[None]
322 325 if len(wctx.parents()) > 1:
323 326 raise util.Abort(_('outstanding uncommitted merge'))
324 327 kwt = kwtools['templater']
325 328 wlock = repo.wlock()
326 329 try:
327 330 status = _status(ui, repo, kwt, *pats, **opts)
328 331 modified, added, removed, deleted, unknown, ignored, clean = status
329 332 if modified or added or removed or deleted:
330 333 raise util.Abort(_('outstanding uncommitted changes'))
331 334 kwt.overwrite(wctx, clean, True, expand)
332 335 finally:
333 336 wlock.release()
334 337
335 338 def demo(ui, repo, *args, **opts):
336 339 '''print [keywordmaps] configuration and an expansion example
337 340
338 341 Show current, custom, or default keyword template maps and their
339 342 expansions.
340 343
341 344 Extend the current configuration by specifying maps as arguments
342 345 and using -f/--rcfile to source an external hgrc file.
343 346
344 347 Use -d/--default to disable current configuration.
345 348
346 349 See :hg:`help templates` for information on templates and filters.
347 350 '''
348 351 def demoitems(section, items):
349 352 ui.write('[%s]\n' % section)
350 353 for k, v in sorted(items):
351 354 ui.write('%s = %s\n' % (k, v))
352 355
353 356 fn = 'demo.txt'
354 357 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
355 358 ui.note(_('creating temporary repository at %s\n') % tmpdir)
356 359 repo = localrepo.localrepository(ui, tmpdir, True)
357 360 ui.setconfig('keyword', fn, '')
358 361 svn = ui.configbool('keywordset', 'svn')
359 362 # explicitly set keywordset for demo output
360 363 ui.setconfig('keywordset', 'svn', svn)
361 364
362 365 uikwmaps = ui.configitems('keywordmaps')
363 366 if args or opts.get('rcfile'):
364 367 ui.status(_('\n\tconfiguration using custom keyword template maps\n'))
365 368 if uikwmaps:
366 369 ui.status(_('\textending current template maps\n'))
367 370 if opts.get('default') or not uikwmaps:
368 371 if svn:
369 372 ui.status(_('\toverriding default svn keywordset\n'))
370 373 else:
371 374 ui.status(_('\toverriding default cvs keywordset\n'))
372 375 if opts.get('rcfile'):
373 376 ui.readconfig(opts.get('rcfile'))
374 377 if args:
375 378 # simulate hgrc parsing
376 379 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
377 380 fp = repo.opener('hgrc', 'w')
378 381 fp.writelines(rcmaps)
379 382 fp.close()
380 383 ui.readconfig(repo.join('hgrc'))
381 384 kwmaps = dict(ui.configitems('keywordmaps'))
382 385 elif opts.get('default'):
383 386 if svn:
384 387 ui.status(_('\n\tconfiguration using default svn keywordset\n'))
385 388 else:
386 389 ui.status(_('\n\tconfiguration using default cvs keywordset\n'))
387 390 kwmaps = _defaultkwmaps(ui)
388 391 if uikwmaps:
389 392 ui.status(_('\tdisabling current template maps\n'))
390 393 for k, v in kwmaps.iteritems():
391 394 ui.setconfig('keywordmaps', k, v)
392 395 else:
393 396 ui.status(_('\n\tconfiguration using current keyword template maps\n'))
394 397 kwmaps = dict(uikwmaps) or _defaultkwmaps(ui)
395 398
396 399 uisetup(ui)
397 400 reposetup(ui, repo)
398 401 ui.write('[extensions]\nkeyword =\n')
399 402 demoitems('keyword', ui.configitems('keyword'))
400 403 demoitems('keywordset', ui.configitems('keywordset'))
401 404 demoitems('keywordmaps', kwmaps.iteritems())
402 405 keywords = '$' + '$\n$'.join(sorted(kwmaps.keys())) + '$\n'
403 406 repo.wopener(fn, 'w').write(keywords)
404 407 repo[None].add([fn])
405 408 ui.note(_('\nkeywords written to %s:\n') % fn)
406 409 ui.note(keywords)
407 410 repo.dirstate.setbranch('demobranch')
408 411 for name, cmd in ui.configitems('hooks'):
409 412 if name.split('.', 1)[0].find('commit') > -1:
410 413 repo.ui.setconfig('hooks', name, '')
411 414 msg = _('hg keyword configuration and expansion example')
412 415 ui.note("hg ci -m '%s'\n" % msg)
413 416 repo.commit(text=msg)
414 417 ui.status(_('\n\tkeywords expanded\n'))
415 418 ui.write(repo.wread(fn))
416 419 shutil.rmtree(tmpdir, ignore_errors=True)
417 420
418 421 def expand(ui, repo, *pats, **opts):
419 422 '''expand keywords in the working directory
420 423
421 424 Run after (re)enabling keyword expansion.
422 425
423 426 kwexpand refuses to run if given files contain local changes.
424 427 '''
425 428 # 3rd argument sets expansion to True
426 429 _kwfwrite(ui, repo, True, *pats, **opts)
427 430
428 431 def files(ui, repo, *pats, **opts):
429 432 '''show files configured for keyword expansion
430 433
431 434 List which files in the working directory are matched by the
432 435 [keyword] configuration patterns.
433 436
434 437 Useful to prevent inadvertent keyword expansion and to speed up
435 438 execution by including only files that are actual candidates for
436 439 expansion.
437 440
438 441 See :hg:`help keyword` on how to construct patterns both for
439 442 inclusion and exclusion of files.
440 443
441 444 With -A/--all and -v/--verbose the codes used to show the status
442 445 of files are::
443 446
444 447 K = keyword expansion candidate
445 448 k = keyword expansion candidate (not tracked)
446 449 I = ignored
447 450 i = ignored (not tracked)
448 451 '''
449 452 kwt = kwtools['templater']
450 453 status = _status(ui, repo, kwt, *pats, **opts)
451 454 cwd = pats and repo.getcwd() or ''
452 455 modified, added, removed, deleted, unknown, ignored, clean = status
453 456 files = []
454 457 if not opts.get('unknown') or opts.get('all'):
455 458 files = sorted(modified + added + clean)
456 459 wctx = repo[None]
457 460 kwfiles = kwt.iskwfile(files, wctx)
458 461 kwdeleted = kwt.iskwfile(deleted, wctx)
459 462 kwunknown = kwt.iskwfile(unknown, wctx)
460 463 if not opts.get('ignore') or opts.get('all'):
461 464 showfiles = kwfiles, kwdeleted, kwunknown
462 465 else:
463 466 showfiles = [], [], []
464 467 if opts.get('all') or opts.get('ignore'):
465 468 showfiles += ([f for f in files if f not in kwfiles],
466 469 [f for f in unknown if f not in kwunknown])
467 470 kwlabels = 'enabled deleted enabledunknown ignored ignoredunknown'.split()
468 471 kwstates = zip('K!kIi', showfiles, kwlabels)
469 472 for char, filenames, kwstate in kwstates:
470 473 fmt = (opts.get('all') or ui.verbose) and '%s %%s\n' % char or '%s\n'
471 474 for f in filenames:
472 475 ui.write(fmt % repo.pathto(f, cwd), label='kwfiles.' + kwstate)
473 476
474 477 def shrink(ui, repo, *pats, **opts):
475 478 '''revert expanded keywords in the working directory
476 479
477 480 Must be run before changing/disabling active keywords.
478 481
479 482 kwshrink refuses to run if given files contain local changes.
480 483 '''
481 484 # 3rd argument sets expansion to False
482 485 _kwfwrite(ui, repo, False, *pats, **opts)
483 486
484 487
485 488 def uisetup(ui):
486 489 ''' Monkeypatches dispatch._parse to retrieve user command.'''
487 490
488 491 def kwdispatch_parse(orig, ui, args):
489 492 '''Monkeypatch dispatch._parse to obtain running hg command.'''
490 493 cmd, func, args, options, cmdoptions = orig(ui, args)
491 494 kwtools['hgcmd'] = cmd
492 495 return cmd, func, args, options, cmdoptions
493 496
494 497 extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse)
495 498
496 499 def reposetup(ui, repo):
497 500 '''Sets up repo as kwrepo for keyword substitution.
498 501 Overrides file method to return kwfilelog instead of filelog
499 502 if file matches user configuration.
500 503 Wraps commit to overwrite configured files with updated
501 504 keyword substitutions.
502 505 Monkeypatches patch and webcommands.'''
503 506
504 507 try:
505 508 if (not repo.local() or kwtools['hgcmd'] in nokwcommands.split()
506 509 or '.hg' in util.splitpath(repo.root)
507 510 or repo._url.startswith('bundle:')):
508 511 return
509 512 except AttributeError:
510 513 pass
511 514
512 515 inc, exc = [], ['.hg*']
513 516 for pat, opt in ui.configitems('keyword'):
514 517 if opt != 'ignore':
515 518 inc.append(pat)
516 519 else:
517 520 exc.append(pat)
518 521 if not inc:
519 522 return
520 523
521 524 kwtools['templater'] = kwt = kwtemplater(ui, repo, inc, exc)
522 525
523 526 class kwrepo(repo.__class__):
524 527 def file(self, f):
525 528 if f[0] == '/':
526 529 f = f[1:]
527 530 return kwfilelog(self.sopener, kwt, f)
528 531
529 532 def wread(self, filename):
530 533 data = super(kwrepo, self).wread(filename)
531 534 return kwt.wread(filename, data)
532 535
533 536 def commit(self, *args, **opts):
534 537 # use custom commitctx for user commands
535 538 # other extensions can still wrap repo.commitctx directly
536 539 self.commitctx = self.kwcommitctx
537 540 try:
538 541 return super(kwrepo, self).commit(*args, **opts)
539 542 finally:
540 543 del self.commitctx
541 544
542 545 def kwcommitctx(self, ctx, error=False):
543 546 n = super(kwrepo, self).commitctx(ctx, error)
544 547 # no lock needed, only called from repo.commit() which already locks
545 548 if not kwt.record:
546 549 restrict = kwt.restrict
547 550 kwt.restrict = True
548 551 kwt.overwrite(self[n], sorted(ctx.added() + ctx.modified()),
549 552 False, True)
550 553 kwt.restrict = restrict
551 554 return n
552 555
553 556 def rollback(self, dryrun=False):
554 557 wlock = self.wlock()
555 558 try:
556 559 if not dryrun:
557 560 changed = self['.'].files()
558 561 ret = super(kwrepo, self).rollback(dryrun)
559 562 if not dryrun:
560 563 ctx = self['.']
561 564 modified, added = _preselect(self[None].status(), changed)
562 565 kwt.overwrite(ctx, modified, True, True)
563 566 kwt.overwrite(ctx, added, True, False)
564 567 return ret
565 568 finally:
566 569 wlock.release()
567 570
568 571 # monkeypatches
569 572 def kwpatchfile_init(orig, self, ui, fname, opener,
570 573 missing=False, eolmode=None):
571 574 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
572 575 rejects or conflicts due to expanded keywords in working dir.'''
573 576 orig(self, ui, fname, opener, missing, eolmode)
574 577 # shrink keywords read from working dir
575 578 self.lines = kwt.shrinklines(self.fname, self.lines)
576 579
577 580 def kw_diff(orig, repo, node1=None, node2=None, match=None, changes=None,
578 581 opts=None, prefix=''):
579 582 '''Monkeypatch patch.diff to avoid expansion.'''
580 583 kwt.restrict = True
581 584 return orig(repo, node1, node2, match, changes, opts, prefix)
582 585
583 586 def kwweb_skip(orig, web, req, tmpl):
584 587 '''Wraps webcommands.x turning off keyword expansion.'''
585 588 kwt.match = util.never
586 589 return orig(web, req, tmpl)
587 590
588 591 def kw_copy(orig, ui, repo, pats, opts, rename=False):
589 592 '''Wraps cmdutil.copy so that copy/rename destinations do not
590 593 contain expanded keywords.
591 594 Note that the source of a regular file destination may also be a
592 595 symlink:
593 596 hg cp sym x -> x is symlink
594 597 cp sym x; hg cp -A sym x -> x is file (maybe expanded keywords)
595 598 For the latter we have to follow the symlink to find out whether its
596 599 target is configured for expansion and we therefore must unexpand the
597 600 keywords in the destination.'''
598 601 orig(ui, repo, pats, opts, rename)
599 602 if opts.get('dry_run'):
600 603 return
601 604 wctx = repo[None]
602 605 cwd = repo.getcwd()
603 606
604 607 def haskwsource(dest):
605 608 '''Returns true if dest is a regular file and configured for
606 609 expansion or a symlink which points to a file configured for
607 610 expansion. '''
608 611 source = repo.dirstate.copied(dest)
609 612 if 'l' in wctx.flags(source):
610 613 source = util.canonpath(repo.root, cwd,
611 614 os.path.realpath(source))
612 615 return kwt.match(source)
613 616
614 617 candidates = [f for f in repo.dirstate.copies() if
615 618 not 'l' in wctx.flags(f) and haskwsource(f)]
616 619 kwt.overwrite(wctx, candidates, False, False)
617 620
618 621 def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts):
619 622 '''Wraps record.dorecord expanding keywords after recording.'''
620 623 wlock = repo.wlock()
621 624 try:
622 625 # record returns 0 even when nothing has changed
623 626 # therefore compare nodes before and after
624 627 kwt.record = True
625 628 ctx = repo['.']
626 629 wstatus = repo[None].status()
627 630 ret = orig(ui, repo, commitfunc, *pats, **opts)
628 631 recctx = repo['.']
629 632 if ctx != recctx:
630 633 modified, added = _preselect(wstatus, recctx.files())
631 634 kwt.restrict = False
632 635 kwt.overwrite(recctx, modified, False, True)
633 636 kwt.overwrite(recctx, added, False, True, True)
634 637 kwt.restrict = True
635 638 return ret
636 639 finally:
637 640 wlock.release()
638 641
639 642 def kwfilectx_cmp(orig, self, fctx):
640 643 # keyword affects data size, comparing wdir and filelog size does
641 644 # not make sense
642 645 if (fctx._filerev is None and
643 646 (self._repo._encodefilterpats or
644 647 kwt.match(fctx.path()) and not 'l' in fctx.flags()) or
645 648 self.size() == fctx.size()):
646 649 return self._filelog.cmp(self._filenode, fctx.data())
647 650 return True
648 651
649 652 extensions.wrapfunction(context.filectx, 'cmp', kwfilectx_cmp)
650 653 extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init)
651 654 extensions.wrapfunction(patch, 'diff', kw_diff)
652 655 extensions.wrapfunction(cmdutil, 'copy', kw_copy)
653 656 for c in 'annotate changeset rev filediff diff'.split():
654 657 extensions.wrapfunction(webcommands, c, kwweb_skip)
655 658 for name in recordextensions.split():
656 659 try:
657 660 record = extensions.find(name)
658 661 extensions.wrapfunction(record, 'dorecord', kw_dorecord)
659 662 except KeyError:
660 663 pass
661 664
662 665 repo.__class__ = kwrepo
663 666
664 667 cmdtable = {
665 668 'kwdemo':
666 669 (demo,
667 670 [('d', 'default', None, _('show default keyword template maps')),
668 671 ('f', 'rcfile', '',
669 672 _('read maps from rcfile'), _('FILE'))],
670 673 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')),
671 674 'kwexpand': (expand, commands.walkopts,
672 675 _('hg kwexpand [OPTION]... [FILE]...')),
673 676 'kwfiles':
674 677 (files,
675 678 [('A', 'all', None, _('show keyword status flags of all files')),
676 679 ('i', 'ignore', None, _('show files excluded from expansion')),
677 680 ('u', 'unknown', None, _('only show unknown (not tracked) files')),
678 681 ] + commands.walkopts,
679 682 _('hg kwfiles [OPTION]... [FILE]...')),
680 683 'kwshrink': (shrink, commands.walkopts,
681 684 _('hg kwshrink [OPTION]... [FILE]...')),
682 685 }
@@ -1,331 +1,334
1 1 # templatekw.py - common changeset template keywords
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com>
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 from node import hex
9 9 import encoding, patch, util, error
10 10 from i18n import gettext
11 11
12 12 def showlist(name, values, plural=None, **args):
13 13 '''expand set of values.
14 14 name is name of key in template map.
15 15 values is list of strings or dicts.
16 16 plural is plural of name, if not simply name + 's'.
17 17
18 18 expansion works like this, given name 'foo'.
19 19
20 20 if values is empty, expand 'no_foos'.
21 21
22 22 if 'foo' not in template map, return values as a string,
23 23 joined by space.
24 24
25 25 expand 'start_foos'.
26 26
27 27 for each value, expand 'foo'. if 'last_foo' in template
28 28 map, expand it instead of 'foo' for last key.
29 29
30 30 expand 'end_foos'.
31 31 '''
32 32 templ = args['templ']
33 33 if plural:
34 34 names = plural
35 35 else: names = name + 's'
36 36 if not values:
37 37 noname = 'no_' + names
38 38 if noname in templ:
39 39 yield templ(noname, **args)
40 40 return
41 41 if name not in templ:
42 42 if isinstance(values[0], str):
43 43 yield ' '.join(values)
44 44 else:
45 45 for v in values:
46 46 yield dict(v, **args)
47 47 return
48 48 startname = 'start_' + names
49 49 if startname in templ:
50 50 yield templ(startname, **args)
51 51 vargs = args.copy()
52 52 def one(v, tag=name):
53 53 try:
54 54 vargs.update(v)
55 55 except (AttributeError, ValueError):
56 56 try:
57 57 for a, b in v:
58 58 vargs[a] = b
59 59 except ValueError:
60 60 vargs[name] = v
61 61 return templ(tag, **vargs)
62 62 lastname = 'last_' + name
63 63 if lastname in templ:
64 64 last = values.pop()
65 65 else:
66 66 last = None
67 67 for v in values:
68 68 yield one(v)
69 69 if last is not None:
70 70 yield one(last, tag=lastname)
71 71 endname = 'end_' + names
72 72 if endname in templ:
73 73 yield templ(endname, **args)
74 74
75 75 def getfiles(repo, ctx, revcache):
76 76 if 'files' not in revcache:
77 77 revcache['files'] = repo.status(ctx.parents()[0].node(),
78 78 ctx.node())[:3]
79 79 return revcache['files']
80 80
81 81 def getlatesttags(repo, ctx, cache):
82 82 '''return date, distance and name for the latest tag of rev'''
83 83
84 84 if 'latesttags' not in cache:
85 85 # Cache mapping from rev to a tuple with tag date, tag
86 86 # distance and tag name
87 87 cache['latesttags'] = {-1: (0, 0, 'null')}
88 88 latesttags = cache['latesttags']
89 89
90 90 rev = ctx.rev()
91 91 todo = [rev]
92 92 while todo:
93 93 rev = todo.pop()
94 94 if rev in latesttags:
95 95 continue
96 96 ctx = repo[rev]
97 97 tags = [t for t in ctx.tags() if repo.tagtype(t) == 'global']
98 98 if tags:
99 99 latesttags[rev] = ctx.date()[0], 0, ':'.join(sorted(tags))
100 100 continue
101 101 try:
102 102 # The tuples are laid out so the right one can be found by
103 103 # comparison.
104 104 pdate, pdist, ptag = max(
105 105 latesttags[p.rev()] for p in ctx.parents())
106 106 except KeyError:
107 107 # Cache miss - recurse
108 108 todo.append(rev)
109 109 todo.extend(p.rev() for p in ctx.parents())
110 110 continue
111 111 latesttags[rev] = pdate, pdist + 1, ptag
112 112 return latesttags[rev]
113 113
114 114 def getrenamedfn(repo, endrev=None):
115 115 rcache = {}
116 116 if endrev is None:
117 117 endrev = len(repo)
118 118
119 119 def getrenamed(fn, rev):
120 120 '''looks up all renames for a file (up to endrev) the first
121 121 time the file is given. It indexes on the changerev and only
122 122 parses the manifest if linkrev != changerev.
123 123 Returns rename info for fn at changerev rev.'''
124 124 if fn not in rcache:
125 125 rcache[fn] = {}
126 126 fl = repo.file(fn)
127 127 for i in fl:
128 128 lr = fl.linkrev(i)
129 129 renamed = fl.renamed(fl.node(i))
130 130 rcache[fn][lr] = renamed
131 131 if lr >= endrev:
132 132 break
133 133 if rev in rcache[fn]:
134 134 return rcache[fn][rev]
135 135
136 136 # If linkrev != rev (i.e. rev not found in rcache) fallback to
137 137 # filectx logic.
138 138 try:
139 139 return repo[rev][fn].renamed()
140 140 except error.LookupError:
141 141 return None
142 142
143 143 return getrenamed
144 144
145 145
146 146 def showauthor(repo, ctx, templ, **args):
147 147 """:author: String. The unmodified author of the changeset."""
148 148 return ctx.user()
149 149
150 150 def showbranch(**args):
151 151 """:branch: String. The name of the branch on which the changeset was
152 152 committed.
153 153 """
154 154 return args['ctx'].branch()
155 155
156 156 def showbranches(**args):
157 157 """:branches: List of strings. The name of the branch on which the
158 158 changeset was committed. Will be empty if the branch name was
159 159 default.
160 160 """
161 161 branch = args['ctx'].branch()
162 162 if branch != 'default':
163 163 return showlist('branch', [branch], plural='branches', **args)
164 164
165 165 def showbookmarks(**args):
166 """:bookmarks: List of strings. Any bookmarks associated with the
167 changeset.
168 """
166 169 bookmarks = args['ctx'].bookmarks()
167 170 return showlist('bookmark', bookmarks, **args)
168 171
169 172 def showchildren(**args):
170 173 """:children: List of strings. The children of the changeset."""
171 174 ctx = args['ctx']
172 175 childrevs = ['%d:%s' % (cctx, cctx) for cctx in ctx.children()]
173 176 return showlist('children', childrevs, **args)
174 177
175 178 def showdate(repo, ctx, templ, **args):
176 179 """:date: Date information. The date when the changeset was committed."""
177 180 return ctx.date()
178 181
179 182 def showdescription(repo, ctx, templ, **args):
180 183 """:desc: String. The text of the changeset description."""
181 184 return ctx.description().strip()
182 185
183 186 def showdiffstat(repo, ctx, templ, **args):
184 187 """:diffstat: String. Statistics of changes with the following format:
185 188 "modified files: +added/-removed lines"
186 189 """
187 190 files, adds, removes = 0, 0, 0
188 191 for i in patch.diffstatdata(util.iterlines(ctx.diff())):
189 192 files += 1
190 193 adds += i[1]
191 194 removes += i[2]
192 195 return '%s: +%s/-%s' % (files, adds, removes)
193 196
194 197 def showextras(**args):
195 198 templ = args['templ']
196 199 for key, value in sorted(args['ctx'].extra().items()):
197 200 args = args.copy()
198 201 args.update(dict(key=key, value=value))
199 202 yield templ('extra', **args)
200 203
201 204 def showfileadds(**args):
202 205 """:file_adds: List of strings. Files added by this changeset."""
203 206 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
204 207 return showlist('file_add', getfiles(repo, ctx, revcache)[1], **args)
205 208
206 209 def showfilecopies(**args):
207 210 """:file_copies: List of strings. Files copied in this changeset with
208 211 their sources.
209 212 """
210 213 cache, ctx = args['cache'], args['ctx']
211 214 copies = args['revcache'].get('copies')
212 215 if copies is None:
213 216 if 'getrenamed' not in cache:
214 217 cache['getrenamed'] = getrenamedfn(args['repo'])
215 218 copies = []
216 219 getrenamed = cache['getrenamed']
217 220 for fn in ctx.files():
218 221 rename = getrenamed(fn, ctx.rev())
219 222 if rename:
220 223 copies.append((fn, rename[0]))
221 224
222 225 c = [{'name': x[0], 'source': x[1]} for x in copies]
223 226 return showlist('file_copy', c, plural='file_copies', **args)
224 227
225 228 # showfilecopiesswitch() displays file copies only if copy records are
226 229 # provided before calling the templater, usually with a --copies
227 230 # command line switch.
228 231 def showfilecopiesswitch(**args):
229 232 """:file_copies_switch: List of strings. Like "file_copies" but displayed
230 233 only if the --copied switch is set.
231 234 """
232 235 copies = args['revcache'].get('copies') or []
233 236 c = [{'name': x[0], 'source': x[1]} for x in copies]
234 237 return showlist('file_copy', c, plural='file_copies', **args)
235 238
236 239 def showfiledels(**args):
237 240 """:file_dels: List of strings. Files removed by this changeset."""
238 241 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
239 242 return showlist('file_del', getfiles(repo, ctx, revcache)[2], **args)
240 243
241 244 def showfilemods(**args):
242 245 """:file_mods: List of strings. Files modified by this changeset."""
243 246 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
244 247 return showlist('file_mod', getfiles(repo, ctx, revcache)[0], **args)
245 248
246 249 def showfiles(**args):
247 250 """:files: List of strings. All files modified, added, or removed by this
248 251 changeset.
249 252 """
250 253 return showlist('file', args['ctx'].files(), **args)
251 254
252 255 def showlatesttag(repo, ctx, templ, cache, **args):
253 256 """:latesttag: String. Most recent global tag in the ancestors of this
254 257 changeset.
255 258 """
256 259 return getlatesttags(repo, ctx, cache)[2]
257 260
258 261 def showlatesttagdistance(repo, ctx, templ, cache, **args):
259 262 """:latesttagdistance: Integer. Longest path to the latest tag."""
260 263 return getlatesttags(repo, ctx, cache)[1]
261 264
262 265 def showmanifest(**args):
263 266 repo, ctx, templ = args['repo'], args['ctx'], args['templ']
264 267 args = args.copy()
265 268 args.update(dict(rev=repo.manifest.rev(ctx.changeset()[0]),
266 269 node=hex(ctx.changeset()[0])))
267 270 return templ('manifest', **args)
268 271
269 272 def shownode(repo, ctx, templ, **args):
270 273 """:node: String. The changeset identification hash, as a 40 hexadecimal
271 274 digit string.
272 275 """
273 276 return ctx.hex()
274 277
275 278 def showrev(repo, ctx, templ, **args):
276 279 """:rev: Integer. The repository-local changeset revision number."""
277 280 return ctx.rev()
278 281
279 282 def showtags(**args):
280 283 """:tags: List of strings. Any tags associated with the changeset."""
281 284 return showlist('tag', args['ctx'].tags(), **args)
282 285
283 286 # keywords are callables like:
284 287 # fn(repo, ctx, templ, cache, revcache, **args)
285 288 # with:
286 289 # repo - current repository instance
287 290 # ctx - the changectx being displayed
288 291 # templ - the templater instance
289 292 # cache - a cache dictionary for the whole templater run
290 293 # revcache - a cache dictionary for the current revision
291 294 keywords = {
292 295 'author': showauthor,
293 296 'branch': showbranch,
294 297 'branches': showbranches,
295 298 'bookmarks': showbookmarks,
296 299 'children': showchildren,
297 300 'date': showdate,
298 301 'desc': showdescription,
299 302 'diffstat': showdiffstat,
300 303 'extras': showextras,
301 304 'file_adds': showfileadds,
302 305 'file_copies': showfilecopies,
303 306 'file_copies_switch': showfilecopiesswitch,
304 307 'file_dels': showfiledels,
305 308 'file_mods': showfilemods,
306 309 'files': showfiles,
307 310 'latesttag': showlatesttag,
308 311 'latesttagdistance': showlatesttagdistance,
309 312 'manifest': showmanifest,
310 313 'node': shownode,
311 314 'rev': showrev,
312 315 'tags': showtags,
313 316 }
314 317
315 318 def makedoc(topic, doc):
316 319 """Generate and include keyword help in templating topic."""
317 320 kw = []
318 321 for name in sorted(keywords):
319 322 text = (keywords[name].__doc__ or '').rstrip()
320 323 if not text:
321 324 continue
322 325 text = gettext(text)
323 326 lines = text.splitlines()
324 327 lines[1:] = [(' ' + l.strip()) for l in lines[1:]]
325 328 kw.append('\n'.join(lines))
326 329 kw = '\n\n'.join(kw)
327 330 doc = doc.replace('.. keywordsmarker', kw)
328 331 return doc
329 332
330 333 # tell hggettext to extract docstrings from these functions:
331 334 i18nfunctions = keywords.values()
General Comments 0
You need to be logged in to leave comments. Login now