##// END OF EJS Templates
keyword: make wrapped repository and kwtemplater refer to each other...
FUJIWARA Katsunori -
r33067:5a51db8b default
parent child Browse files
Show More
@@ -1,771 +1,778 b''
1 1 # keyword.py - $Keyword$ expansion for Mercurial
2 2 #
3 3 # Copyright 2007-2015 Christian Ebert <blacktrash@gmx.net>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7 #
8 8 # $Id$
9 9 #
10 10 # Keyword expansion hack against the grain of a Distributed SCM
11 11 #
12 12 # There are many good reasons why this is not needed in a distributed
13 13 # SCM, still it may be useful in very small projects based on single
14 14 # files (like LaTeX packages), that are mostly addressed to an
15 15 # audience not running a version control system.
16 16 #
17 17 # For in-depth discussion refer to
18 18 # <https://mercurial-scm.org/wiki/KeywordPlan>.
19 19 #
20 20 # Keyword expansion is based on Mercurial's changeset template mappings.
21 21 #
22 22 # Binary files are not touched.
23 23 #
24 24 # Files to act upon/ignore are specified in the [keyword] section.
25 25 # Customized keyword template mappings in the [keywordmaps] section.
26 26 #
27 27 # Run 'hg help keyword' and 'hg kwdemo' to get info on configuration.
28 28
29 29 '''expand keywords in tracked files
30 30
31 31 This extension expands RCS/CVS-like or self-customized $Keywords$ in
32 32 tracked text files selected by your configuration.
33 33
34 34 Keywords are only expanded in local repositories and not stored in the
35 35 change history. The mechanism can be regarded as a convenience for the
36 36 current user or for archive distribution.
37 37
38 38 Keywords expand to the changeset data pertaining to the latest change
39 39 relative to the working directory parent of each file.
40 40
41 41 Configuration is done in the [keyword], [keywordset] and [keywordmaps]
42 42 sections of hgrc files.
43 43
44 44 Example::
45 45
46 46 [keyword]
47 47 # expand keywords in every python file except those matching "x*"
48 48 **.py =
49 49 x* = ignore
50 50
51 51 [keywordset]
52 52 # prefer svn- over cvs-like default keywordmaps
53 53 svn = True
54 54
55 55 .. note::
56 56
57 57 The more specific you are in your filename patterns the less you
58 58 lose speed in huge repositories.
59 59
60 60 For [keywordmaps] template mapping and expansion demonstration and
61 61 control run :hg:`kwdemo`. See :hg:`help templates` for a list of
62 62 available templates and filters.
63 63
64 64 Three additional date template filters are provided:
65 65
66 66 :``utcdate``: "2006/09/18 15:13:13"
67 67 :``svnutcdate``: "2006-09-18 15:13:13Z"
68 68 :``svnisodate``: "2006-09-18 08:13:13 -700 (Mon, 18 Sep 2006)"
69 69
70 70 The default template mappings (view with :hg:`kwdemo -d`) can be
71 71 replaced with customized keywords and templates. Again, run
72 72 :hg:`kwdemo` to control the results of your configuration changes.
73 73
74 74 Before changing/disabling active keywords, you must run :hg:`kwshrink`
75 75 to avoid storing expanded keywords in the change history.
76 76
77 77 To force expansion after enabling it, or a configuration change, run
78 78 :hg:`kwexpand`.
79 79
80 80 Expansions spanning more than one line and incremental expansions,
81 81 like CVS' $Log$, are not supported. A keyword template map "Log =
82 82 {desc}" expands to the first line of the changeset description.
83 83 '''
84 84
85 85
86 86 from __future__ import absolute_import
87 87
88 88 import os
89 89 import re
90 90 import tempfile
91 import weakref
91 92
92 93 from mercurial.i18n import _
93 94 from mercurial.hgweb import webcommands
94 95
95 96 from mercurial import (
96 97 cmdutil,
97 98 context,
98 99 dispatch,
99 100 error,
100 101 extensions,
101 102 filelog,
102 103 localrepo,
103 104 match,
104 105 patch,
105 106 pathutil,
106 107 registrar,
107 108 scmutil,
108 109 templatefilters,
109 110 util,
110 111 )
111 112
112 113 cmdtable = {}
113 114 command = registrar.command(cmdtable)
114 115 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
115 116 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
116 117 # be specifying the version(s) of Mercurial they are tested with, or
117 118 # leave the attribute unspecified.
118 119 testedwith = 'ships-with-hg-core'
119 120
120 121 # hg commands that do not act on keywords
121 122 nokwcommands = ('add addremove annotate bundle export grep incoming init log'
122 123 ' outgoing push tip verify convert email glog')
123 124
124 125 # webcommands that do not act on keywords
125 126 nokwwebcommands = ('annotate changeset rev filediff diff comparison')
126 127
127 128 # hg commands that trigger expansion only when writing to working dir,
128 129 # not when reading filelog, and unexpand when reading from working dir
129 130 restricted = ('merge kwexpand kwshrink record qrecord resolve transplant'
130 131 ' unshelve rebase graft backout histedit fetch')
131 132
132 133 # names of extensions using dorecord
133 134 recordextensions = 'record'
134 135
135 136 colortable = {
136 137 'kwfiles.enabled': 'green bold',
137 138 'kwfiles.deleted': 'cyan bold underline',
138 139 'kwfiles.enabledunknown': 'green',
139 140 'kwfiles.ignored': 'bold',
140 141 'kwfiles.ignoredunknown': 'none'
141 142 }
142 143
143 144 templatefilter = registrar.templatefilter()
144 145
145 146 # date like in cvs' $Date
146 147 @templatefilter('utcdate')
147 148 def utcdate(text):
148 149 '''Date. Returns a UTC-date in this format: "2009/08/18 11:00:13".
149 150 '''
150 151 return util.datestr((util.parsedate(text)[0], 0), '%Y/%m/%d %H:%M:%S')
151 152 # date like in svn's $Date
152 153 @templatefilter('svnisodate')
153 154 def svnisodate(text):
154 155 '''Date. Returns a date in this format: "2009-08-18 13:00:13
155 156 +0200 (Tue, 18 Aug 2009)".
156 157 '''
157 158 return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2 (%a, %d %b %Y)')
158 159 # date like in svn's $Id
159 160 @templatefilter('svnutcdate')
160 161 def svnutcdate(text):
161 162 '''Date. Returns a UTC-date in this format: "2009-08-18
162 163 11:00:13Z".
163 164 '''
164 165 return util.datestr((util.parsedate(text)[0], 0), '%Y-%m-%d %H:%M:%SZ')
165 166
166 167 # make keyword tools accessible
167 168 kwtools = {'templater': None, 'hgcmd': ''}
168 169
169 170 def _defaultkwmaps(ui):
170 171 '''Returns default keywordmaps according to keywordset configuration.'''
171 172 templates = {
172 173 'Revision': '{node|short}',
173 174 'Author': '{author|user}',
174 175 }
175 176 kwsets = ({
176 177 'Date': '{date|utcdate}',
177 178 'RCSfile': '{file|basename},v',
178 179 'RCSFile': '{file|basename},v', # kept for backwards compatibility
179 180 # with hg-keyword
180 181 'Source': '{root}/{file},v',
181 182 'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
182 183 'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
183 184 }, {
184 185 'Date': '{date|svnisodate}',
185 186 'Id': '{file|basename},v {node|short} {date|svnutcdate} {author|user}',
186 187 'LastChangedRevision': '{node|short}',
187 188 'LastChangedBy': '{author|user}',
188 189 'LastChangedDate': '{date|svnisodate}',
189 190 })
190 191 templates.update(kwsets[ui.configbool('keywordset', 'svn')])
191 192 return templates
192 193
193 194 def _shrinktext(text, subfunc):
194 195 '''Helper for keyword expansion removal in text.
195 196 Depending on subfunc also returns number of substitutions.'''
196 197 return subfunc(r'$\1$', text)
197 198
198 199 def _preselect(wstatus, changed):
199 200 '''Retrieves modified and added files from a working directory state
200 201 and returns the subset of each contained in given changed files
201 202 retrieved from a change context.'''
202 203 modified = [f for f in wstatus.modified if f in changed]
203 204 added = [f for f in wstatus.added if f in changed]
204 205 return modified, added
205 206
206 207
207 208 class kwtemplater(object):
208 209 '''
209 210 Sets up keyword templates, corresponding keyword regex, and
210 211 provides keyword substitution functions.
211 212 '''
212 213
213 214 def __init__(self, ui, repo, inc, exc):
214 215 self.ui = ui
215 self.repo = repo
216 self._repo = weakref.ref(repo)
216 217 self.match = match.match(repo.root, '', [], inc, exc)
217 218 self.restrict = kwtools['hgcmd'] in restricted.split()
218 219 self.postcommit = False
219 220
220 221 kwmaps = self.ui.configitems('keywordmaps')
221 222 if kwmaps: # override default templates
222 223 self.templates = dict(kwmaps)
223 224 else:
224 225 self.templates = _defaultkwmaps(self.ui)
225 226
227 @property
228 def repo(self):
229 return self._repo()
230
226 231 @util.propertycache
227 232 def escape(self):
228 233 '''Returns bar-separated and escaped keywords.'''
229 234 return '|'.join(map(re.escape, self.templates.keys()))
230 235
231 236 @util.propertycache
232 237 def rekw(self):
233 238 '''Returns regex for unexpanded keywords.'''
234 239 return re.compile(r'\$(%s)\$' % self.escape)
235 240
236 241 @util.propertycache
237 242 def rekwexp(self):
238 243 '''Returns regex for expanded keywords.'''
239 244 return re.compile(r'\$(%s): [^$\n\r]*? \$' % self.escape)
240 245
241 246 def substitute(self, data, path, ctx, subfunc):
242 247 '''Replaces keywords in data with expanded template.'''
243 248 def kwsub(mobj):
244 249 kw = mobj.group(1)
245 250 ct = cmdutil.makelogtemplater(self.ui, self.repo,
246 251 self.templates[kw])
247 252 self.ui.pushbuffer()
248 253 ct.show(ctx, root=self.repo.root, file=path)
249 254 ekw = templatefilters.firstline(self.ui.popbuffer())
250 255 return '$%s: %s $' % (kw, ekw)
251 256 return subfunc(kwsub, data)
252 257
253 258 def linkctx(self, path, fileid):
254 259 '''Similar to filelog.linkrev, but returns a changectx.'''
255 260 return self.repo.filectx(path, fileid=fileid).changectx()
256 261
257 262 def expand(self, path, node, data):
258 263 '''Returns data with keywords expanded.'''
259 264 if not self.restrict and self.match(path) and not util.binary(data):
260 265 ctx = self.linkctx(path, node)
261 266 return self.substitute(data, path, ctx, self.rekw.sub)
262 267 return data
263 268
264 269 def iskwfile(self, cand, ctx):
265 270 '''Returns subset of candidates which are configured for keyword
266 271 expansion but are not symbolic links.'''
267 272 return [f for f in cand if self.match(f) and 'l' not in ctx.flags(f)]
268 273
269 274 def overwrite(self, ctx, candidates, lookup, expand, rekw=False):
270 275 '''Overwrites selected files expanding/shrinking keywords.'''
271 276 if self.restrict or lookup or self.postcommit: # exclude kw_copy
272 277 candidates = self.iskwfile(candidates, ctx)
273 278 if not candidates:
274 279 return
275 280 kwcmd = self.restrict and lookup # kwexpand/kwshrink
276 281 if self.restrict or expand and lookup:
277 282 mf = ctx.manifest()
278 283 if self.restrict or rekw:
279 284 re_kw = self.rekw
280 285 else:
281 286 re_kw = self.rekwexp
282 287 if expand:
283 288 msg = _('overwriting %s expanding keywords\n')
284 289 else:
285 290 msg = _('overwriting %s shrinking keywords\n')
286 291 for f in candidates:
287 292 if self.restrict:
288 293 data = self.repo.file(f).read(mf[f])
289 294 else:
290 295 data = self.repo.wread(f)
291 296 if util.binary(data):
292 297 continue
293 298 if expand:
294 299 parents = ctx.parents()
295 300 if lookup:
296 301 ctx = self.linkctx(f, mf[f])
297 302 elif self.restrict and len(parents) > 1:
298 303 # merge commit
299 304 # in case of conflict f is in modified state during
300 305 # merge, even if f does not differ from f in parent
301 306 for p in parents:
302 307 if f in p and not p[f].cmp(ctx[f]):
303 308 ctx = p[f].changectx()
304 309 break
305 310 data, found = self.substitute(data, f, ctx, re_kw.subn)
306 311 elif self.restrict:
307 312 found = re_kw.search(data)
308 313 else:
309 314 data, found = _shrinktext(data, re_kw.subn)
310 315 if found:
311 316 self.ui.note(msg % f)
312 317 fp = self.repo.wvfs(f, "wb", atomictemp=True)
313 318 fp.write(data)
314 319 fp.close()
315 320 if kwcmd:
316 321 self.repo.dirstate.normal(f)
317 322 elif self.postcommit:
318 323 self.repo.dirstate.normallookup(f)
319 324
320 325 def shrink(self, fname, text):
321 326 '''Returns text with all keyword substitutions removed.'''
322 327 if self.match(fname) and not util.binary(text):
323 328 return _shrinktext(text, self.rekwexp.sub)
324 329 return text
325 330
326 331 def shrinklines(self, fname, lines):
327 332 '''Returns lines with keyword substitutions removed.'''
328 333 if self.match(fname):
329 334 text = ''.join(lines)
330 335 if not util.binary(text):
331 336 return _shrinktext(text, self.rekwexp.sub).splitlines(True)
332 337 return lines
333 338
334 339 def wread(self, fname, data):
335 340 '''If in restricted mode returns data read from wdir with
336 341 keyword substitutions removed.'''
337 342 if self.restrict:
338 343 return self.shrink(fname, data)
339 344 return data
340 345
341 346 class kwfilelog(filelog.filelog):
342 347 '''
343 348 Subclass of filelog to hook into its read, add, cmp methods.
344 349 Keywords are "stored" unexpanded, and processed on reading.
345 350 '''
346 351 def __init__(self, opener, kwt, path):
347 352 super(kwfilelog, self).__init__(opener, path)
348 353 self.kwt = kwt
349 354 self.path = path
350 355
351 356 def read(self, node):
352 357 '''Expands keywords when reading filelog.'''
353 358 data = super(kwfilelog, self).read(node)
354 359 if self.renamed(node):
355 360 return data
356 361 return self.kwt.expand(self.path, node, data)
357 362
358 363 def add(self, text, meta, tr, link, p1=None, p2=None):
359 364 '''Removes keyword substitutions when adding to filelog.'''
360 365 text = self.kwt.shrink(self.path, text)
361 366 return super(kwfilelog, self).add(text, meta, tr, link, p1, p2)
362 367
363 368 def cmp(self, node, text):
364 369 '''Removes keyword substitutions for comparison.'''
365 370 text = self.kwt.shrink(self.path, text)
366 371 return super(kwfilelog, self).cmp(node, text)
367 372
368 373 def _status(ui, repo, wctx, kwt, *pats, **opts):
369 374 '''Bails out if [keyword] configuration is not active.
370 375 Returns status of working directory.'''
371 376 if kwt:
372 377 return repo.status(match=scmutil.match(wctx, pats, opts), clean=True,
373 378 unknown=opts.get('unknown') or opts.get('all'))
374 379 if ui.configitems('keyword'):
375 380 raise error.Abort(_('[keyword] patterns cannot match'))
376 381 raise error.Abort(_('no [keyword] patterns configured'))
377 382
378 383 def _kwfwrite(ui, repo, expand, *pats, **opts):
379 384 '''Selects files and passes them to kwtemplater.overwrite.'''
380 385 wctx = repo[None]
381 386 if len(wctx.parents()) > 1:
382 387 raise error.Abort(_('outstanding uncommitted merge'))
383 388 kwt = kwtools['templater']
384 389 with repo.wlock():
385 390 status = _status(ui, repo, wctx, kwt, *pats, **opts)
386 391 if status.modified or status.added or status.removed or status.deleted:
387 392 raise error.Abort(_('outstanding uncommitted changes'))
388 393 kwt.overwrite(wctx, status.clean, True, expand)
389 394
390 395 @command('kwdemo',
391 396 [('d', 'default', None, _('show default keyword template maps')),
392 397 ('f', 'rcfile', '',
393 398 _('read maps from rcfile'), _('FILE'))],
394 399 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...'),
395 400 optionalrepo=True)
396 401 def demo(ui, repo, *args, **opts):
397 402 '''print [keywordmaps] configuration and an expansion example
398 403
399 404 Show current, custom, or default keyword template maps and their
400 405 expansions.
401 406
402 407 Extend the current configuration by specifying maps as arguments
403 408 and using -f/--rcfile to source an external hgrc file.
404 409
405 410 Use -d/--default to disable current configuration.
406 411
407 412 See :hg:`help templates` for information on templates and filters.
408 413 '''
409 414 def demoitems(section, items):
410 415 ui.write('[%s]\n' % section)
411 416 for k, v in sorted(items):
412 417 ui.write('%s = %s\n' % (k, v))
413 418
414 419 fn = 'demo.txt'
415 420 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
416 421 ui.note(_('creating temporary repository at %s\n') % tmpdir)
417 422 if repo is None:
418 423 baseui = ui
419 424 else:
420 425 baseui = repo.baseui
421 426 repo = localrepo.localrepository(baseui, tmpdir, True)
422 427 ui.setconfig('keyword', fn, '', 'keyword')
423 428 svn = ui.configbool('keywordset', 'svn')
424 429 # explicitly set keywordset for demo output
425 430 ui.setconfig('keywordset', 'svn', svn, 'keyword')
426 431
427 432 uikwmaps = ui.configitems('keywordmaps')
428 433 if args or opts.get('rcfile'):
429 434 ui.status(_('\n\tconfiguration using custom keyword template maps\n'))
430 435 if uikwmaps:
431 436 ui.status(_('\textending current template maps\n'))
432 437 if opts.get('default') or not uikwmaps:
433 438 if svn:
434 439 ui.status(_('\toverriding default svn keywordset\n'))
435 440 else:
436 441 ui.status(_('\toverriding default cvs keywordset\n'))
437 442 if opts.get('rcfile'):
438 443 ui.readconfig(opts.get('rcfile'))
439 444 if args:
440 445 # simulate hgrc parsing
441 446 rcmaps = '[keywordmaps]\n%s\n' % '\n'.join(args)
442 447 repo.vfs.write('hgrc', rcmaps)
443 448 ui.readconfig(repo.vfs.join('hgrc'))
444 449 kwmaps = dict(ui.configitems('keywordmaps'))
445 450 elif opts.get('default'):
446 451 if svn:
447 452 ui.status(_('\n\tconfiguration using default svn keywordset\n'))
448 453 else:
449 454 ui.status(_('\n\tconfiguration using default cvs keywordset\n'))
450 455 kwmaps = _defaultkwmaps(ui)
451 456 if uikwmaps:
452 457 ui.status(_('\tdisabling current template maps\n'))
453 458 for k, v in kwmaps.iteritems():
454 459 ui.setconfig('keywordmaps', k, v, 'keyword')
455 460 else:
456 461 ui.status(_('\n\tconfiguration using current keyword template maps\n'))
457 462 if uikwmaps:
458 463 kwmaps = dict(uikwmaps)
459 464 else:
460 465 kwmaps = _defaultkwmaps(ui)
461 466
462 467 uisetup(ui)
463 468 reposetup(ui, repo)
464 469 ui.write(('[extensions]\nkeyword =\n'))
465 470 demoitems('keyword', ui.configitems('keyword'))
466 471 demoitems('keywordset', ui.configitems('keywordset'))
467 472 demoitems('keywordmaps', kwmaps.iteritems())
468 473 keywords = '$' + '$\n$'.join(sorted(kwmaps.keys())) + '$\n'
469 474 repo.wvfs.write(fn, keywords)
470 475 repo[None].add([fn])
471 476 ui.note(_('\nkeywords written to %s:\n') % fn)
472 477 ui.note(keywords)
473 478 with repo.wlock():
474 479 repo.dirstate.setbranch('demobranch')
475 480 for name, cmd in ui.configitems('hooks'):
476 481 if name.split('.', 1)[0].find('commit') > -1:
477 482 repo.ui.setconfig('hooks', name, '', 'keyword')
478 483 msg = _('hg keyword configuration and expansion example')
479 484 ui.note(("hg ci -m '%s'\n" % msg))
480 485 repo.commit(text=msg)
481 486 ui.status(_('\n\tkeywords expanded\n'))
482 487 ui.write(repo.wread(fn))
483 488 repo.wvfs.rmtree(repo.root)
484 489
485 490 @command('kwexpand',
486 491 cmdutil.walkopts,
487 492 _('hg kwexpand [OPTION]... [FILE]...'),
488 493 inferrepo=True)
489 494 def expand(ui, repo, *pats, **opts):
490 495 '''expand keywords in the working directory
491 496
492 497 Run after (re)enabling keyword expansion.
493 498
494 499 kwexpand refuses to run if given files contain local changes.
495 500 '''
496 501 # 3rd argument sets expansion to True
497 502 _kwfwrite(ui, repo, True, *pats, **opts)
498 503
499 504 @command('kwfiles',
500 505 [('A', 'all', None, _('show keyword status flags of all files')),
501 506 ('i', 'ignore', None, _('show files excluded from expansion')),
502 507 ('u', 'unknown', None, _('only show unknown (not tracked) files')),
503 508 ] + cmdutil.walkopts,
504 509 _('hg kwfiles [OPTION]... [FILE]...'),
505 510 inferrepo=True)
506 511 def files(ui, repo, *pats, **opts):
507 512 '''show files configured for keyword expansion
508 513
509 514 List which files in the working directory are matched by the
510 515 [keyword] configuration patterns.
511 516
512 517 Useful to prevent inadvertent keyword expansion and to speed up
513 518 execution by including only files that are actual candidates for
514 519 expansion.
515 520
516 521 See :hg:`help keyword` on how to construct patterns both for
517 522 inclusion and exclusion of files.
518 523
519 524 With -A/--all and -v/--verbose the codes used to show the status
520 525 of files are::
521 526
522 527 K = keyword expansion candidate
523 528 k = keyword expansion candidate (not tracked)
524 529 I = ignored
525 530 i = ignored (not tracked)
526 531 '''
527 532 kwt = kwtools['templater']
528 533 wctx = repo[None]
529 534 status = _status(ui, repo, wctx, kwt, *pats, **opts)
530 535 if pats:
531 536 cwd = repo.getcwd()
532 537 else:
533 538 cwd = ''
534 539 files = []
535 540 if not opts.get('unknown') or opts.get('all'):
536 541 files = sorted(status.modified + status.added + status.clean)
537 542 kwfiles = kwt.iskwfile(files, wctx)
538 543 kwdeleted = kwt.iskwfile(status.deleted, wctx)
539 544 kwunknown = kwt.iskwfile(status.unknown, wctx)
540 545 if not opts.get('ignore') or opts.get('all'):
541 546 showfiles = kwfiles, kwdeleted, kwunknown
542 547 else:
543 548 showfiles = [], [], []
544 549 if opts.get('all') or opts.get('ignore'):
545 550 showfiles += ([f for f in files if f not in kwfiles],
546 551 [f for f in status.unknown if f not in kwunknown])
547 552 kwlabels = 'enabled deleted enabledunknown ignored ignoredunknown'.split()
548 553 kwstates = zip(kwlabels, 'K!kIi', showfiles)
549 554 fm = ui.formatter('kwfiles', opts)
550 555 fmt = '%.0s%s\n'
551 556 if opts.get('all') or ui.verbose:
552 557 fmt = '%s %s\n'
553 558 for kwstate, char, filenames in kwstates:
554 559 label = 'kwfiles.' + kwstate
555 560 for f in filenames:
556 561 fm.startitem()
557 562 fm.write('kwstatus path', fmt, char,
558 563 repo.pathto(f, cwd), label=label)
559 564 fm.end()
560 565
561 566 @command('kwshrink',
562 567 cmdutil.walkopts,
563 568 _('hg kwshrink [OPTION]... [FILE]...'),
564 569 inferrepo=True)
565 570 def shrink(ui, repo, *pats, **opts):
566 571 '''revert expanded keywords in the working directory
567 572
568 573 Must be run before changing/disabling active keywords.
569 574
570 575 kwshrink refuses to run if given files contain local changes.
571 576 '''
572 577 # 3rd argument sets expansion to False
573 578 _kwfwrite(ui, repo, False, *pats, **opts)
574 579
575 580
576 581 def uisetup(ui):
577 582 ''' Monkeypatches dispatch._parse to retrieve user command.'''
578 583
579 584 def kwdispatch_parse(orig, ui, args):
580 585 '''Monkeypatch dispatch._parse to obtain running hg command.'''
581 586 cmd, func, args, options, cmdoptions = orig(ui, args)
582 587 kwtools['hgcmd'] = cmd
583 588 return cmd, func, args, options, cmdoptions
584 589
585 590 extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse)
586 591
587 592 def reposetup(ui, repo):
588 593 '''Sets up repo as kwrepo for keyword substitution.
589 594 Overrides file method to return kwfilelog instead of filelog
590 595 if file matches user configuration.
591 596 Wraps commit to overwrite configured files with updated
592 597 keyword substitutions.
593 598 Monkeypatches patch and webcommands.'''
594 599
595 600 try:
596 601 if (not repo.local() or kwtools['hgcmd'] in nokwcommands.split()
597 602 or '.hg' in util.splitpath(repo.root)
598 603 or repo._url.startswith('bundle:')):
599 604 return
600 605 except AttributeError:
601 606 pass
602 607
603 608 inc, exc = [], ['.hg*']
604 609 for pat, opt in ui.configitems('keyword'):
605 610 if opt != 'ignore':
606 611 inc.append(pat)
607 612 else:
608 613 exc.append(pat)
609 614 if not inc:
610 615 return
611 616
612 617 kwtools['templater'] = kwt = kwtemplater(ui, repo, inc, exc)
613 618
614 619 class kwrepo(repo.__class__):
615 620 def file(self, f):
616 621 if f[0] == '/':
617 622 f = f[1:]
618 623 return kwfilelog(self.svfs, kwt, f)
619 624
620 625 def wread(self, filename):
621 626 data = super(kwrepo, self).wread(filename)
622 627 return kwt.wread(filename, data)
623 628
624 629 def commit(self, *args, **opts):
625 630 # use custom commitctx for user commands
626 631 # other extensions can still wrap repo.commitctx directly
627 632 self.commitctx = self.kwcommitctx
628 633 try:
629 634 return super(kwrepo, self).commit(*args, **opts)
630 635 finally:
631 636 del self.commitctx
632 637
633 638 def kwcommitctx(self, ctx, error=False):
634 639 n = super(kwrepo, self).commitctx(ctx, error)
635 640 # no lock needed, only called from repo.commit() which already locks
636 641 if not kwt.postcommit:
637 642 restrict = kwt.restrict
638 643 kwt.restrict = True
639 644 kwt.overwrite(self[n], sorted(ctx.added() + ctx.modified()),
640 645 False, True)
641 646 kwt.restrict = restrict
642 647 return n
643 648
644 649 def rollback(self, dryrun=False, force=False):
645 650 with self.wlock():
646 651 origrestrict = kwt.restrict
647 652 try:
648 653 if not dryrun:
649 654 changed = self['.'].files()
650 655 ret = super(kwrepo, self).rollback(dryrun, force)
651 656 if not dryrun:
652 657 ctx = self['.']
653 658 modified, added = _preselect(ctx.status(), changed)
654 659 kwt.restrict = False
655 660 kwt.overwrite(ctx, modified, True, True)
656 661 kwt.overwrite(ctx, added, True, False)
657 662 return ret
658 663 finally:
659 664 kwt.restrict = origrestrict
660 665
666 repo.__class__ = kwrepo
667 repo._keywordkwt = kwt
668
661 669 # monkeypatches
662 670 def kwpatchfile_init(orig, self, ui, gp, backend, store, eolmode=None):
663 671 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
664 672 rejects or conflicts due to expanded keywords in working dir.'''
665 673 orig(self, ui, gp, backend, store, eolmode)
666 674 # shrink keywords read from working dir
667 675 self.lines = kwt.shrinklines(self.fname, self.lines)
668 676
669 677 def kwdiff(orig, *args, **kwargs):
670 678 '''Monkeypatch patch.diff to avoid expansion.'''
671 679 restrict = kwt.restrict
672 680 kwt.restrict = True
673 681 try:
674 682 for chunk in orig(*args, **kwargs):
675 683 yield chunk
676 684 finally:
677 685 kwt.restrict = restrict
678 686
679 687 def kwweb_skip(orig, web, req, tmpl):
680 688 '''Wraps webcommands.x turning off keyword expansion.'''
681 689 origmatch = kwt.match
682 690 kwt.match = util.never
683 691 try:
684 692 for chunk in orig(web, req, tmpl):
685 693 yield chunk
686 694 finally:
687 695 kwt.match = origmatch
688 696
689 697 def kw_amend(orig, ui, repo, commitfunc, old, extra, pats, opts):
690 698 '''Wraps cmdutil.amend expanding keywords after amend.'''
691 699 with repo.wlock():
692 700 kwt.postcommit = True
693 701 newid = orig(ui, repo, commitfunc, old, extra, pats, opts)
694 702 if newid != old.node():
695 703 ctx = repo[newid]
696 704 kwt.restrict = True
697 705 kwt.overwrite(ctx, ctx.files(), False, True)
698 706 kwt.restrict = False
699 707 return newid
700 708
701 709 def kw_copy(orig, ui, repo, pats, opts, rename=False):
702 710 '''Wraps cmdutil.copy so that copy/rename destinations do not
703 711 contain expanded keywords.
704 712 Note that the source of a regular file destination may also be a
705 713 symlink:
706 714 hg cp sym x -> x is symlink
707 715 cp sym x; hg cp -A sym x -> x is file (maybe expanded keywords)
708 716 For the latter we have to follow the symlink to find out whether its
709 717 target is configured for expansion and we therefore must unexpand the
710 718 keywords in the destination.'''
711 719 with repo.wlock():
712 720 orig(ui, repo, pats, opts, rename)
713 721 if opts.get('dry_run'):
714 722 return
715 723 wctx = repo[None]
716 724 cwd = repo.getcwd()
717 725
718 726 def haskwsource(dest):
719 727 '''Returns true if dest is a regular file and configured for
720 728 expansion or a symlink which points to a file configured for
721 729 expansion. '''
722 730 source = repo.dirstate.copied(dest)
723 731 if 'l' in wctx.flags(source):
724 732 source = pathutil.canonpath(repo.root, cwd,
725 733 os.path.realpath(source))
726 734 return kwt.match(source)
727 735
728 736 candidates = [f for f in repo.dirstate.copies() if
729 737 'l' not in wctx.flags(f) and haskwsource(f)]
730 738 kwt.overwrite(wctx, candidates, False, False)
731 739
732 740 def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts):
733 741 '''Wraps record.dorecord expanding keywords after recording.'''
734 742 with repo.wlock():
735 743 # record returns 0 even when nothing has changed
736 744 # therefore compare nodes before and after
737 745 kwt.postcommit = True
738 746 ctx = repo['.']
739 747 wstatus = ctx.status()
740 748 ret = orig(ui, repo, commitfunc, *pats, **opts)
741 749 recctx = repo['.']
742 750 if ctx != recctx:
743 751 modified, added = _preselect(wstatus, recctx.files())
744 752 kwt.restrict = False
745 753 kwt.overwrite(recctx, modified, False, True)
746 754 kwt.overwrite(recctx, added, False, True, True)
747 755 kwt.restrict = True
748 756 return ret
749 757
750 758 def kwfilectx_cmp(orig, self, fctx):
751 759 if fctx._customcmp:
752 760 return fctx.cmp(self)
753 761 # keyword affects data size, comparing wdir and filelog size does
754 762 # not make sense
755 763 if (fctx._filenode is None and
756 764 (self._repo._encodefilterpats or
757 765 kwt.match(fctx.path()) and 'l' not in fctx.flags() or
758 766 self.size() - 4 == fctx.size()) or
759 767 self.size() == fctx.size()):
760 768 return self._filelog.cmp(self._filenode, fctx.data())
761 769 return True
762 770
763 771 extensions.wrapfunction(context.filectx, 'cmp', kwfilectx_cmp)
764 772 extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init)
765 773 extensions.wrapfunction(patch, 'diff', kwdiff)
766 774 extensions.wrapfunction(cmdutil, 'amend', kw_amend)
767 775 extensions.wrapfunction(cmdutil, 'copy', kw_copy)
768 776 extensions.wrapfunction(cmdutil, 'dorecord', kw_dorecord)
769 777 for c in nokwwebcommands.split():
770 778 extensions.wrapfunction(webcommands, c, kwweb_skip)
771 repo.__class__ = kwrepo
General Comments 0
You need to be logged in to leave comments. Login now