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