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