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