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