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