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