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