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