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