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