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