##// END OF EJS Templates
keyword: lowercase status flags of untracked files in kwfile output...
Christian Ebert -
r8956:4b8d8f19 default
parent child Browse files
Show More
@@ -1,530 +1,540 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 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
49 49 the less you 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
74 74 "Log = {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, hex
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 kwmaps = [(k, templater.parsestring(v, False))
129 129 for (k, v) in kwmaps]
130 130 self.templates = dict(kwmaps)
131 131 escaped = map(re.escape, self.templates.keys())
132 132 kwpat = r'\$(%s)(: [^$\n\r]*? )??\$' % '|'.join(escaped)
133 133 self.re_kw = re.compile(kwpat)
134 134
135 135 templatefilters.filters['utcdate'] = utcdate
136 136 self.ct = cmdutil.changeset_templater(self.ui, self.repo,
137 137 False, None, '', False)
138 138
139 139 def substitute(self, data, path, ctx, subfunc):
140 140 '''Replaces keywords in data with expanded template.'''
141 141 def kwsub(mobj):
142 142 kw = mobj.group(1)
143 143 self.ct.use_template(self.templates[kw])
144 144 self.ui.pushbuffer()
145 145 self.ct.show(ctx, root=self.repo.root, file=path)
146 146 ekw = templatefilters.firstline(self.ui.popbuffer())
147 147 return '$%s: %s $' % (kw, ekw)
148 148 return subfunc(kwsub, data)
149 149
150 150 def expand(self, path, node, data):
151 151 '''Returns data with keywords expanded.'''
152 152 if not self.restrict and self.match(path) and not util.binary(data):
153 153 ctx = self.repo.filectx(path, fileid=node).changectx()
154 154 return self.substitute(data, path, ctx, self.re_kw.sub)
155 155 return data
156 156
157 157 def iskwfile(self, path, flagfunc):
158 158 '''Returns true if path matches [keyword] pattern
159 159 and is not a symbolic link.
160 160 Caveat: localrepository._link fails on Windows.'''
161 161 return self.match(path) and not 'l' in flagfunc(path)
162 162
163 163 def overwrite(self, node, expand, files):
164 164 '''Overwrites selected files expanding/shrinking keywords.'''
165 165 ctx = self.repo[node]
166 166 mf = ctx.manifest()
167 167 if node is not None: # commit
168 168 files = [f for f in ctx.files() if f in mf]
169 169 notify = self.ui.debug
170 170 else: # kwexpand/kwshrink
171 171 notify = self.ui.note
172 172 candidates = [f for f in files if self.iskwfile(f, ctx.flags)]
173 173 if candidates:
174 174 self.restrict = True # do not expand when reading
175 175 msg = (expand and _('overwriting %s expanding keywords\n')
176 176 or _('overwriting %s shrinking keywords\n'))
177 177 for f in candidates:
178 178 fp = self.repo.file(f)
179 179 data = fp.read(mf[f])
180 180 if util.binary(data):
181 181 continue
182 182 if expand:
183 183 if node is None:
184 184 ctx = self.repo.filectx(f, fileid=mf[f]).changectx()
185 185 data, found = self.substitute(data, f, ctx,
186 186 self.re_kw.subn)
187 187 else:
188 188 found = self.re_kw.search(data)
189 189 if found:
190 190 notify(msg % f)
191 191 self.repo.wwrite(f, data, mf.flags(f))
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 current configuration by specifying maps as arguments and
280 280 optionally by reading from an additional hgrc file.
281 281
282 282 Override current keyword template maps with "default" option.
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 kwstatus = 'current'
291 291 fn = 'demo.txt'
292 292 branchname = 'demobranch'
293 293 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
294 294 ui.note(_('creating temporary repository at %s\n') % tmpdir)
295 295 repo = localrepo.localrepository(ui, tmpdir, True)
296 296 ui.setconfig('keyword', fn, '')
297 297 if args or opts.get('rcfile'):
298 298 kwstatus = 'custom'
299 299 if opts.get('rcfile'):
300 300 ui.readconfig(opts.get('rcfile'))
301 301 if opts.get('default'):
302 302 kwstatus = 'default'
303 303 kwmaps = kwtemplater.templates
304 304 if ui.configitems('keywordmaps'):
305 305 # override maps from optional rcfile
306 306 for k, v in kwmaps.iteritems():
307 307 ui.setconfig('keywordmaps', k, v)
308 308 elif args:
309 309 # simulate hgrc parsing
310 310 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
311 311 fp = repo.opener('hgrc', 'w')
312 312 fp.writelines(rcmaps)
313 313 fp.close()
314 314 ui.readconfig(repo.join('hgrc'))
315 315 if not opts.get('default'):
316 316 kwmaps = dict(ui.configitems('keywordmaps')) or kwtemplater.templates
317 317 uisetup(ui)
318 318 reposetup(ui, repo)
319 319 for k, v in ui.configitems('extensions'):
320 320 if k.endswith('keyword'):
321 321 extension = '%s = %s' % (k, v)
322 322 break
323 323 ui.status(_('\n\tconfig using %s keyword template maps\n') % kwstatus)
324 324 ui.write('[extensions]\n%s\n' % extension)
325 325 demoitems('keyword', ui.configitems('keyword'))
326 326 demoitems('keywordmaps', kwmaps.iteritems())
327 327 keywords = '$' + '$\n$'.join(kwmaps.keys()) + '$\n'
328 328 repo.wopener(fn, 'w').write(keywords)
329 329 repo.add([fn])
330 330 path = repo.wjoin(fn)
331 331 ui.note(_('\n%s keywords written to %s:\n') % (kwstatus, path))
332 332 ui.note(keywords)
333 333 ui.note('\nhg -R "%s" branch "%s"\n' % (tmpdir, branchname))
334 334 # silence branch command if not verbose
335 335 quiet = ui.quiet
336 336 ui.quiet = not ui.verbose
337 337 commands.branch(ui, repo, branchname)
338 338 ui.quiet = quiet
339 339 for name, cmd in ui.configitems('hooks'):
340 340 if name.split('.', 1)[0].find('commit') > -1:
341 341 repo.ui.setconfig('hooks', name, '')
342 342 ui.note(_('unhooked all commit hooks\n'))
343 343 ui.note('hg -R "%s" ci -m "%s"\n' % (tmpdir, msg))
344 344 repo.commit(text=msg)
345 345 fmt = ui.verbose and ' in %s' % path or ''
346 346 ui.status(_('\n\t%s keywords expanded%s\n') % (kwstatus, fmt))
347 347 ui.write(repo.wread(fn))
348 348 ui.debug(_('\nremoving temporary repository %s\n') % tmpdir)
349 349 shutil.rmtree(tmpdir, ignore_errors=True)
350 350
351 351 def expand(ui, repo, *pats, **opts):
352 352 '''expand keywords in the working directory
353 353
354 354 Run after (re)enabling keyword expansion.
355 355
356 356 kwexpand refuses to run if given files contain local changes.
357 357 '''
358 358 # 3rd argument sets expansion to True
359 359 _kwfwrite(ui, repo, True, *pats, **opts)
360 360
361 361 def files(ui, repo, *pats, **opts):
362 362 '''print filenames configured for keyword expansion
363 363
364 364 Check which filenames in the working directory are matched by the
365 365 [keyword] configuration patterns.
366 366
367 367 Useful to prevent inadvertent keyword expansion and to speed up
368 368 execution by including only filenames that are actual candidates
369 369 for expansion.
370 370
371 371 Use -u/--untracked to display untracked filenames as well.
372
373 With -a/--all and -v/--verbose the codes used to show the status
374 of files are:
375 K = keyword expansion candidate
376 k = keyword expansion candidate (untracked)
377 I = ignored
378 i = ignored (untracked)
372 379 '''
373 380 kwt = kwtools['templater']
374 381 status = _status(ui, repo, kwt, opts.get('untracked'), *pats, **opts)
375 382 modified, added, removed, deleted, unknown, ignored, clean = status
376 files = sorted(modified + added + clean + unknown)
383 files = sorted(modified + added + clean)
377 384 wctx = repo[None]
378 385 kwfiles = [f for f in files if kwt.iskwfile(f, wctx.flags)]
386 kwuntracked = [f for f in unknown if kwt.iskwfile(f, wctx.flags)]
379 387 cwd = pats and repo.getcwd() or ''
380 kwfstats = not opts.get('ignore') and (('K', kwfiles),) or ()
388 kwfstats = (not opts.get('ignore') and
389 (('K', kwfiles), ('k', kwuntracked),) or ())
381 390 if opts.get('all') or opts.get('ignore'):
382 kwfstats += (('I', [f for f in files if f not in kwfiles]),)
391 kwfstats += (('I', [f for f in files if f not in kwfiles]),
392 ('i', [f for f in unknown if f not in kwuntracked]),)
383 393 for char, filenames in kwfstats:
384 394 fmt = (opts.get('all') or ui.verbose) and '%s %%s\n' % char or '%s\n'
385 395 for f in filenames:
386 396 ui.write(fmt % repo.pathto(f, cwd))
387 397
388 398 def shrink(ui, repo, *pats, **opts):
389 399 '''revert expanded keywords in the working directory
390 400
391 401 Run before changing/disabling active keywords or if you experience
392 402 problems with "hg import" or "hg merge".
393 403
394 404 kwshrink refuses to run if given files contain local changes.
395 405 '''
396 406 # 3rd argument sets expansion to False
397 407 _kwfwrite(ui, repo, False, *pats, **opts)
398 408
399 409
400 410 def uisetup(ui):
401 411 '''Collects [keyword] config in kwtools.
402 412 Monkeypatches dispatch._parse if needed.'''
403 413
404 414 for pat, opt in ui.configitems('keyword'):
405 415 if opt != 'ignore':
406 416 kwtools['inc'].append(pat)
407 417 else:
408 418 kwtools['exc'].append(pat)
409 419
410 420 if kwtools['inc']:
411 421 def kwdispatch_parse(orig, ui, args):
412 422 '''Monkeypatch dispatch._parse to obtain running hg command.'''
413 423 cmd, func, args, options, cmdoptions = orig(ui, args)
414 424 kwtools['hgcmd'] = cmd
415 425 return cmd, func, args, options, cmdoptions
416 426
417 427 extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse)
418 428
419 429 def reposetup(ui, repo):
420 430 '''Sets up repo as kwrepo for keyword substitution.
421 431 Overrides file method to return kwfilelog instead of filelog
422 432 if file matches user configuration.
423 433 Wraps commit to overwrite configured files with updated
424 434 keyword substitutions.
425 435 Monkeypatches patch and webcommands.'''
426 436
427 437 try:
428 438 if (not repo.local() or not kwtools['inc']
429 439 or kwtools['hgcmd'] in nokwcommands.split()
430 440 or '.hg' in util.splitpath(repo.root)
431 441 or repo._url.startswith('bundle:')):
432 442 return
433 443 except AttributeError:
434 444 pass
435 445
436 446 kwtools['templater'] = kwt = kwtemplater(ui, repo)
437 447
438 448 class kwrepo(repo.__class__):
439 449 def file(self, f):
440 450 if f[0] == '/':
441 451 f = f[1:]
442 452 return kwfilelog(self.sopener, kwt, f)
443 453
444 454 def wread(self, filename):
445 455 data = super(kwrepo, self).wread(filename)
446 456 return kwt.wread(filename, data)
447 457
448 458 def commit(self, text='', user=None, date=None, match=None,
449 459 force=False, editor=None, extra={}):
450 460 wlock = lock = None
451 461 _p1 = _p2 = None
452 462 try:
453 463 wlock = self.wlock()
454 464 lock = self.lock()
455 465 # store and postpone commit hooks
456 466 commithooks = {}
457 467 for name, cmd in ui.configitems('hooks'):
458 468 if name.split('.', 1)[0] == 'commit':
459 469 commithooks[name] = cmd
460 470 ui.setconfig('hooks', name, None)
461 471 if commithooks:
462 472 # store parents for commit hook environment
463 473 _p1, _p2 = repo.dirstate.parents()
464 474 _p1 = hex(_p1)
465 475 if _p2 == nullid:
466 476 _p2 = ''
467 477 else:
468 478 _p2 = hex(_p2)
469 479
470 480 n = super(kwrepo, self).commit(text, user, date, match, force,
471 481 editor, extra)
472 482
473 483 # restore commit hooks
474 484 for name, cmd in commithooks.iteritems():
475 485 ui.setconfig('hooks', name, cmd)
476 486 if n is not None:
477 487 kwt.overwrite(n, True, None)
478 488 repo.hook('commit', node=n, parent1=_p1, parent2=_p2)
479 489 return n
480 490 finally:
481 491 release(lock, wlock)
482 492
483 493 # monkeypatches
484 494 def kwpatchfile_init(orig, self, ui, fname, opener, missing=False, eol=None):
485 495 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
486 496 rejects or conflicts due to expanded keywords in working dir.'''
487 497 orig(self, ui, fname, opener, missing, eol)
488 498 # shrink keywords read from working dir
489 499 self.lines = kwt.shrinklines(self.fname, self.lines)
490 500
491 501 def kw_diff(orig, repo, node1=None, node2=None, match=None, changes=None,
492 502 opts=None):
493 503 '''Monkeypatch patch.diff to avoid expansion except when
494 504 comparing against working dir.'''
495 505 if node2 is not None:
496 506 kwt.match = util.never
497 507 elif node1 is not None and node1 != repo['.'].node():
498 508 kwt.restrict = True
499 509 return orig(repo, node1, node2, match, changes, opts)
500 510
501 511 def kwweb_skip(orig, web, req, tmpl):
502 512 '''Wraps webcommands.x turning off keyword expansion.'''
503 513 kwt.match = util.never
504 514 return orig(web, req, tmpl)
505 515
506 516 repo.__class__ = kwrepo
507 517
508 518 extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init)
509 519 extensions.wrapfunction(patch, 'diff', kw_diff)
510 520 for c in 'annotate changeset rev filediff diff'.split():
511 521 extensions.wrapfunction(webcommands, c, kwweb_skip)
512 522
513 523 cmdtable = {
514 524 'kwdemo':
515 525 (demo,
516 526 [('d', 'default', None, _('show default keyword template maps')),
517 527 ('f', 'rcfile', [], _('read maps from rcfile'))],
518 528 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')),
519 529 'kwexpand': (expand, commands.walkopts,
520 530 _('hg kwexpand [OPTION]... [FILE]...')),
521 531 'kwfiles':
522 532 (files,
523 533 [('a', 'all', None, _('show keyword status flags of all files')),
524 534 ('i', 'ignore', None, _('show files excluded from expansion')),
525 535 ('u', 'untracked', None, _('additionally show untracked files')),
526 536 ] + commands.walkopts,
527 537 _('hg kwfiles [OPTION]... [FILE]...')),
528 538 'kwshrink': (shrink, commands.walkopts,
529 539 _('hg kwshrink [OPTION]... [FILE]...')),
530 540 }
General Comments 0
You need to be logged in to leave comments. Login now