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