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