##// END OF EJS Templates
keyword: allow keyword expansion on clone...
Christian Ebert -
r6021:717881b4 default
parent child Browse files
Show More
@@ -1,520 +1,519 b''
1 1 # keyword.py - $Keyword$ expansion for Mercurial
2 2 #
3 3 # Copyright 2007, 2008 Christian Ebert <blacktrash@gmx.net>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7 #
8 8 # $Id$
9 9 #
10 10 # Keyword expansion hack against the grain of a DSCM
11 11 #
12 12 # There are many good reasons why this is not needed in a distributed
13 13 # SCM, still it may be useful in very small projects based on single
14 14 # files (like LaTeX packages), that are mostly addressed to an audience
15 15 # not running a version control system.
16 16 #
17 17 # For in-depth discussion refer to
18 18 # <http://www.selenic.com/mercurial/wiki/index.cgi/KeywordPlan>.
19 19 #
20 20 # Keyword expansion is based on Mercurial's changeset template mappings.
21 21 #
22 22 # Binary files are not touched.
23 23 #
24 24 # Setup in hgrc:
25 25 #
26 26 # [extensions]
27 27 # # enable extension
28 28 # hgext.keyword =
29 29 #
30 30 # Files to act upon/ignore are specified in the [keyword] section.
31 31 # Customized keyword template mappings in the [keywordmaps] section.
32 32 #
33 33 # Run "hg help keyword" and "hg kwdemo" to get info on configuration.
34 34
35 35 '''keyword expansion in local repositories
36 36
37 37 This extension expands RCS/CVS-like or self-customized $Keywords$
38 38 in tracked text files selected by your configuration.
39 39
40 40 Keywords are only expanded in local repositories and not stored in
41 41 the change history. The mechanism can be regarded as a convenience
42 42 for the current user or for archive distribution.
43 43
44 44 Configuration is done in the [keyword] and [keywordmaps] sections
45 45 of hgrc files.
46 46
47 47 Example:
48 48
49 49 [keyword]
50 50 # expand keywords in every python file except those matching "x*"
51 51 **.py =
52 52 x* = ignore
53 53
54 54 Note: the more specific you are in your filename patterns
55 55 the less you lose speed in huge repos.
56 56
57 57 For [keywordmaps] template mapping and expansion demonstration and
58 58 control run "hg kwdemo".
59 59
60 60 An additional date template filter {date|utcdate} is provided.
61 61
62 62 The default template mappings (view with "hg kwdemo -d") can be replaced
63 63 with customized keywords and templates.
64 64 Again, run "hg kwdemo" to control the results of your config changes.
65 65
66 66 Before changing/disabling active keywords, run "hg kwshrink" to avoid
67 67 the risk of inadvertedly storing expanded keywords in the change history.
68 68
69 69 To force expansion after enabling it, or a configuration change, run
70 70 "hg kwexpand".
71 71
72 72 Also, when committing with the record extension or using mq's qrecord, be aware
73 73 that keywords cannot be updated. Again, run "hg kwexpand" on the files in
74 74 question to update keyword expansions after all changes have been checked in.
75 75
76 76 Expansions spanning more than one line and incremental expansions,
77 77 like CVS' $Log$, are not supported. A keyword template map
78 78 "Log = {desc}" expands to the first line of the changeset description.
79 79 '''
80 80
81 81 from mercurial import commands, cmdutil, context, dispatch, filelog, revlog
82 82 from mercurial import patch, localrepo, templater, templatefilters, util
83 83 from mercurial.node import *
84 84 from mercurial.i18n import _
85 85 import re, shutil, sys, tempfile, time
86 86
87 87 commands.optionalrepo += ' kwdemo'
88 88
89 89 # hg commands that trigger expansion only when writing to working dir,
90 90 # not when reading filelog, and unexpand when reading from working dir
91 91 restricted = ('diff1', 'record',
92 92 'qfold', 'qimport', 'qnew', 'qpush', 'qrefresh', 'qrecord')
93 93
94 94 def utcdate(date):
95 95 '''Returns hgdate in cvs-like UTC format.'''
96 96 return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0]))
97 97
98 98
99 99 _kwtemplater = None
100 100
101 101 class kwtemplater(object):
102 102 '''
103 103 Sets up keyword templates, corresponding keyword regex, and
104 104 provides keyword substitution functions.
105 105 '''
106 106 templates = {
107 107 'Revision': '{node|short}',
108 108 'Author': '{author|user}',
109 109 'Date': '{date|utcdate}',
110 110 'RCSFile': '{file|basename},v',
111 111 'Source': '{root}/{file},v',
112 112 'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
113 113 'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
114 114 }
115 115
116 116 def __init__(self, ui, repo, inc, exc, hgcmd):
117 117 self.ui = ui
118 118 self.repo = repo
119 119 self.matcher = util.matcher(repo.root, inc=inc, exc=exc)[1]
120 120 self.hgcmd = hgcmd
121 121 self.commitnode = None
122 122 self.path = ''
123 123
124 124 kwmaps = self.ui.configitems('keywordmaps')
125 125 if kwmaps: # override default templates
126 126 kwmaps = [(k, templater.parsestring(v, quoted=False))
127 127 for (k, v) in kwmaps]
128 128 self.templates = dict(kwmaps)
129 129 escaped = map(re.escape, self.templates.keys())
130 130 kwpat = r'\$(%s)(: [^$\n\r]*? )??\$' % '|'.join(escaped)
131 131 self.re_kw = re.compile(kwpat)
132 132
133 133 templatefilters.filters['utcdate'] = utcdate
134 134 self.ct = cmdutil.changeset_templater(self.ui, self.repo,
135 135 False, '', False)
136 136
137 137 def substitute(self, node, data, subfunc):
138 138 '''Obtains file's changenode if commit node not given,
139 139 and calls given substitution function.'''
140 140 if self.commitnode:
141 141 fnode = self.commitnode
142 142 else:
143 143 c = context.filectx(self.repo, self.path, fileid=node)
144 144 fnode = c.node()
145 145
146 146 def kwsub(mobj):
147 147 '''Substitutes keyword using corresponding template.'''
148 148 kw = mobj.group(1)
149 149 self.ct.use_template(self.templates[kw])
150 150 self.ui.pushbuffer()
151 151 self.ct.show(changenode=fnode, root=self.repo.root, file=self.path)
152 152 return '$%s: %s $' % (kw, templatefilters.firstline(
153 153 self.ui.popbuffer()))
154 154
155 155 return subfunc(kwsub, data)
156 156
157 157 def expand(self, node, data):
158 158 '''Returns data with keywords expanded.'''
159 159 if util.binary(data) or self.hgcmd in restricted:
160 160 return data
161 161 return self.substitute(node, data, self.re_kw.sub)
162 162
163 163 def process(self, node, data, expand):
164 164 '''Returns a tuple: data, count.
165 165 Count is number of keywords/keyword substitutions,
166 166 telling caller whether to act on file containing data.'''
167 167 if util.binary(data):
168 168 return data, None
169 169 if expand:
170 170 return self.substitute(node, data, self.re_kw.subn)
171 171 return data, self.re_kw.search(data)
172 172
173 173 def shrink(self, text):
174 174 '''Returns text with all keyword substitutions removed.'''
175 175 if util.binary(text):
176 176 return text
177 177 return self.re_kw.sub(r'$\1$', text)
178 178
179 179 class kwfilelog(filelog.filelog):
180 180 '''
181 181 Subclass of filelog to hook into its read, add, cmp methods.
182 182 Keywords are "stored" unexpanded, and processed on reading.
183 183 '''
184 184 def __init__(self, opener, path):
185 185 super(kwfilelog, self).__init__(opener, path)
186 186 _kwtemplater.path = path
187 187
188 188 def kwctread(self, node, expand):
189 189 '''Reads expanding and counting keywords, called from _overwrite.'''
190 190 data = super(kwfilelog, self).read(node)
191 191 return _kwtemplater.process(node, data, expand)
192 192
193 193 def read(self, node):
194 194 '''Expands keywords when reading filelog.'''
195 195 data = super(kwfilelog, self).read(node)
196 196 return _kwtemplater.expand(node, data)
197 197
198 198 def add(self, text, meta, tr, link, p1=None, p2=None):
199 199 '''Removes keyword substitutions when adding to filelog.'''
200 200 text = _kwtemplater.shrink(text)
201 201 return super(kwfilelog, self).add(text, meta, tr, link, p1=p1, p2=p2)
202 202
203 203 def cmp(self, node, text):
204 204 '''Removes keyword substitutions for comparison.'''
205 205 text = _kwtemplater.shrink(text)
206 206 if self.renamed(node):
207 207 t2 = super(kwfilelog, self).read(node)
208 208 return t2 != text
209 209 return revlog.revlog.cmp(self, node, text)
210 210
211 211
212 212 # store original patch.patchfile.__init__
213 213 _patchfile_init = patch.patchfile.__init__
214 214
215 215 def _kwpatchfile_init(self, ui, fname, missing=False):
216 216 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
217 217 rejects or conflicts due to expanded keywords in working dir.'''
218 218 _patchfile_init(self, ui, fname, missing=missing)
219 219
220 220 if _kwtemplater.matcher(self.fname):
221 221 # shrink keywords read from working dir
222 222 kwshrunk = _kwtemplater.shrink(''.join(self.lines))
223 223 self.lines = kwshrunk.splitlines(True)
224 224
225 225
226 226 def _iskwfile(f, link):
227 227 return not link(f) and _kwtemplater.matcher(f)
228 228
229 229 def _status(ui, repo, *pats, **opts):
230 230 '''Bails out if [keyword] configuration is not active.
231 231 Returns status of working directory.'''
232 232 if _kwtemplater:
233 233 files, match, anypats = cmdutil.matchpats(repo, pats, opts)
234 234 return repo.status(files=files, match=match, list_clean=True)
235 235 if ui.configitems('keyword'):
236 236 raise util.Abort(_('[keyword] patterns cannot match'))
237 237 raise util.Abort(_('no [keyword] patterns configured'))
238 238
239 239 def _overwrite(ui, repo, node=None, expand=True, files=None):
240 240 '''Overwrites selected files expanding/shrinking keywords.'''
241 241 ctx = repo.changectx(node)
242 242 mf = ctx.manifest()
243 243 if node is not None: # commit
244 244 _kwtemplater.commitnode = node
245 245 files = [f for f in ctx.files() if f in mf]
246 246 notify = ui.debug
247 247 else: # kwexpand/kwshrink
248 248 notify = ui.note
249 249 candidates = [f for f in files if _iskwfile(f, mf.linkf)]
250 250 if candidates:
251 251 candidates.sort()
252 252 action = expand and 'expanding' or 'shrinking'
253 253 for f in candidates:
254 254 fp = repo.file(f, kwmatch=True)
255 255 data, kwfound = fp.kwctread(mf[f], expand)
256 256 if kwfound:
257 257 notify(_('overwriting %s %s keywords\n') % (f, action))
258 258 repo.wwrite(f, data, mf.flags(f))
259 259 repo.dirstate.normal(f)
260 260
261 261 def _kwfwrite(ui, repo, expand, *pats, **opts):
262 262 '''Selects files and passes them to _overwrite.'''
263 263 status = _status(ui, repo, *pats, **opts)
264 264 modified, added, removed, deleted, unknown, ignored, clean = status
265 265 if modified or added or removed or deleted:
266 266 raise util.Abort(_('outstanding uncommitted changes in given files'))
267 267 wlock = lock = None
268 268 try:
269 269 wlock = repo.wlock()
270 270 lock = repo.lock()
271 271 _overwrite(ui, repo, expand=expand, files=clean)
272 272 finally:
273 273 del wlock, lock
274 274
275 275
276 276 def demo(ui, repo, *args, **opts):
277 277 '''print [keywordmaps] configuration and an expansion example
278 278
279 279 Show current, custom, or default keyword template maps
280 280 and their expansion.
281 281
282 282 Extend current configuration by specifying maps as arguments
283 283 and optionally by reading from an additional hgrc file.
284 284
285 285 Override current keyword template maps with "default" option.
286 286 '''
287 287 def demostatus(stat):
288 288 ui.status(_('\n\t%s\n') % stat)
289 289
290 290 def demoitems(section, items):
291 291 ui.write('[%s]\n' % section)
292 292 for k, v in items:
293 293 ui.write('%s = %s\n' % (k, v))
294 294
295 295 msg = 'hg keyword config and expansion example'
296 296 kwstatus = 'current'
297 297 fn = 'demo.txt'
298 298 branchname = 'demobranch'
299 299 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
300 300 ui.note(_('creating temporary repo at %s\n') % tmpdir)
301 301 repo = localrepo.localrepository(ui, path=tmpdir, create=True)
302 302 ui.setconfig('keyword', fn, '')
303 303 if args or opts.get('rcfile'):
304 304 kwstatus = 'custom'
305 305 if opts.get('rcfile'):
306 306 ui.readconfig(opts.get('rcfile'))
307 307 if opts.get('default'):
308 308 kwstatus = 'default'
309 309 kwmaps = kwtemplater.templates
310 310 if ui.configitems('keywordmaps'):
311 311 # override maps from optional rcfile
312 312 for k, v in kwmaps.iteritems():
313 313 ui.setconfig('keywordmaps', k, v)
314 314 elif args:
315 315 # simulate hgrc parsing
316 316 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
317 317 fp = repo.opener('hgrc', 'w')
318 318 fp.writelines(rcmaps)
319 319 fp.close()
320 320 ui.readconfig(repo.join('hgrc'))
321 321 if not opts.get('default'):
322 322 kwmaps = dict(ui.configitems('keywordmaps')) or kwtemplater.templates
323 323 reposetup(ui, repo)
324 324 for k, v in ui.configitems('extensions'):
325 325 if k.endswith('keyword'):
326 326 extension = '%s = %s' % (k, v)
327 327 break
328 328 demostatus('config using %s keyword template maps' % kwstatus)
329 329 ui.write('[extensions]\n%s\n' % extension)
330 330 demoitems('keyword', ui.configitems('keyword'))
331 331 demoitems('keywordmaps', kwmaps.iteritems())
332 332 keywords = '$' + '$\n$'.join(kwmaps.keys()) + '$\n'
333 333 repo.wopener(fn, 'w').write(keywords)
334 334 repo.add([fn])
335 335 path = repo.wjoin(fn)
336 336 ui.note(_('\n%s keywords written to %s:\n') % (kwstatus, path))
337 337 ui.note(keywords)
338 338 ui.note('\nhg -R "%s" branch "%s"\n' % (tmpdir, branchname))
339 339 # silence branch command if not verbose
340 340 quiet = ui.quiet
341 341 ui.quiet = not ui.verbose
342 342 commands.branch(ui, repo, branchname)
343 343 ui.quiet = quiet
344 344 for name, cmd in ui.configitems('hooks'):
345 345 if name.split('.', 1)[0].find('commit') > -1:
346 346 repo.ui.setconfig('hooks', name, '')
347 347 ui.note(_('unhooked all commit hooks\n'))
348 348 ui.note('hg -R "%s" ci -m "%s"\n' % (tmpdir, msg))
349 349 repo.commit(text=msg)
350 350 format = ui.verbose and ' in %s' % path or ''
351 351 demostatus('%s keywords expanded%s' % (kwstatus, format))
352 352 ui.write(repo.wread(fn))
353 353 ui.debug(_('\nremoving temporary repo %s\n') % tmpdir)
354 354 shutil.rmtree(tmpdir, ignore_errors=True)
355 355
356 356 def expand(ui, repo, *pats, **opts):
357 357 '''expand keywords in working directory
358 358
359 359 Run after (re)enabling keyword expansion.
360 360
361 361 kwexpand refuses to run if given files contain local changes.
362 362 '''
363 363 # 3rd argument sets expansion to True
364 364 _kwfwrite(ui, repo, True, *pats, **opts)
365 365
366 366 def files(ui, repo, *pats, **opts):
367 367 '''print files currently configured for keyword expansion
368 368
369 369 Crosscheck which files in working directory are potential targets for
370 370 keyword expansion.
371 371 That is, files matched by [keyword] config patterns but not symlinks.
372 372 '''
373 373 status = _status(ui, repo, *pats, **opts)
374 374 modified, added, removed, deleted, unknown, ignored, clean = status
375 375 files = modified + added + clean
376 376 if opts.get('untracked'):
377 377 files += unknown
378 378 files.sort()
379 379 kwfiles = [f for f in files if _iskwfile(f, repo._link)]
380 380 cwd = pats and repo.getcwd() or ''
381 381 kwfstats = not opts.get('ignore') and (('K', kwfiles),) or ()
382 382 if opts.get('all') or opts.get('ignore'):
383 383 kwfstats += (('I', [f for f in files if f not in kwfiles]),)
384 384 for char, filenames in kwfstats:
385 385 format = (opts.get('all') or ui.verbose) and '%s %%s\n' % char or '%s\n'
386 386 for f in filenames:
387 387 ui.write(format % repo.pathto(f, cwd))
388 388
389 389 def shrink(ui, repo, *pats, **opts):
390 390 '''revert expanded keywords in working directory
391 391
392 392 Run before changing/disabling active keywords
393 393 or if you experience problems with "hg import" or "hg merge".
394 394
395 395 kwshrink refuses to run if given files contain local changes.
396 396 '''
397 397 # 3rd argument sets expansion to False
398 398 _kwfwrite(ui, repo, False, *pats, **opts)
399 399
400 400
401 401 def reposetup(ui, repo):
402 402 '''Sets up repo as kwrepo for keyword substitution.
403 403 Overrides file method to return kwfilelog instead of filelog
404 404 if file matches user configuration.
405 405 Wraps commit to overwrite configured files with updated
406 406 keyword substitutions.
407 407 This is done for local repos only, and only if there are
408 408 files configured at all for keyword substitution.'''
409 409
410 410 if not repo.local():
411 411 return
412 412
413 nokwcommands = ('add', 'addremove', 'bundle', 'clone', 'copy',
414 'export', 'grep', 'identify', 'incoming', 'init',
415 'log', 'outgoing', 'push', 'remove', 'rename',
416 'rollback', 'tip',
413 nokwcommands = ('add', 'addremove', 'bundle', 'copy', 'export', 'grep',
414 'identify', 'incoming', 'init', 'log', 'outgoing', 'push',
415 'remove', 'rename', 'rollback', 'tip',
417 416 'convert')
418 417 hgcmd, func, args, opts, cmdopts = dispatch._parse(ui, sys.argv[1:])
419 418 if hgcmd in nokwcommands:
420 419 return
421 420
422 421 if hgcmd == 'diff':
423 422 # only expand if comparing against working dir
424 423 node1, node2 = cmdutil.revpair(repo, cmdopts.get('rev'))
425 424 if node2 is not None:
426 425 return
427 426 # shrink if rev is not current node
428 427 if node1 is not None and node1 != repo.changectx().node():
429 428 hgcmd = 'diff1'
430 429
431 430 inc, exc = [], ['.hgtags']
432 431 for pat, opt in ui.configitems('keyword'):
433 432 if opt != 'ignore':
434 433 inc.append(pat)
435 434 else:
436 435 exc.append(pat)
437 436 if not inc:
438 437 return
439 438
440 439 global _kwtemplater
441 440 _kwtemplater = kwtemplater(ui, repo, inc, exc, hgcmd)
442 441
443 442 class kwrepo(repo.__class__):
444 443 def file(self, f, kwmatch=False):
445 444 if f[0] == '/':
446 445 f = f[1:]
447 446 if kwmatch or _kwtemplater.matcher(f):
448 447 return kwfilelog(self.sopener, f)
449 448 return filelog.filelog(self.sopener, f)
450 449
451 450 def wread(self, filename):
452 451 data = super(kwrepo, self).wread(filename)
453 452 if hgcmd in restricted and _kwtemplater.matcher(filename):
454 453 return _kwtemplater.shrink(data)
455 454 return data
456 455
457 456 def commit(self, files=None, text='', user=None, date=None,
458 457 match=util.always, force=False, force_editor=False,
459 458 p1=None, p2=None, extra={}):
460 459 wlock = lock = None
461 460 _p1 = _p2 = None
462 461 try:
463 462 wlock = self.wlock()
464 463 lock = self.lock()
465 464 # store and postpone commit hooks
466 465 commithooks = {}
467 466 for name, cmd in ui.configitems('hooks'):
468 467 if name.split('.', 1)[0] == 'commit':
469 468 commithooks[name] = cmd
470 469 ui.setconfig('hooks', name, None)
471 470 if commithooks:
472 471 # store parents for commit hook environment
473 472 if p1 is None:
474 473 _p1, _p2 = repo.dirstate.parents()
475 474 else:
476 475 _p1, _p2 = p1, p2 or nullid
477 476 _p1 = hex(_p1)
478 477 if _p2 == nullid:
479 478 _p2 = ''
480 479 else:
481 480 _p2 = hex(_p2)
482 481
483 482 node = super(kwrepo,
484 483 self).commit(files=files, text=text, user=user,
485 484 date=date, match=match, force=force,
486 485 force_editor=force_editor,
487 486 p1=p1, p2=p2, extra=extra)
488 487
489 488 # restore commit hooks
490 489 for name, cmd in commithooks.iteritems():
491 490 ui.setconfig('hooks', name, cmd)
492 491 if node is not None:
493 492 _overwrite(ui, self, node=node)
494 493 repo.hook('commit', node=node, parent1=_p1, parent2=_p2)
495 494 return node
496 495 finally:
497 496 del wlock, lock
498 497
499 498 repo.__class__ = kwrepo
500 499 patch.patchfile.__init__ = _kwpatchfile_init
501 500
502 501
503 502 cmdtable = {
504 503 'kwdemo':
505 504 (demo,
506 505 [('d', 'default', None, _('show default keyword template maps')),
507 506 ('f', 'rcfile', [], _('read maps from rcfile'))],
508 507 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')),
509 508 'kwexpand': (expand, commands.walkopts,
510 509 _('hg kwexpand [OPTION]... [FILE]...')),
511 510 'kwfiles':
512 511 (files,
513 512 [('a', 'all', None, _('show keyword status flags of all files')),
514 513 ('i', 'ignore', None, _('show files excluded from expansion')),
515 514 ('u', 'untracked', None, _('additionally show untracked files')),
516 515 ] + commands.walkopts,
517 516 _('hg kwfiles [OPTION]... [FILE]...')),
518 517 'kwshrink': (shrink, commands.walkopts,
519 518 _('hg kwshrink [OPTION]... [FILE]...')),
520 519 }
General Comments 0
You need to be logged in to leave comments. Login now