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