##// END OF EJS Templates
keyword: avoid x = a and b or c
Christian Ebert -
r15030:26148257 default
parent child Browse files
Show More
@@ -1,690 +1,700 b''
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 import scmutil
87 87 from mercurial.hgweb import webcommands
88 88 from mercurial.i18n import _
89 89 import os, re, shutil, tempfile
90 90
91 91 commands.optionalrepo += ' kwdemo'
92 92
93 93 cmdtable = {}
94 94 command = cmdutil.command(cmdtable)
95 95
96 96 # hg commands that do not act on keywords
97 97 nokwcommands = ('add addremove annotate bundle export grep incoming init log'
98 98 ' outgoing push tip verify convert email glog')
99 99
100 100 # hg commands that trigger expansion only when writing to working dir,
101 101 # not when reading filelog, and unexpand when reading from working dir
102 102 restricted = 'merge kwexpand kwshrink record qrecord resolve transplant'
103 103
104 104 # names of extensions using dorecord
105 105 recordextensions = 'record'
106 106
107 107 colortable = {
108 108 'kwfiles.enabled': 'green bold',
109 109 'kwfiles.deleted': 'cyan bold underline',
110 110 'kwfiles.enabledunknown': 'green',
111 111 'kwfiles.ignored': 'bold',
112 112 'kwfiles.ignoredunknown': 'none'
113 113 }
114 114
115 115 # date like in cvs' $Date
116 116 def utcdate(text):
117 117 ''':utcdate: Date. Returns a UTC-date in this format: "2009/08/18 11:00:13".
118 118 '''
119 119 return util.datestr((text[0], 0), '%Y/%m/%d %H:%M:%S')
120 120 # date like in svn's $Date
121 121 def svnisodate(text):
122 122 ''':svnisodate: Date. Returns a date in this format: "2009-08-18 13:00:13
123 123 +0200 (Tue, 18 Aug 2009)".
124 124 '''
125 125 return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2 (%a, %d %b %Y)')
126 126 # date like in svn's $Id
127 127 def svnutcdate(text):
128 128 ''':svnutcdate: Date. Returns a UTC-date in this format: "2009-08-18
129 129 11:00:13Z".
130 130 '''
131 131 return util.datestr((text[0], 0), '%Y-%m-%d %H:%M:%SZ')
132 132
133 133 templatefilters.filters.update({'utcdate': utcdate,
134 134 'svnisodate': svnisodate,
135 135 'svnutcdate': svnutcdate})
136 136
137 137 # make keyword tools accessible
138 138 kwtools = {'templater': None, 'hgcmd': ''}
139 139
140 140 def _defaultkwmaps(ui):
141 141 '''Returns default keywordmaps according to keywordset configuration.'''
142 142 templates = {
143 143 'Revision': '{node|short}',
144 144 'Author': '{author|user}',
145 145 }
146 146 kwsets = ({
147 147 'Date': '{date|utcdate}',
148 148 'RCSfile': '{file|basename},v',
149 149 'RCSFile': '{file|basename},v', # kept for backwards compatibility
150 150 # with hg-keyword
151 151 'Source': '{root}/{file},v',
152 152 'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
153 153 'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
154 154 }, {
155 155 'Date': '{date|svnisodate}',
156 156 'Id': '{file|basename},v {node|short} {date|svnutcdate} {author|user}',
157 157 'LastChangedRevision': '{node|short}',
158 158 'LastChangedBy': '{author|user}',
159 159 'LastChangedDate': '{date|svnisodate}',
160 160 })
161 161 templates.update(kwsets[ui.configbool('keywordset', 'svn')])
162 162 return templates
163 163
164 164 def _shrinktext(text, subfunc):
165 165 '''Helper for keyword expansion removal in text.
166 166 Depending on subfunc also returns number of substitutions.'''
167 167 return subfunc(r'$\1$', text)
168 168
169 169 def _preselect(wstatus, changed):
170 170 '''Retrieves modfied and added files from a working directory state
171 171 and returns the subset of each contained in given changed files
172 172 retrieved from a change context.'''
173 173 modified, added = wstatus[:2]
174 174 modified = [f for f in modified if f in changed]
175 175 added = [f for f in added if f in changed]
176 176 return modified, added
177 177
178 178
179 179 class kwtemplater(object):
180 180 '''
181 181 Sets up keyword templates, corresponding keyword regex, and
182 182 provides keyword substitution functions.
183 183 '''
184 184
185 185 def __init__(self, ui, repo, inc, exc):
186 186 self.ui = ui
187 187 self.repo = repo
188 188 self.match = match.match(repo.root, '', [], inc, exc)
189 189 self.restrict = kwtools['hgcmd'] in restricted.split()
190 190 self.record = False
191 191
192 192 kwmaps = self.ui.configitems('keywordmaps')
193 193 if kwmaps: # override default templates
194 194 self.templates = dict((k, templater.parsestring(v, False))
195 195 for k, v in kwmaps)
196 196 else:
197 197 self.templates = _defaultkwmaps(self.ui)
198 198
199 199 @util.propertycache
200 200 def escape(self):
201 201 '''Returns bar-separated and escaped keywords.'''
202 202 return '|'.join(map(re.escape, self.templates.keys()))
203 203
204 204 @util.propertycache
205 205 def rekw(self):
206 206 '''Returns regex for unexpanded keywords.'''
207 207 return re.compile(r'\$(%s)\$' % self.escape)
208 208
209 209 @util.propertycache
210 210 def rekwexp(self):
211 211 '''Returns regex for expanded keywords.'''
212 212 return re.compile(r'\$(%s): [^$\n\r]*? \$' % self.escape)
213 213
214 214 def substitute(self, data, path, ctx, subfunc):
215 215 '''Replaces keywords in data with expanded template.'''
216 216 def kwsub(mobj):
217 217 kw = mobj.group(1)
218 218 ct = cmdutil.changeset_templater(self.ui, self.repo,
219 219 False, None, '', False)
220 220 ct.use_template(self.templates[kw])
221 221 self.ui.pushbuffer()
222 222 ct.show(ctx, root=self.repo.root, file=path)
223 223 ekw = templatefilters.firstline(self.ui.popbuffer())
224 224 return '$%s: %s $' % (kw, ekw)
225 225 return subfunc(kwsub, data)
226 226
227 227 def linkctx(self, path, fileid):
228 228 '''Similar to filelog.linkrev, but returns a changectx.'''
229 229 return self.repo.filectx(path, fileid=fileid).changectx()
230 230
231 231 def expand(self, path, node, data):
232 232 '''Returns data with keywords expanded.'''
233 233 if not self.restrict and self.match(path) and not util.binary(data):
234 234 ctx = self.linkctx(path, node)
235 235 return self.substitute(data, path, ctx, self.rekw.sub)
236 236 return data
237 237
238 238 def iskwfile(self, cand, ctx):
239 239 '''Returns subset of candidates which are configured for keyword
240 240 expansion are not symbolic links.'''
241 241 return [f for f in cand if self.match(f) and not 'l' in ctx.flags(f)]
242 242
243 243 def overwrite(self, ctx, candidates, lookup, expand, rekw=False):
244 244 '''Overwrites selected files expanding/shrinking keywords.'''
245 245 if self.restrict or lookup or self.record: # exclude kw_copy
246 246 candidates = self.iskwfile(candidates, ctx)
247 247 if not candidates:
248 248 return
249 249 kwcmd = self.restrict and lookup # kwexpand/kwshrink
250 250 if self.restrict or expand and lookup:
251 251 mf = ctx.manifest()
252 if self.restrict or rekw:
253 re_kw = self.rekw
254 else:
255 re_kw = self.rekwexp
256 if expand:
257 msg = _('overwriting %s expanding keywords\n')
258 else:
259 msg = _('overwriting %s shrinking keywords\n')
252 260 lctx = ctx
253 re_kw = (self.restrict or rekw) and self.rekw or self.rekwexp
254 msg = (expand and _('overwriting %s expanding keywords\n')
255 or _('overwriting %s shrinking keywords\n'))
256 261 for f in candidates:
257 262 if self.restrict:
258 263 data = self.repo.file(f).read(mf[f])
259 264 else:
260 265 data = self.repo.wread(f)
261 266 if util.binary(data):
262 267 continue
263 268 if expand:
264 269 if lookup:
265 270 lctx = self.linkctx(f, mf[f])
266 271 data, found = self.substitute(data, f, lctx, re_kw.subn)
267 272 elif self.restrict:
268 273 found = re_kw.search(data)
269 274 else:
270 275 data, found = _shrinktext(data, re_kw.subn)
271 276 if found:
272 277 self.ui.note(msg % f)
273 278 self.repo.wwrite(f, data, ctx.flags(f))
274 279 if kwcmd:
275 280 self.repo.dirstate.normal(f)
276 281 elif self.record:
277 282 self.repo.dirstate.normallookup(f)
278 283
279 284 def shrink(self, fname, text):
280 285 '''Returns text with all keyword substitutions removed.'''
281 286 if self.match(fname) and not util.binary(text):
282 287 return _shrinktext(text, self.rekwexp.sub)
283 288 return text
284 289
285 290 def shrinklines(self, fname, lines):
286 291 '''Returns lines with keyword substitutions removed.'''
287 292 if self.match(fname):
288 293 text = ''.join(lines)
289 294 if not util.binary(text):
290 295 return _shrinktext(text, self.rekwexp.sub).splitlines(True)
291 296 return lines
292 297
293 298 def wread(self, fname, data):
294 299 '''If in restricted mode returns data read from wdir with
295 300 keyword substitutions removed.'''
296 return self.restrict and self.shrink(fname, data) or data
301 if self.restrict:
302 return self.shrink(fname, data)
303 return data
297 304
298 305 class kwfilelog(filelog.filelog):
299 306 '''
300 307 Subclass of filelog to hook into its read, add, cmp methods.
301 308 Keywords are "stored" unexpanded, and processed on reading.
302 309 '''
303 310 def __init__(self, opener, kwt, path):
304 311 super(kwfilelog, self).__init__(opener, path)
305 312 self.kwt = kwt
306 313 self.path = path
307 314
308 315 def read(self, node):
309 316 '''Expands keywords when reading filelog.'''
310 317 data = super(kwfilelog, self).read(node)
311 318 if self.renamed(node):
312 319 return data
313 320 return self.kwt.expand(self.path, node, data)
314 321
315 322 def add(self, text, meta, tr, link, p1=None, p2=None):
316 323 '''Removes keyword substitutions when adding to filelog.'''
317 324 text = self.kwt.shrink(self.path, text)
318 325 return super(kwfilelog, self).add(text, meta, tr, link, p1, p2)
319 326
320 327 def cmp(self, node, text):
321 328 '''Removes keyword substitutions for comparison.'''
322 329 text = self.kwt.shrink(self.path, text)
323 330 return super(kwfilelog, self).cmp(node, text)
324 331
325 332 def _status(ui, repo, wctx, kwt, *pats, **opts):
326 333 '''Bails out if [keyword] configuration is not active.
327 334 Returns status of working directory.'''
328 335 if kwt:
329 336 return repo.status(match=scmutil.match(wctx, pats, opts), clean=True,
330 337 unknown=opts.get('unknown') or opts.get('all'))
331 338 if ui.configitems('keyword'):
332 339 raise util.Abort(_('[keyword] patterns cannot match'))
333 340 raise util.Abort(_('no [keyword] patterns configured'))
334 341
335 342 def _kwfwrite(ui, repo, expand, *pats, **opts):
336 343 '''Selects files and passes them to kwtemplater.overwrite.'''
337 344 wctx = repo[None]
338 345 if len(wctx.parents()) > 1:
339 346 raise util.Abort(_('outstanding uncommitted merge'))
340 347 kwt = kwtools['templater']
341 348 wlock = repo.wlock()
342 349 try:
343 350 status = _status(ui, repo, wctx, kwt, *pats, **opts)
344 351 modified, added, removed, deleted, unknown, ignored, clean = status
345 352 if modified or added or removed or deleted:
346 353 raise util.Abort(_('outstanding uncommitted changes'))
347 354 kwt.overwrite(wctx, clean, True, expand)
348 355 finally:
349 356 wlock.release()
350 357
351 358 @command('kwdemo',
352 359 [('d', 'default', None, _('show default keyword template maps')),
353 360 ('f', 'rcfile', '',
354 361 _('read maps from rcfile'), _('FILE'))],
355 362 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...'))
356 363 def demo(ui, repo, *args, **opts):
357 364 '''print [keywordmaps] configuration and an expansion example
358 365
359 366 Show current, custom, or default keyword template maps and their
360 367 expansions.
361 368
362 369 Extend the current configuration by specifying maps as arguments
363 370 and using -f/--rcfile to source an external hgrc file.
364 371
365 372 Use -d/--default to disable current configuration.
366 373
367 374 See :hg:`help templates` for information on templates and filters.
368 375 '''
369 376 def demoitems(section, items):
370 377 ui.write('[%s]\n' % section)
371 378 for k, v in sorted(items):
372 379 ui.write('%s = %s\n' % (k, v))
373 380
374 381 fn = 'demo.txt'
375 382 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
376 383 ui.note(_('creating temporary repository at %s\n') % tmpdir)
377 384 repo = localrepo.localrepository(ui, tmpdir, True)
378 385 ui.setconfig('keyword', fn, '')
379 386 svn = ui.configbool('keywordset', 'svn')
380 387 # explicitly set keywordset for demo output
381 388 ui.setconfig('keywordset', 'svn', svn)
382 389
383 390 uikwmaps = ui.configitems('keywordmaps')
384 391 if args or opts.get('rcfile'):
385 392 ui.status(_('\n\tconfiguration using custom keyword template maps\n'))
386 393 if uikwmaps:
387 394 ui.status(_('\textending current template maps\n'))
388 395 if opts.get('default') or not uikwmaps:
389 396 if svn:
390 397 ui.status(_('\toverriding default svn keywordset\n'))
391 398 else:
392 399 ui.status(_('\toverriding default cvs keywordset\n'))
393 400 if opts.get('rcfile'):
394 401 ui.readconfig(opts.get('rcfile'))
395 402 if args:
396 403 # simulate hgrc parsing
397 404 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
398 405 fp = repo.opener('hgrc', 'w')
399 406 fp.writelines(rcmaps)
400 407 fp.close()
401 408 ui.readconfig(repo.join('hgrc'))
402 409 kwmaps = dict(ui.configitems('keywordmaps'))
403 410 elif opts.get('default'):
404 411 if svn:
405 412 ui.status(_('\n\tconfiguration using default svn keywordset\n'))
406 413 else:
407 414 ui.status(_('\n\tconfiguration using default cvs keywordset\n'))
408 415 kwmaps = _defaultkwmaps(ui)
409 416 if uikwmaps:
410 417 ui.status(_('\tdisabling current template maps\n'))
411 418 for k, v in kwmaps.iteritems():
412 419 ui.setconfig('keywordmaps', k, v)
413 420 else:
414 421 ui.status(_('\n\tconfiguration using current keyword template maps\n'))
415 kwmaps = dict(uikwmaps) or _defaultkwmaps(ui)
422 if uikwmaps:
423 kwmaps = dict(uikwmaps)
424 else:
425 kwmaps = _defaultkwmaps(ui)
416 426
417 427 uisetup(ui)
418 428 reposetup(ui, repo)
419 429 ui.write('[extensions]\nkeyword =\n')
420 430 demoitems('keyword', ui.configitems('keyword'))
421 431 demoitems('keywordset', ui.configitems('keywordset'))
422 432 demoitems('keywordmaps', kwmaps.iteritems())
423 433 keywords = '$' + '$\n$'.join(sorted(kwmaps.keys())) + '$\n'
424 434 repo.wopener.write(fn, keywords)
425 435 repo[None].add([fn])
426 436 ui.note(_('\nkeywords written to %s:\n') % fn)
427 437 ui.note(keywords)
428 438 repo.dirstate.setbranch('demobranch')
429 439 for name, cmd in ui.configitems('hooks'):
430 440 if name.split('.', 1)[0].find('commit') > -1:
431 441 repo.ui.setconfig('hooks', name, '')
432 442 msg = _('hg keyword configuration and expansion example')
433 443 ui.note("hg ci -m '%s'\n" % msg)
434 444 repo.commit(text=msg)
435 445 ui.status(_('\n\tkeywords expanded\n'))
436 446 ui.write(repo.wread(fn))
437 447 shutil.rmtree(tmpdir, ignore_errors=True)
438 448
439 449 @command('kwexpand', commands.walkopts, _('hg kwexpand [OPTION]... [FILE]...'))
440 450 def expand(ui, repo, *pats, **opts):
441 451 '''expand keywords in the working directory
442 452
443 453 Run after (re)enabling keyword expansion.
444 454
445 455 kwexpand refuses to run if given files contain local changes.
446 456 '''
447 457 # 3rd argument sets expansion to True
448 458 _kwfwrite(ui, repo, True, *pats, **opts)
449 459
450 460 @command('kwfiles',
451 461 [('A', 'all', None, _('show keyword status flags of all files')),
452 462 ('i', 'ignore', None, _('show files excluded from expansion')),
453 463 ('u', 'unknown', None, _('only show unknown (not tracked) files')),
454 464 ] + commands.walkopts,
455 465 _('hg kwfiles [OPTION]... [FILE]...'))
456 466 def files(ui, repo, *pats, **opts):
457 467 '''show files configured for keyword expansion
458 468
459 469 List which files in the working directory are matched by the
460 470 [keyword] configuration patterns.
461 471
462 472 Useful to prevent inadvertent keyword expansion and to speed up
463 473 execution by including only files that are actual candidates for
464 474 expansion.
465 475
466 476 See :hg:`help keyword` on how to construct patterns both for
467 477 inclusion and exclusion of files.
468 478
469 479 With -A/--all and -v/--verbose the codes used to show the status
470 480 of files are::
471 481
472 482 K = keyword expansion candidate
473 483 k = keyword expansion candidate (not tracked)
474 484 I = ignored
475 485 i = ignored (not tracked)
476 486 '''
477 487 kwt = kwtools['templater']
478 488 wctx = repo[None]
479 489 status = _status(ui, repo, wctx, kwt, *pats, **opts)
480 490 cwd = pats and repo.getcwd() or ''
481 491 modified, added, removed, deleted, unknown, ignored, clean = status
482 492 files = []
483 493 if not opts.get('unknown') or opts.get('all'):
484 494 files = sorted(modified + added + clean)
485 495 kwfiles = kwt.iskwfile(files, wctx)
486 496 kwdeleted = kwt.iskwfile(deleted, wctx)
487 497 kwunknown = kwt.iskwfile(unknown, wctx)
488 498 if not opts.get('ignore') or opts.get('all'):
489 499 showfiles = kwfiles, kwdeleted, kwunknown
490 500 else:
491 501 showfiles = [], [], []
492 502 if opts.get('all') or opts.get('ignore'):
493 503 showfiles += ([f for f in files if f not in kwfiles],
494 504 [f for f in unknown if f not in kwunknown])
495 505 kwlabels = 'enabled deleted enabledunknown ignored ignoredunknown'.split()
496 506 kwstates = zip('K!kIi', showfiles, kwlabels)
497 507 for char, filenames, kwstate in kwstates:
498 508 fmt = (opts.get('all') or ui.verbose) and '%s %%s\n' % char or '%s\n'
499 509 for f in filenames:
500 510 ui.write(fmt % repo.pathto(f, cwd), label='kwfiles.' + kwstate)
501 511
502 512 @command('kwshrink', commands.walkopts, _('hg kwshrink [OPTION]... [FILE]...'))
503 513 def shrink(ui, repo, *pats, **opts):
504 514 '''revert expanded keywords in the working directory
505 515
506 516 Must be run before changing/disabling active keywords.
507 517
508 518 kwshrink refuses to run if given files contain local changes.
509 519 '''
510 520 # 3rd argument sets expansion to False
511 521 _kwfwrite(ui, repo, False, *pats, **opts)
512 522
513 523
514 524 def uisetup(ui):
515 525 ''' Monkeypatches dispatch._parse to retrieve user command.'''
516 526
517 527 def kwdispatch_parse(orig, ui, args):
518 528 '''Monkeypatch dispatch._parse to obtain running hg command.'''
519 529 cmd, func, args, options, cmdoptions = orig(ui, args)
520 530 kwtools['hgcmd'] = cmd
521 531 return cmd, func, args, options, cmdoptions
522 532
523 533 extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse)
524 534
525 535 def reposetup(ui, repo):
526 536 '''Sets up repo as kwrepo for keyword substitution.
527 537 Overrides file method to return kwfilelog instead of filelog
528 538 if file matches user configuration.
529 539 Wraps commit to overwrite configured files with updated
530 540 keyword substitutions.
531 541 Monkeypatches patch and webcommands.'''
532 542
533 543 try:
534 544 if (not repo.local() or kwtools['hgcmd'] in nokwcommands.split()
535 545 or '.hg' in util.splitpath(repo.root)
536 546 or repo._url.startswith('bundle:')):
537 547 return
538 548 except AttributeError:
539 549 pass
540 550
541 551 inc, exc = [], ['.hg*']
542 552 for pat, opt in ui.configitems('keyword'):
543 553 if opt != 'ignore':
544 554 inc.append(pat)
545 555 else:
546 556 exc.append(pat)
547 557 if not inc:
548 558 return
549 559
550 560 kwtools['templater'] = kwt = kwtemplater(ui, repo, inc, exc)
551 561
552 562 class kwrepo(repo.__class__):
553 563 def file(self, f):
554 564 if f[0] == '/':
555 565 f = f[1:]
556 566 return kwfilelog(self.sopener, kwt, f)
557 567
558 568 def wread(self, filename):
559 569 data = super(kwrepo, self).wread(filename)
560 570 return kwt.wread(filename, data)
561 571
562 572 def commit(self, *args, **opts):
563 573 # use custom commitctx for user commands
564 574 # other extensions can still wrap repo.commitctx directly
565 575 self.commitctx = self.kwcommitctx
566 576 try:
567 577 return super(kwrepo, self).commit(*args, **opts)
568 578 finally:
569 579 del self.commitctx
570 580
571 581 def kwcommitctx(self, ctx, error=False):
572 582 n = super(kwrepo, self).commitctx(ctx, error)
573 583 # no lock needed, only called from repo.commit() which already locks
574 584 if not kwt.record:
575 585 restrict = kwt.restrict
576 586 kwt.restrict = True
577 587 kwt.overwrite(self[n], sorted(ctx.added() + ctx.modified()),
578 588 False, True)
579 589 kwt.restrict = restrict
580 590 return n
581 591
582 592 def rollback(self, dryrun=False):
583 593 wlock = self.wlock()
584 594 try:
585 595 if not dryrun:
586 596 changed = self['.'].files()
587 597 ret = super(kwrepo, self).rollback(dryrun)
588 598 if not dryrun:
589 599 ctx = self['.']
590 600 modified, added = _preselect(self[None].status(), changed)
591 601 kwt.overwrite(ctx, modified, True, True)
592 602 kwt.overwrite(ctx, added, True, False)
593 603 return ret
594 604 finally:
595 605 wlock.release()
596 606
597 607 # monkeypatches
598 608 def kwpatchfile_init(orig, self, ui, gp, backend, store, eolmode=None):
599 609 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
600 610 rejects or conflicts due to expanded keywords in working dir.'''
601 611 orig(self, ui, gp, backend, store, eolmode)
602 612 # shrink keywords read from working dir
603 613 self.lines = kwt.shrinklines(self.fname, self.lines)
604 614
605 615 def kw_diff(orig, repo, node1=None, node2=None, match=None, changes=None,
606 616 opts=None, prefix=''):
607 617 '''Monkeypatch patch.diff to avoid expansion.'''
608 618 kwt.restrict = True
609 619 return orig(repo, node1, node2, match, changes, opts, prefix)
610 620
611 621 def kwweb_skip(orig, web, req, tmpl):
612 622 '''Wraps webcommands.x turning off keyword expansion.'''
613 623 kwt.match = util.never
614 624 return orig(web, req, tmpl)
615 625
616 626 def kw_copy(orig, ui, repo, pats, opts, rename=False):
617 627 '''Wraps cmdutil.copy so that copy/rename destinations do not
618 628 contain expanded keywords.
619 629 Note that the source of a regular file destination may also be a
620 630 symlink:
621 631 hg cp sym x -> x is symlink
622 632 cp sym x; hg cp -A sym x -> x is file (maybe expanded keywords)
623 633 For the latter we have to follow the symlink to find out whether its
624 634 target is configured for expansion and we therefore must unexpand the
625 635 keywords in the destination.'''
626 636 orig(ui, repo, pats, opts, rename)
627 637 if opts.get('dry_run'):
628 638 return
629 639 wctx = repo[None]
630 640 cwd = repo.getcwd()
631 641
632 642 def haskwsource(dest):
633 643 '''Returns true if dest is a regular file and configured for
634 644 expansion or a symlink which points to a file configured for
635 645 expansion. '''
636 646 source = repo.dirstate.copied(dest)
637 647 if 'l' in wctx.flags(source):
638 648 source = scmutil.canonpath(repo.root, cwd,
639 649 os.path.realpath(source))
640 650 return kwt.match(source)
641 651
642 652 candidates = [f for f in repo.dirstate.copies() if
643 653 not 'l' in wctx.flags(f) and haskwsource(f)]
644 654 kwt.overwrite(wctx, candidates, False, False)
645 655
646 656 def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts):
647 657 '''Wraps record.dorecord expanding keywords after recording.'''
648 658 wlock = repo.wlock()
649 659 try:
650 660 # record returns 0 even when nothing has changed
651 661 # therefore compare nodes before and after
652 662 kwt.record = True
653 663 ctx = repo['.']
654 664 wstatus = repo[None].status()
655 665 ret = orig(ui, repo, commitfunc, *pats, **opts)
656 666 recctx = repo['.']
657 667 if ctx != recctx:
658 668 modified, added = _preselect(wstatus, recctx.files())
659 669 kwt.restrict = False
660 670 kwt.overwrite(recctx, modified, False, True)
661 671 kwt.overwrite(recctx, added, False, True, True)
662 672 kwt.restrict = True
663 673 return ret
664 674 finally:
665 675 wlock.release()
666 676
667 677 def kwfilectx_cmp(orig, self, fctx):
668 678 # keyword affects data size, comparing wdir and filelog size does
669 679 # not make sense
670 680 if (fctx._filerev is None and
671 681 (self._repo._encodefilterpats or
672 682 kwt.match(fctx.path()) and not 'l' in fctx.flags()) or
673 683 self.size() == fctx.size()):
674 684 return self._filelog.cmp(self._filenode, fctx.data())
675 685 return True
676 686
677 687 extensions.wrapfunction(context.filectx, 'cmp', kwfilectx_cmp)
678 688 extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init)
679 689 extensions.wrapfunction(patch, 'diff', kw_diff)
680 690 extensions.wrapfunction(cmdutil, 'copy', kw_copy)
681 691 for c in 'annotate changeset rev filediff diff'.split():
682 692 extensions.wrapfunction(webcommands, c, kwweb_skip)
683 693 for name in recordextensions.split():
684 694 try:
685 695 record = extensions.find(name)
686 696 extensions.wrapfunction(record, 'dorecord', kw_dorecord)
687 697 except KeyError:
688 698 pass
689 699
690 700 repo.__class__ = kwrepo
General Comments 0
You need to be logged in to leave comments. Login now