##// END OF EJS Templates
dirstate: explicitly write the dirstate after `keyword` "overwrite"...
marmoute -
r50893:44528f4a default
parent child Browse files
Show More
@@ -1,894 +1,895
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 repo.dirstate.write(repo.currenttransaction())
445 446
446 447
447 448 @command(
448 449 b'kwdemo',
449 450 [
450 451 (b'd', b'default', None, _(b'show default keyword template maps')),
451 452 (b'f', b'rcfile', b'', _(b'read maps from rcfile'), _(b'FILE')),
452 453 ],
453 454 _(b'hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...'),
454 455 optionalrepo=True,
455 456 )
456 457 def demo(ui, repo, *args, **opts):
457 458 """print [keywordmaps] configuration and an expansion example
458 459
459 460 Show current, custom, or default keyword template maps and their
460 461 expansions.
461 462
462 463 Extend the current configuration by specifying maps as arguments
463 464 and using -f/--rcfile to source an external hgrc file.
464 465
465 466 Use -d/--default to disable current configuration.
466 467
467 468 See :hg:`help templates` for information on templates and filters.
468 469 """
469 470
470 471 def demoitems(section, items):
471 472 ui.write(b'[%s]\n' % section)
472 473 for k, v in sorted(items):
473 474 if isinstance(v, bool):
474 475 v = stringutil.pprint(v)
475 476 ui.write(b'%s = %s\n' % (k, v))
476 477
477 478 fn = b'demo.txt'
478 479 tmpdir = pycompat.mkdtemp(b'', b'kwdemo.')
479 480 ui.note(_(b'creating temporary repository at %s\n') % tmpdir)
480 481 if repo is None:
481 482 baseui = ui
482 483 else:
483 484 baseui = repo.baseui
484 485 repo = localrepo.instance(baseui, tmpdir, create=True)
485 486 ui.setconfig(b'keyword', fn, b'', b'keyword')
486 487 svn = ui.configbool(b'keywordset', b'svn')
487 488 # explicitly set keywordset for demo output
488 489 ui.setconfig(b'keywordset', b'svn', svn, b'keyword')
489 490
490 491 uikwmaps = ui.configitems(b'keywordmaps')
491 492 if args or opts.get('rcfile'):
492 493 ui.status(_(b'\n\tconfiguration using custom keyword template maps\n'))
493 494 if uikwmaps:
494 495 ui.status(_(b'\textending current template maps\n'))
495 496 if opts.get('default') or not uikwmaps:
496 497 if svn:
497 498 ui.status(_(b'\toverriding default svn keywordset\n'))
498 499 else:
499 500 ui.status(_(b'\toverriding default cvs keywordset\n'))
500 501 if opts.get('rcfile'):
501 502 ui.readconfig(opts.get(b'rcfile'))
502 503 if args:
503 504 # simulate hgrc parsing
504 505 rcmaps = b'[keywordmaps]\n%s\n' % b'\n'.join(args)
505 506 repo.vfs.write(b'hgrc', rcmaps)
506 507 ui.readconfig(repo.vfs.join(b'hgrc'))
507 508 kwmaps = dict(ui.configitems(b'keywordmaps'))
508 509 elif opts.get('default'):
509 510 if svn:
510 511 ui.status(_(b'\n\tconfiguration using default svn keywordset\n'))
511 512 else:
512 513 ui.status(_(b'\n\tconfiguration using default cvs keywordset\n'))
513 514 kwmaps = _defaultkwmaps(ui)
514 515 if uikwmaps:
515 516 ui.status(_(b'\tdisabling current template maps\n'))
516 517 for k, v in kwmaps.items():
517 518 ui.setconfig(b'keywordmaps', k, v, b'keyword')
518 519 else:
519 520 ui.status(_(b'\n\tconfiguration using current keyword template maps\n'))
520 521 if uikwmaps:
521 522 kwmaps = dict(uikwmaps)
522 523 else:
523 524 kwmaps = _defaultkwmaps(ui)
524 525
525 526 uisetup(ui)
526 527 reposetup(ui, repo)
527 528 ui.writenoi18n(b'[extensions]\nkeyword =\n')
528 529 demoitems(b'keyword', ui.configitems(b'keyword'))
529 530 demoitems(b'keywordset', ui.configitems(b'keywordset'))
530 531 demoitems(b'keywordmaps', kwmaps.items())
531 532 keywords = b'$' + b'$\n$'.join(sorted(kwmaps.keys())) + b'$\n'
532 533 repo.wvfs.write(fn, keywords)
533 534 repo[None].add([fn])
534 535 ui.note(_(b'\nkeywords written to %s:\n') % fn)
535 536 ui.note(keywords)
536 537 with repo.wlock():
537 538 repo.dirstate.setbranch(b'demobranch')
538 539 for name, cmd in ui.configitems(b'hooks'):
539 540 if name.split(b'.', 1)[0].find(b'commit') > -1:
540 541 repo.ui.setconfig(b'hooks', name, b'', b'keyword')
541 542 msg = _(b'hg keyword configuration and expansion example')
542 543 ui.note((b"hg ci -m '%s'\n" % msg))
543 544 repo.commit(text=msg)
544 545 ui.status(_(b'\n\tkeywords expanded\n'))
545 546 ui.write(repo.wread(fn))
546 547 repo.wvfs.rmtree(repo.root)
547 548
548 549
549 550 @command(
550 551 b'kwexpand',
551 552 cmdutil.walkopts,
552 553 _(b'hg kwexpand [OPTION]... [FILE]...'),
553 554 inferrepo=True,
554 555 )
555 556 def expand(ui, repo, *pats, **opts):
556 557 """expand keywords in the working directory
557 558
558 559 Run after (re)enabling keyword expansion.
559 560
560 561 kwexpand refuses to run if given files contain local changes.
561 562 """
562 563 # 3rd argument sets expansion to True
563 564 _kwfwrite(ui, repo, True, *pats, **opts)
564 565
565 566
566 567 @command(
567 568 b'kwfiles',
568 569 [
569 570 (b'A', b'all', None, _(b'show keyword status flags of all files')),
570 571 (b'i', b'ignore', None, _(b'show files excluded from expansion')),
571 572 (b'u', b'unknown', None, _(b'only show unknown (not tracked) files')),
572 573 ]
573 574 + cmdutil.walkopts,
574 575 _(b'hg kwfiles [OPTION]... [FILE]...'),
575 576 inferrepo=True,
576 577 )
577 578 def files(ui, repo, *pats, **opts):
578 579 """show files configured for keyword expansion
579 580
580 581 List which files in the working directory are matched by the
581 582 [keyword] configuration patterns.
582 583
583 584 Useful to prevent inadvertent keyword expansion and to speed up
584 585 execution by including only files that are actual candidates for
585 586 expansion.
586 587
587 588 See :hg:`help keyword` on how to construct patterns both for
588 589 inclusion and exclusion of files.
589 590
590 591 With -A/--all and -v/--verbose the codes used to show the status
591 592 of files are::
592 593
593 594 K = keyword expansion candidate
594 595 k = keyword expansion candidate (not tracked)
595 596 I = ignored
596 597 i = ignored (not tracked)
597 598 """
598 599 kwt = getattr(repo, '_keywordkwt', None)
599 600 wctx = repo[None]
600 601 status = _status(ui, repo, wctx, kwt, *pats, **opts)
601 602 if pats:
602 603 cwd = repo.getcwd()
603 604 else:
604 605 cwd = b''
605 606 files = []
606 607 opts = pycompat.byteskwargs(opts)
607 608 if not opts.get(b'unknown') or opts.get(b'all'):
608 609 files = sorted(status.modified + status.added + status.clean)
609 610 kwfiles = kwt.iskwfile(files, wctx)
610 611 kwdeleted = kwt.iskwfile(status.deleted, wctx)
611 612 kwunknown = kwt.iskwfile(status.unknown, wctx)
612 613 if not opts.get(b'ignore') or opts.get(b'all'):
613 614 showfiles = kwfiles, kwdeleted, kwunknown
614 615 else:
615 616 showfiles = [], [], []
616 617 if opts.get(b'all') or opts.get(b'ignore'):
617 618 showfiles += (
618 619 [f for f in files if f not in kwfiles],
619 620 [f for f in status.unknown if f not in kwunknown],
620 621 )
621 622 kwlabels = b'enabled deleted enabledunknown ignored ignoredunknown'.split()
622 623 kwstates = zip(kwlabels, pycompat.bytestr(b'K!kIi'), showfiles)
623 624 fm = ui.formatter(b'kwfiles', opts)
624 625 fmt = b'%.0s%s\n'
625 626 if opts.get(b'all') or ui.verbose:
626 627 fmt = b'%s %s\n'
627 628 for kwstate, char, filenames in kwstates:
628 629 label = b'kwfiles.' + kwstate
629 630 for f in filenames:
630 631 fm.startitem()
631 632 fm.data(kwstatus=char, path=f)
632 633 fm.plain(fmt % (char, repo.pathto(f, cwd)), label=label)
633 634 fm.end()
634 635
635 636
636 637 @command(
637 638 b'kwshrink',
638 639 cmdutil.walkopts,
639 640 _(b'hg kwshrink [OPTION]... [FILE]...'),
640 641 inferrepo=True,
641 642 )
642 643 def shrink(ui, repo, *pats, **opts):
643 644 """revert expanded keywords in the working directory
644 645
645 646 Must be run before changing/disabling active keywords.
646 647
647 648 kwshrink refuses to run if given files contain local changes.
648 649 """
649 650 # 3rd argument sets expansion to False
650 651 _kwfwrite(ui, repo, False, *pats, **opts)
651 652
652 653
653 654 # monkeypatches
654 655
655 656
656 657 def kwpatchfile_init(orig, self, ui, gp, backend, store, eolmode=None):
657 658 """Monkeypatch/wrap patch.patchfile.__init__ to avoid
658 659 rejects or conflicts due to expanded keywords in working dir."""
659 660 orig(self, ui, gp, backend, store, eolmode)
660 661 kwt = getattr(getattr(backend, 'repo', None), '_keywordkwt', None)
661 662 if kwt:
662 663 # shrink keywords read from working dir
663 664 self.lines = kwt.shrinklines(self.fname, self.lines)
664 665
665 666
666 667 def kwdiff(orig, repo, *args, **kwargs):
667 668 '''Monkeypatch patch.diff to avoid expansion.'''
668 669 kwt = getattr(repo, '_keywordkwt', None)
669 670 if kwt:
670 671 restrict = kwt.restrict
671 672 kwt.restrict = True
672 673 try:
673 674 for chunk in orig(repo, *args, **kwargs):
674 675 yield chunk
675 676 finally:
676 677 if kwt:
677 678 kwt.restrict = restrict
678 679
679 680
680 681 def kwweb_skip(orig, web):
681 682 '''Wraps webcommands.x turning off keyword expansion.'''
682 683 kwt = getattr(web.repo, '_keywordkwt', None)
683 684 if kwt:
684 685 origmatch = kwt.match
685 686 kwt.match = util.never
686 687 try:
687 688 for chunk in orig(web):
688 689 yield chunk
689 690 finally:
690 691 if kwt:
691 692 kwt.match = origmatch
692 693
693 694
694 695 def kw_amend(orig, ui, repo, old, extra, pats, opts):
695 696 '''Wraps cmdutil.amend expanding keywords after amend.'''
696 697 kwt = getattr(repo, '_keywordkwt', None)
697 698 if kwt is None:
698 699 return orig(ui, repo, old, extra, pats, opts)
699 700 with repo.wlock(), repo.dirstate.changing_parents(repo):
700 701 kwt.postcommit = True
701 702 newid = orig(ui, repo, old, extra, pats, opts)
702 703 if newid != old.node():
703 704 ctx = repo[newid]
704 705 kwt.restrict = True
705 706 kwt.overwrite(ctx, ctx.files(), False, True)
706 707 kwt.restrict = False
707 708 return newid
708 709
709 710
710 711 def kw_copy(orig, ui, repo, pats, opts, rename=False):
711 712 """Wraps cmdutil.copy so that copy/rename destinations do not
712 713 contain expanded keywords.
713 714 Note that the source of a regular file destination may also be a
714 715 symlink:
715 716 hg cp sym x -> x is symlink
716 717 cp sym x; hg cp -A sym x -> x is file (maybe expanded keywords)
717 718 For the latter we have to follow the symlink to find out whether its
718 719 target is configured for expansion and we therefore must unexpand the
719 720 keywords in the destination."""
720 721 kwt = getattr(repo, '_keywordkwt', None)
721 722 if kwt is None:
722 723 return orig(ui, repo, pats, opts, rename)
723 724 with repo.wlock():
724 725 orig(ui, repo, pats, opts, rename)
725 726 if opts.get(b'dry_run'):
726 727 return
727 728 wctx = repo[None]
728 729 cwd = repo.getcwd()
729 730
730 731 def haskwsource(dest):
731 732 """Returns true if dest is a regular file and configured for
732 733 expansion or a symlink which points to a file configured for
733 734 expansion."""
734 735 source = repo.dirstate.copied(dest)
735 736 if b'l' in wctx.flags(source):
736 737 source = pathutil.canonpath(
737 738 repo.root, cwd, os.path.realpath(source)
738 739 )
739 740 return kwt.match(source)
740 741
741 742 candidates = [
742 743 f
743 744 for f in repo.dirstate.copies()
744 745 if b'l' not in wctx.flags(f) and haskwsource(f)
745 746 ]
746 747 kwt.overwrite(wctx, candidates, False, False)
747 748
748 749
749 750 def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts):
750 751 '''Wraps record.dorecord expanding keywords after recording.'''
751 752 kwt = getattr(repo, '_keywordkwt', None)
752 753 if kwt is None:
753 754 return orig(ui, repo, commitfunc, *pats, **opts)
754 755 with repo.wlock():
755 756 # record returns 0 even when nothing has changed
756 757 # therefore compare nodes before and after
757 758 kwt.postcommit = True
758 759 ctx = repo[b'.']
759 760 wstatus = ctx.status()
760 761 ret = orig(ui, repo, commitfunc, *pats, **opts)
761 762 recctx = repo[b'.']
762 763 if ctx != recctx:
763 764 modified, added = _preselect(wstatus, recctx.files())
764 765 kwt.restrict = False
765 766 with repo.dirstate.changing_parents(repo):
766 767 kwt.overwrite(recctx, modified, False, True)
767 768 kwt.overwrite(recctx, added, False, True, True)
768 769 kwt.restrict = True
769 770 return ret
770 771
771 772
772 773 def kwfilectx_cmp(orig, self, fctx):
773 774 if fctx._customcmp:
774 775 return fctx.cmp(self)
775 776 kwt = getattr(self._repo, '_keywordkwt', None)
776 777 if kwt is None:
777 778 return orig(self, fctx)
778 779 # keyword affects data size, comparing wdir and filelog size does
779 780 # not make sense
780 781 if (
781 782 fctx._filenode is None
782 783 and (
783 784 self._repo._encodefilterpats
784 785 or kwt.match(fctx.path())
785 786 and b'l' not in fctx.flags()
786 787 or self.size() - 4 == fctx.size()
787 788 )
788 789 or self.size() == fctx.size()
789 790 ):
790 791 return self._filelog.cmp(self._filenode, fctx.data())
791 792 return True
792 793
793 794
794 795 def uisetup(ui):
795 796 """Monkeypatches dispatch._parse to retrieve user command.
796 797 Overrides file method to return kwfilelog instead of filelog
797 798 if file matches user configuration.
798 799 Wraps commit to overwrite configured files with updated
799 800 keyword substitutions.
800 801 Monkeypatches patch and webcommands."""
801 802
802 803 def kwdispatch_parse(orig, ui, args):
803 804 '''Monkeypatch dispatch._parse to obtain running hg command.'''
804 805 cmd, func, args, options, cmdoptions = orig(ui, args)
805 806 kwtools[b'hgcmd'] = cmd
806 807 return cmd, func, args, options, cmdoptions
807 808
808 809 extensions.wrapfunction(dispatch, b'_parse', kwdispatch_parse)
809 810
810 811 extensions.wrapfunction(context.filectx, b'cmp', kwfilectx_cmp)
811 812 extensions.wrapfunction(patch.patchfile, b'__init__', kwpatchfile_init)
812 813 extensions.wrapfunction(patch, b'diff', kwdiff)
813 814 extensions.wrapfunction(cmdutil, b'amend', kw_amend)
814 815 extensions.wrapfunction(cmdutil, b'copy', kw_copy)
815 816 extensions.wrapfunction(cmdutil, b'dorecord', kw_dorecord)
816 817 for c in nokwwebcommands.split():
817 818 extensions.wrapfunction(webcommands, c, kwweb_skip)
818 819
819 820
820 821 def reposetup(ui, repo):
821 822 '''Sets up repo as kwrepo for keyword substitution.'''
822 823
823 824 try:
824 825 if (
825 826 not repo.local()
826 827 or kwtools[b'hgcmd'] in nokwcommands.split()
827 828 or b'.hg' in util.splitpath(repo.root)
828 829 or repo._url.startswith(b'bundle:')
829 830 ):
830 831 return
831 832 except AttributeError:
832 833 pass
833 834
834 835 inc, exc = [], [b'.hg*']
835 836 for pat, opt in ui.configitems(b'keyword'):
836 837 if opt != b'ignore':
837 838 inc.append(pat)
838 839 else:
839 840 exc.append(pat)
840 841 if not inc:
841 842 return
842 843
843 844 kwt = kwtemplater(ui, repo, inc, exc)
844 845
845 846 class kwrepo(repo.__class__):
846 847 def file(self, f):
847 848 if f[0] == b'/':
848 849 f = f[1:]
849 850 return kwfilelog(self.svfs, kwt, f)
850 851
851 852 def wread(self, filename):
852 853 data = super(kwrepo, self).wread(filename)
853 854 return kwt.wread(filename, data)
854 855
855 856 def commit(self, *args, **opts):
856 857 # use custom commitctx for user commands
857 858 # other extensions can still wrap repo.commitctx directly
858 859 self.commitctx = self.kwcommitctx
859 860 try:
860 861 return super(kwrepo, self).commit(*args, **opts)
861 862 finally:
862 863 del self.commitctx
863 864
864 865 def kwcommitctx(self, ctx, error=False, origctx=None):
865 866 n = super(kwrepo, self).commitctx(ctx, error, origctx)
866 867 # no lock needed, only called from repo.commit() which already locks
867 868 if not kwt.postcommit:
868 869 restrict = kwt.restrict
869 870 kwt.restrict = True
870 871 kwt.overwrite(
871 872 self[n], sorted(ctx.added() + ctx.modified()), False, True
872 873 )
873 874 kwt.restrict = restrict
874 875 return n
875 876
876 877 def rollback(self, dryrun=False, force=False):
877 878 with self.wlock():
878 879 origrestrict = kwt.restrict
879 880 try:
880 881 if not dryrun:
881 882 changed = self[b'.'].files()
882 883 ret = super(kwrepo, self).rollback(dryrun, force)
883 884 if not dryrun:
884 885 ctx = self[b'.']
885 886 modified, added = _preselect(ctx.status(), changed)
886 887 kwt.restrict = False
887 888 kwt.overwrite(ctx, modified, True, True)
888 889 kwt.overwrite(ctx, added, True, False)
889 890 return ret
890 891 finally:
891 892 kwt.restrict = origrestrict
892 893
893 894 repo.__class__ = kwrepo
894 895 repo._keywordkwt = kwt
General Comments 0
You need to be logged in to leave comments. Login now