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