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