##// END OF EJS Templates
keyword: use wopener(..., atomictemp=True) to overwrite
Christian Ebert -
r15083:6e069366 default
parent child Browse files
Show More
@@ -1,703 +1,701 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 252 if self.restrict or rekw:
253 253 re_kw = self.rekw
254 254 else:
255 255 re_kw = self.rekwexp
256 256 if expand:
257 257 msg = _('overwriting %s expanding keywords\n')
258 258 else:
259 259 msg = _('overwriting %s shrinking keywords\n')
260 lctx = ctx
261 260 for f in candidates:
262 261 if self.restrict:
263 262 data = self.repo.file(f).read(mf[f])
264 263 else:
265 264 data = self.repo.wread(f)
266 265 if util.binary(data):
267 266 continue
268 267 if expand:
269 268 if lookup:
270 lctx = self.linkctx(f, mf[f])
271 data, found = self.substitute(data, f, lctx, re_kw.subn)
269 ctx = self.linkctx(f, mf[f])
270 data, found = self.substitute(data, f, ctx, re_kw.subn)
272 271 elif self.restrict:
273 272 found = re_kw.search(data)
274 273 else:
275 274 data, found = _shrinktext(data, re_kw.subn)
276 275 if found:
277 276 self.ui.note(msg % f)
278 fpath = self.repo.wjoin(f)
279 mode = os.lstat(fpath).st_mode
280 self.repo.wwrite(f, data, ctx.flags(f))
281 os.chmod(fpath, mode)
277 fp = self.repo.wopener(f, "wb", atomictemp=True)
278 fp.write(data)
279 fp.close()
282 280 if kwcmd:
283 281 self.repo.dirstate.normal(f)
284 282 elif self.record:
285 283 self.repo.dirstate.normallookup(f)
286 284
287 285 def shrink(self, fname, text):
288 286 '''Returns text with all keyword substitutions removed.'''
289 287 if self.match(fname) and not util.binary(text):
290 288 return _shrinktext(text, self.rekwexp.sub)
291 289 return text
292 290
293 291 def shrinklines(self, fname, lines):
294 292 '''Returns lines with keyword substitutions removed.'''
295 293 if self.match(fname):
296 294 text = ''.join(lines)
297 295 if not util.binary(text):
298 296 return _shrinktext(text, self.rekwexp.sub).splitlines(True)
299 297 return lines
300 298
301 299 def wread(self, fname, data):
302 300 '''If in restricted mode returns data read from wdir with
303 301 keyword substitutions removed.'''
304 302 if self.restrict:
305 303 return self.shrink(fname, data)
306 304 return data
307 305
308 306 class kwfilelog(filelog.filelog):
309 307 '''
310 308 Subclass of filelog to hook into its read, add, cmp methods.
311 309 Keywords are "stored" unexpanded, and processed on reading.
312 310 '''
313 311 def __init__(self, opener, kwt, path):
314 312 super(kwfilelog, self).__init__(opener, path)
315 313 self.kwt = kwt
316 314 self.path = path
317 315
318 316 def read(self, node):
319 317 '''Expands keywords when reading filelog.'''
320 318 data = super(kwfilelog, self).read(node)
321 319 if self.renamed(node):
322 320 return data
323 321 return self.kwt.expand(self.path, node, data)
324 322
325 323 def add(self, text, meta, tr, link, p1=None, p2=None):
326 324 '''Removes keyword substitutions when adding to filelog.'''
327 325 text = self.kwt.shrink(self.path, text)
328 326 return super(kwfilelog, self).add(text, meta, tr, link, p1, p2)
329 327
330 328 def cmp(self, node, text):
331 329 '''Removes keyword substitutions for comparison.'''
332 330 text = self.kwt.shrink(self.path, text)
333 331 return super(kwfilelog, self).cmp(node, text)
334 332
335 333 def _status(ui, repo, wctx, kwt, *pats, **opts):
336 334 '''Bails out if [keyword] configuration is not active.
337 335 Returns status of working directory.'''
338 336 if kwt:
339 337 return repo.status(match=scmutil.match(wctx, pats, opts), clean=True,
340 338 unknown=opts.get('unknown') or opts.get('all'))
341 339 if ui.configitems('keyword'):
342 340 raise util.Abort(_('[keyword] patterns cannot match'))
343 341 raise util.Abort(_('no [keyword] patterns configured'))
344 342
345 343 def _kwfwrite(ui, repo, expand, *pats, **opts):
346 344 '''Selects files and passes them to kwtemplater.overwrite.'''
347 345 wctx = repo[None]
348 346 if len(wctx.parents()) > 1:
349 347 raise util.Abort(_('outstanding uncommitted merge'))
350 348 kwt = kwtools['templater']
351 349 wlock = repo.wlock()
352 350 try:
353 351 status = _status(ui, repo, wctx, kwt, *pats, **opts)
354 352 modified, added, removed, deleted, unknown, ignored, clean = status
355 353 if modified or added or removed or deleted:
356 354 raise util.Abort(_('outstanding uncommitted changes'))
357 355 kwt.overwrite(wctx, clean, True, expand)
358 356 finally:
359 357 wlock.release()
360 358
361 359 @command('kwdemo',
362 360 [('d', 'default', None, _('show default keyword template maps')),
363 361 ('f', 'rcfile', '',
364 362 _('read maps from rcfile'), _('FILE'))],
365 363 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...'))
366 364 def demo(ui, repo, *args, **opts):
367 365 '''print [keywordmaps] configuration and an expansion example
368 366
369 367 Show current, custom, or default keyword template maps and their
370 368 expansions.
371 369
372 370 Extend the current configuration by specifying maps as arguments
373 371 and using -f/--rcfile to source an external hgrc file.
374 372
375 373 Use -d/--default to disable current configuration.
376 374
377 375 See :hg:`help templates` for information on templates and filters.
378 376 '''
379 377 def demoitems(section, items):
380 378 ui.write('[%s]\n' % section)
381 379 for k, v in sorted(items):
382 380 ui.write('%s = %s\n' % (k, v))
383 381
384 382 fn = 'demo.txt'
385 383 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
386 384 ui.note(_('creating temporary repository at %s\n') % tmpdir)
387 385 repo = localrepo.localrepository(ui, tmpdir, True)
388 386 ui.setconfig('keyword', fn, '')
389 387 svn = ui.configbool('keywordset', 'svn')
390 388 # explicitly set keywordset for demo output
391 389 ui.setconfig('keywordset', 'svn', svn)
392 390
393 391 uikwmaps = ui.configitems('keywordmaps')
394 392 if args or opts.get('rcfile'):
395 393 ui.status(_('\n\tconfiguration using custom keyword template maps\n'))
396 394 if uikwmaps:
397 395 ui.status(_('\textending current template maps\n'))
398 396 if opts.get('default') or not uikwmaps:
399 397 if svn:
400 398 ui.status(_('\toverriding default svn keywordset\n'))
401 399 else:
402 400 ui.status(_('\toverriding default cvs keywordset\n'))
403 401 if opts.get('rcfile'):
404 402 ui.readconfig(opts.get('rcfile'))
405 403 if args:
406 404 # simulate hgrc parsing
407 405 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
408 406 fp = repo.opener('hgrc', 'w')
409 407 fp.writelines(rcmaps)
410 408 fp.close()
411 409 ui.readconfig(repo.join('hgrc'))
412 410 kwmaps = dict(ui.configitems('keywordmaps'))
413 411 elif opts.get('default'):
414 412 if svn:
415 413 ui.status(_('\n\tconfiguration using default svn keywordset\n'))
416 414 else:
417 415 ui.status(_('\n\tconfiguration using default cvs keywordset\n'))
418 416 kwmaps = _defaultkwmaps(ui)
419 417 if uikwmaps:
420 418 ui.status(_('\tdisabling current template maps\n'))
421 419 for k, v in kwmaps.iteritems():
422 420 ui.setconfig('keywordmaps', k, v)
423 421 else:
424 422 ui.status(_('\n\tconfiguration using current keyword template maps\n'))
425 423 if uikwmaps:
426 424 kwmaps = dict(uikwmaps)
427 425 else:
428 426 kwmaps = _defaultkwmaps(ui)
429 427
430 428 uisetup(ui)
431 429 reposetup(ui, repo)
432 430 ui.write('[extensions]\nkeyword =\n')
433 431 demoitems('keyword', ui.configitems('keyword'))
434 432 demoitems('keywordset', ui.configitems('keywordset'))
435 433 demoitems('keywordmaps', kwmaps.iteritems())
436 434 keywords = '$' + '$\n$'.join(sorted(kwmaps.keys())) + '$\n'
437 435 repo.wopener.write(fn, keywords)
438 436 repo[None].add([fn])
439 437 ui.note(_('\nkeywords written to %s:\n') % fn)
440 438 ui.note(keywords)
441 439 repo.dirstate.setbranch('demobranch')
442 440 for name, cmd in ui.configitems('hooks'):
443 441 if name.split('.', 1)[0].find('commit') > -1:
444 442 repo.ui.setconfig('hooks', name, '')
445 443 msg = _('hg keyword configuration and expansion example')
446 444 ui.note("hg ci -m '%s'\n" % msg)
447 445 repo.commit(text=msg)
448 446 ui.status(_('\n\tkeywords expanded\n'))
449 447 ui.write(repo.wread(fn))
450 448 shutil.rmtree(tmpdir, ignore_errors=True)
451 449
452 450 @command('kwexpand', commands.walkopts, _('hg kwexpand [OPTION]... [FILE]...'))
453 451 def expand(ui, repo, *pats, **opts):
454 452 '''expand keywords in the working directory
455 453
456 454 Run after (re)enabling keyword expansion.
457 455
458 456 kwexpand refuses to run if given files contain local changes.
459 457 '''
460 458 # 3rd argument sets expansion to True
461 459 _kwfwrite(ui, repo, True, *pats, **opts)
462 460
463 461 @command('kwfiles',
464 462 [('A', 'all', None, _('show keyword status flags of all files')),
465 463 ('i', 'ignore', None, _('show files excluded from expansion')),
466 464 ('u', 'unknown', None, _('only show unknown (not tracked) files')),
467 465 ] + commands.walkopts,
468 466 _('hg kwfiles [OPTION]... [FILE]...'))
469 467 def files(ui, repo, *pats, **opts):
470 468 '''show files configured for keyword expansion
471 469
472 470 List which files in the working directory are matched by the
473 471 [keyword] configuration patterns.
474 472
475 473 Useful to prevent inadvertent keyword expansion and to speed up
476 474 execution by including only files that are actual candidates for
477 475 expansion.
478 476
479 477 See :hg:`help keyword` on how to construct patterns both for
480 478 inclusion and exclusion of files.
481 479
482 480 With -A/--all and -v/--verbose the codes used to show the status
483 481 of files are::
484 482
485 483 K = keyword expansion candidate
486 484 k = keyword expansion candidate (not tracked)
487 485 I = ignored
488 486 i = ignored (not tracked)
489 487 '''
490 488 kwt = kwtools['templater']
491 489 wctx = repo[None]
492 490 status = _status(ui, repo, wctx, kwt, *pats, **opts)
493 491 cwd = pats and repo.getcwd() or ''
494 492 modified, added, removed, deleted, unknown, ignored, clean = status
495 493 files = []
496 494 if not opts.get('unknown') or opts.get('all'):
497 495 files = sorted(modified + added + clean)
498 496 kwfiles = kwt.iskwfile(files, wctx)
499 497 kwdeleted = kwt.iskwfile(deleted, wctx)
500 498 kwunknown = kwt.iskwfile(unknown, wctx)
501 499 if not opts.get('ignore') or opts.get('all'):
502 500 showfiles = kwfiles, kwdeleted, kwunknown
503 501 else:
504 502 showfiles = [], [], []
505 503 if opts.get('all') or opts.get('ignore'):
506 504 showfiles += ([f for f in files if f not in kwfiles],
507 505 [f for f in unknown if f not in kwunknown])
508 506 kwlabels = 'enabled deleted enabledunknown ignored ignoredunknown'.split()
509 507 kwstates = zip('K!kIi', showfiles, kwlabels)
510 508 for char, filenames, kwstate in kwstates:
511 509 fmt = (opts.get('all') or ui.verbose) and '%s %%s\n' % char or '%s\n'
512 510 for f in filenames:
513 511 ui.write(fmt % repo.pathto(f, cwd), label='kwfiles.' + kwstate)
514 512
515 513 @command('kwshrink', commands.walkopts, _('hg kwshrink [OPTION]... [FILE]...'))
516 514 def shrink(ui, repo, *pats, **opts):
517 515 '''revert expanded keywords in the working directory
518 516
519 517 Must be run before changing/disabling active keywords.
520 518
521 519 kwshrink refuses to run if given files contain local changes.
522 520 '''
523 521 # 3rd argument sets expansion to False
524 522 _kwfwrite(ui, repo, False, *pats, **opts)
525 523
526 524
527 525 def uisetup(ui):
528 526 ''' Monkeypatches dispatch._parse to retrieve user command.'''
529 527
530 528 def kwdispatch_parse(orig, ui, args):
531 529 '''Monkeypatch dispatch._parse to obtain running hg command.'''
532 530 cmd, func, args, options, cmdoptions = orig(ui, args)
533 531 kwtools['hgcmd'] = cmd
534 532 return cmd, func, args, options, cmdoptions
535 533
536 534 extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse)
537 535
538 536 def reposetup(ui, repo):
539 537 '''Sets up repo as kwrepo for keyword substitution.
540 538 Overrides file method to return kwfilelog instead of filelog
541 539 if file matches user configuration.
542 540 Wraps commit to overwrite configured files with updated
543 541 keyword substitutions.
544 542 Monkeypatches patch and webcommands.'''
545 543
546 544 try:
547 545 if (not repo.local() or kwtools['hgcmd'] in nokwcommands.split()
548 546 or '.hg' in util.splitpath(repo.root)
549 547 or repo._url.startswith('bundle:')):
550 548 return
551 549 except AttributeError:
552 550 pass
553 551
554 552 inc, exc = [], ['.hg*']
555 553 for pat, opt in ui.configitems('keyword'):
556 554 if opt != 'ignore':
557 555 inc.append(pat)
558 556 else:
559 557 exc.append(pat)
560 558 if not inc:
561 559 return
562 560
563 561 kwtools['templater'] = kwt = kwtemplater(ui, repo, inc, exc)
564 562
565 563 class kwrepo(repo.__class__):
566 564 def file(self, f):
567 565 if f[0] == '/':
568 566 f = f[1:]
569 567 return kwfilelog(self.sopener, kwt, f)
570 568
571 569 def wread(self, filename):
572 570 data = super(kwrepo, self).wread(filename)
573 571 return kwt.wread(filename, data)
574 572
575 573 def commit(self, *args, **opts):
576 574 # use custom commitctx for user commands
577 575 # other extensions can still wrap repo.commitctx directly
578 576 self.commitctx = self.kwcommitctx
579 577 try:
580 578 return super(kwrepo, self).commit(*args, **opts)
581 579 finally:
582 580 del self.commitctx
583 581
584 582 def kwcommitctx(self, ctx, error=False):
585 583 n = super(kwrepo, self).commitctx(ctx, error)
586 584 # no lock needed, only called from repo.commit() which already locks
587 585 if not kwt.record:
588 586 restrict = kwt.restrict
589 587 kwt.restrict = True
590 588 kwt.overwrite(self[n], sorted(ctx.added() + ctx.modified()),
591 589 False, True)
592 590 kwt.restrict = restrict
593 591 return n
594 592
595 593 def rollback(self, dryrun=False):
596 594 wlock = self.wlock()
597 595 try:
598 596 if not dryrun:
599 597 changed = self['.'].files()
600 598 ret = super(kwrepo, self).rollback(dryrun)
601 599 if not dryrun:
602 600 ctx = self['.']
603 601 modified, added = _preselect(self[None].status(), changed)
604 602 kwt.overwrite(ctx, modified, True, True)
605 603 kwt.overwrite(ctx, added, True, False)
606 604 return ret
607 605 finally:
608 606 wlock.release()
609 607
610 608 # monkeypatches
611 609 def kwpatchfile_init(orig, self, ui, gp, backend, store, eolmode=None):
612 610 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
613 611 rejects or conflicts due to expanded keywords in working dir.'''
614 612 orig(self, ui, gp, backend, store, eolmode)
615 613 # shrink keywords read from working dir
616 614 self.lines = kwt.shrinklines(self.fname, self.lines)
617 615
618 616 def kw_diff(orig, repo, node1=None, node2=None, match=None, changes=None,
619 617 opts=None, prefix=''):
620 618 '''Monkeypatch patch.diff to avoid expansion.'''
621 619 kwt.restrict = True
622 620 return orig(repo, node1, node2, match, changes, opts, prefix)
623 621
624 622 def kwweb_skip(orig, web, req, tmpl):
625 623 '''Wraps webcommands.x turning off keyword expansion.'''
626 624 kwt.match = util.never
627 625 return orig(web, req, tmpl)
628 626
629 627 def kw_copy(orig, ui, repo, pats, opts, rename=False):
630 628 '''Wraps cmdutil.copy so that copy/rename destinations do not
631 629 contain expanded keywords.
632 630 Note that the source of a regular file destination may also be a
633 631 symlink:
634 632 hg cp sym x -> x is symlink
635 633 cp sym x; hg cp -A sym x -> x is file (maybe expanded keywords)
636 634 For the latter we have to follow the symlink to find out whether its
637 635 target is configured for expansion and we therefore must unexpand the
638 636 keywords in the destination.'''
639 637 orig(ui, repo, pats, opts, rename)
640 638 if opts.get('dry_run'):
641 639 return
642 640 wctx = repo[None]
643 641 cwd = repo.getcwd()
644 642
645 643 def haskwsource(dest):
646 644 '''Returns true if dest is a regular file and configured for
647 645 expansion or a symlink which points to a file configured for
648 646 expansion. '''
649 647 source = repo.dirstate.copied(dest)
650 648 if 'l' in wctx.flags(source):
651 649 source = scmutil.canonpath(repo.root, cwd,
652 650 os.path.realpath(source))
653 651 return kwt.match(source)
654 652
655 653 candidates = [f for f in repo.dirstate.copies() if
656 654 not 'l' in wctx.flags(f) and haskwsource(f)]
657 655 kwt.overwrite(wctx, candidates, False, False)
658 656
659 657 def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts):
660 658 '''Wraps record.dorecord expanding keywords after recording.'''
661 659 wlock = repo.wlock()
662 660 try:
663 661 # record returns 0 even when nothing has changed
664 662 # therefore compare nodes before and after
665 663 kwt.record = True
666 664 ctx = repo['.']
667 665 wstatus = repo[None].status()
668 666 ret = orig(ui, repo, commitfunc, *pats, **opts)
669 667 recctx = repo['.']
670 668 if ctx != recctx:
671 669 modified, added = _preselect(wstatus, recctx.files())
672 670 kwt.restrict = False
673 671 kwt.overwrite(recctx, modified, False, True)
674 672 kwt.overwrite(recctx, added, False, True, True)
675 673 kwt.restrict = True
676 674 return ret
677 675 finally:
678 676 wlock.release()
679 677
680 678 def kwfilectx_cmp(orig, self, fctx):
681 679 # keyword affects data size, comparing wdir and filelog size does
682 680 # not make sense
683 681 if (fctx._filerev is None and
684 682 (self._repo._encodefilterpats or
685 683 kwt.match(fctx.path()) and not 'l' in fctx.flags()) or
686 684 self.size() == fctx.size()):
687 685 return self._filelog.cmp(self._filenode, fctx.data())
688 686 return True
689 687
690 688 extensions.wrapfunction(context.filectx, 'cmp', kwfilectx_cmp)
691 689 extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init)
692 690 extensions.wrapfunction(patch, 'diff', kw_diff)
693 691 extensions.wrapfunction(cmdutil, 'copy', kw_copy)
694 692 for c in 'annotate changeset rev filediff diff'.split():
695 693 extensions.wrapfunction(webcommands, c, kwweb_skip)
696 694 for name in recordextensions.split():
697 695 try:
698 696 record = extensions.find(name)
699 697 extensions.wrapfunction(record, 'dorecord', kw_dorecord)
700 698 except KeyError:
701 699 pass
702 700
703 701 repo.__class__ = kwrepo
General Comments 0
You need to be logged in to leave comments. Login now