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