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