##// END OF EJS Templates
keyword: use util.datestr for utcdate filter, expose through variable
Christian Ebert -
r9308:6fdd39f5 default
parent child Browse files
Show More
@@ -1,556 +1,555 b''
1 1 # keyword.py - $Keyword$ expansion for Mercurial
2 2 #
3 3 # Copyright 2007-2009 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, incorporated herein by reference.
7 7 #
8 8 # $Id$
9 9 #
10 10 # Keyword expansion hack against the grain of a DSCM
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 Configuration is done in the [keyword] and [keywordmaps] sections of
39 39 hgrc files.
40 40
41 41 Example::
42 42
43 43 [keyword]
44 44 # expand keywords in every python file except those matching "x*"
45 45 **.py =
46 46 x* = ignore
47 47
48 48 NOTE: the more specific you are in your filename patterns the less you
49 49 lose speed in huge repositories.
50 50
51 51 For [keywordmaps] template mapping and expansion demonstration and
52 52 control run "hg kwdemo". See "hg help templates" for a list of
53 53 available templates and filters.
54 54
55 55 An additional date template filter {date|utcdate} is provided. It
56 56 returns a date like "2006/09/18 15:13:13".
57 57
58 58 The default template mappings (view with "hg kwdemo -d") can be
59 59 replaced with customized keywords and templates. Again, run "hg
60 60 kwdemo" to control the results of your config changes.
61 61
62 62 Before changing/disabling active keywords, run "hg kwshrink" to avoid
63 63 the risk of inadvertently storing expanded keywords in the change
64 64 history.
65 65
66 66 To force expansion after enabling it, or a configuration change, run
67 67 "hg kwexpand".
68 68
69 69 Also, when committing with the record extension or using mq's qrecord,
70 70 be aware that keywords cannot be updated. Again, run "hg kwexpand" on
71 71 the files in question to update keyword expansions after all changes
72 72 have been checked in.
73 73
74 74 Expansions spanning more than one line and incremental expansions,
75 75 like CVS' $Log$, are not supported. A keyword template map "Log =
76 76 {desc}" expands to the first line of the changeset description.
77 77 '''
78 78
79 79 from mercurial import commands, cmdutil, dispatch, filelog, revlog, extensions
80 80 from mercurial import patch, localrepo, templater, templatefilters, util, match
81 81 from mercurial.hgweb import webcommands
82 82 from mercurial.lock import release
83 83 from mercurial.node import nullid
84 84 from mercurial.i18n import _
85 import re, shutil, tempfile, time
85 import re, shutil, tempfile
86 86
87 87 commands.optionalrepo += ' kwdemo'
88 88
89 89 # hg commands that do not act on keywords
90 90 nokwcommands = ('add addremove annotate bundle copy export grep incoming init'
91 91 ' log outgoing push rename rollback tip verify'
92 92 ' convert email glog')
93 93
94 94 # hg commands that trigger expansion only when writing to working dir,
95 95 # not when reading filelog, and unexpand when reading from working dir
96 96 restricted = 'merge record resolve qfold qimport qnew qpush qrefresh qrecord'
97 97
98 def utcdate(date):
99 '''Returns hgdate in cvs-like UTC format.'''
100 return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0]))
98 # provide cvs-like UTC date filter
99 utcdate = lambda x: util.datestr(x, '%Y/%m/%d %H:%M:%S')
101 100
102 101 # make keyword tools accessible
103 102 kwtools = {'templater': None, 'hgcmd': '', 'inc': [], 'exc': ['.hg*']}
104 103
105 104
106 105 class kwtemplater(object):
107 106 '''
108 107 Sets up keyword templates, corresponding keyword regex, and
109 108 provides keyword substitution functions.
110 109 '''
111 110 templates = {
112 111 'Revision': '{node|short}',
113 112 'Author': '{author|user}',
114 113 'Date': '{date|utcdate}',
115 114 'RCSFile': '{file|basename},v',
116 115 'Source': '{root}/{file},v',
117 116 'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
118 117 'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
119 118 }
120 119
121 120 def __init__(self, ui, repo):
122 121 self.ui = ui
123 122 self.repo = repo
124 123 self.match = match.match(repo.root, '', [],
125 124 kwtools['inc'], kwtools['exc'])
126 125 self.restrict = kwtools['hgcmd'] in restricted.split()
127 126
128 127 kwmaps = self.ui.configitems('keywordmaps')
129 128 if kwmaps: # override default templates
130 129 self.templates = dict((k, templater.parsestring(v, False))
131 130 for k, v in kwmaps)
132 131 escaped = map(re.escape, self.templates.keys())
133 132 kwpat = r'\$(%s)(: [^$\n\r]*? )??\$' % '|'.join(escaped)
134 133 self.re_kw = re.compile(kwpat)
135 134
136 135 templatefilters.filters['utcdate'] = utcdate
137 136 self.ct = cmdutil.changeset_templater(self.ui, self.repo,
138 137 False, None, '', False)
139 138
140 139 def substitute(self, data, path, ctx, subfunc):
141 140 '''Replaces keywords in data with expanded template.'''
142 141 def kwsub(mobj):
143 142 kw = mobj.group(1)
144 143 self.ct.use_template(self.templates[kw])
145 144 self.ui.pushbuffer()
146 145 self.ct.show(ctx, root=self.repo.root, file=path)
147 146 ekw = templatefilters.firstline(self.ui.popbuffer())
148 147 return '$%s: %s $' % (kw, ekw)
149 148 return subfunc(kwsub, data)
150 149
151 150 def expand(self, path, node, data):
152 151 '''Returns data with keywords expanded.'''
153 152 if not self.restrict and self.match(path) and not util.binary(data):
154 153 ctx = self.repo.filectx(path, fileid=node).changectx()
155 154 return self.substitute(data, path, ctx, self.re_kw.sub)
156 155 return data
157 156
158 157 def iskwfile(self, path, flagfunc):
159 158 '''Returns true if path matches [keyword] pattern
160 159 and is not a symbolic link.
161 160 Caveat: localrepository._link fails on Windows.'''
162 161 return self.match(path) and not 'l' in flagfunc(path)
163 162
164 163 def overwrite(self, node, expand, files):
165 164 '''Overwrites selected files expanding/shrinking keywords.'''
166 165 ctx = self.repo[node]
167 166 mf = ctx.manifest()
168 167 if node is not None: # commit
169 168 files = [f for f in ctx.files() if f in mf]
170 169 notify = self.ui.debug
171 170 else: # kwexpand/kwshrink
172 171 notify = self.ui.note
173 172 candidates = [f for f in files if self.iskwfile(f, ctx.flags)]
174 173 if candidates:
175 174 self.restrict = True # do not expand when reading
176 175 msg = (expand and _('overwriting %s expanding keywords\n')
177 176 or _('overwriting %s shrinking keywords\n'))
178 177 for f in candidates:
179 178 fp = self.repo.file(f)
180 179 data = fp.read(mf[f])
181 180 if util.binary(data):
182 181 continue
183 182 if expand:
184 183 if node is None:
185 184 ctx = self.repo.filectx(f, fileid=mf[f]).changectx()
186 185 data, found = self.substitute(data, f, ctx,
187 186 self.re_kw.subn)
188 187 else:
189 188 found = self.re_kw.search(data)
190 189 if found:
191 190 notify(msg % f)
192 191 self.repo.wwrite(f, data, mf.flags(f))
193 192 if node is None:
194 193 self.repo.dirstate.normal(f)
195 194 self.restrict = False
196 195
197 196 def shrinktext(self, text):
198 197 '''Unconditionally removes all keyword substitutions from text.'''
199 198 return self.re_kw.sub(r'$\1$', text)
200 199
201 200 def shrink(self, fname, text):
202 201 '''Returns text with all keyword substitutions removed.'''
203 202 if self.match(fname) and not util.binary(text):
204 203 return self.shrinktext(text)
205 204 return text
206 205
207 206 def shrinklines(self, fname, lines):
208 207 '''Returns lines with keyword substitutions removed.'''
209 208 if self.match(fname):
210 209 text = ''.join(lines)
211 210 if not util.binary(text):
212 211 return self.shrinktext(text).splitlines(True)
213 212 return lines
214 213
215 214 def wread(self, fname, data):
216 215 '''If in restricted mode returns data read from wdir with
217 216 keyword substitutions removed.'''
218 217 return self.restrict and self.shrink(fname, data) or data
219 218
220 219 class kwfilelog(filelog.filelog):
221 220 '''
222 221 Subclass of filelog to hook into its read, add, cmp methods.
223 222 Keywords are "stored" unexpanded, and processed on reading.
224 223 '''
225 224 def __init__(self, opener, kwt, path):
226 225 super(kwfilelog, self).__init__(opener, path)
227 226 self.kwt = kwt
228 227 self.path = path
229 228
230 229 def read(self, node):
231 230 '''Expands keywords when reading filelog.'''
232 231 data = super(kwfilelog, self).read(node)
233 232 return self.kwt.expand(self.path, node, data)
234 233
235 234 def add(self, text, meta, tr, link, p1=None, p2=None):
236 235 '''Removes keyword substitutions when adding to filelog.'''
237 236 text = self.kwt.shrink(self.path, text)
238 237 return super(kwfilelog, self).add(text, meta, tr, link, p1, p2)
239 238
240 239 def cmp(self, node, text):
241 240 '''Removes keyword substitutions for comparison.'''
242 241 text = self.kwt.shrink(self.path, text)
243 242 if self.renamed(node):
244 243 t2 = super(kwfilelog, self).read(node)
245 244 return t2 != text
246 245 return revlog.revlog.cmp(self, node, text)
247 246
248 247 def _status(ui, repo, kwt, unknown, *pats, **opts):
249 248 '''Bails out if [keyword] configuration is not active.
250 249 Returns status of working directory.'''
251 250 if kwt:
252 251 match = cmdutil.match(repo, pats, opts)
253 252 return repo.status(match=match, unknown=unknown, clean=True)
254 253 if ui.configitems('keyword'):
255 254 raise util.Abort(_('[keyword] patterns cannot match'))
256 255 raise util.Abort(_('no [keyword] patterns configured'))
257 256
258 257 def _kwfwrite(ui, repo, expand, *pats, **opts):
259 258 '''Selects files and passes them to kwtemplater.overwrite.'''
260 259 if repo.dirstate.parents()[1] != nullid:
261 260 raise util.Abort(_('outstanding uncommitted merge'))
262 261 kwt = kwtools['templater']
263 262 status = _status(ui, repo, kwt, False, *pats, **opts)
264 263 modified, added, removed, deleted = status[:4]
265 264 if modified or added or removed or deleted:
266 265 raise util.Abort(_('outstanding uncommitted changes'))
267 266 wlock = lock = None
268 267 try:
269 268 wlock = repo.wlock()
270 269 lock = repo.lock()
271 270 kwt.overwrite(None, expand, status[6])
272 271 finally:
273 272 release(lock, wlock)
274 273
275 274 def demo(ui, repo, *args, **opts):
276 275 '''print [keywordmaps] configuration and an expansion example
277 276
278 277 Show current, custom, or default keyword template maps and their
279 278 expansions.
280 279
281 280 Extend the current configuration by specifying maps as arguments
282 281 and using -f/--rcfile to source an external hgrc file.
283 282
284 283 Use -d/--default to disable current configuration.
285 284
286 285 See "hg help templates" for information on templates and filters.
287 286 '''
288 287 def demoitems(section, items):
289 288 ui.write('[%s]\n' % section)
290 289 for k, v in items:
291 290 ui.write('%s = %s\n' % (k, v))
292 291
293 292 msg = 'hg keyword config and expansion example'
294 293 fn = 'demo.txt'
295 294 branchname = 'demobranch'
296 295 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
297 296 ui.note(_('creating temporary repository at %s\n') % tmpdir)
298 297 repo = localrepo.localrepository(ui, tmpdir, True)
299 298 ui.setconfig('keyword', fn, '')
300 299
301 300 uikwmaps = ui.configitems('keywordmaps')
302 301 if args or opts.get('rcfile'):
303 302 ui.status(_('\n\tconfiguration using custom keyword template maps\n'))
304 303 if uikwmaps:
305 304 ui.status(_('\textending current template maps\n'))
306 305 if opts.get('default') or not uikwmaps:
307 306 ui.status(_('\toverriding default template maps\n'))
308 307 if opts.get('rcfile'):
309 308 ui.readconfig(opts.get('rcfile'))
310 309 if args:
311 310 # simulate hgrc parsing
312 311 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
313 312 fp = repo.opener('hgrc', 'w')
314 313 fp.writelines(rcmaps)
315 314 fp.close()
316 315 ui.readconfig(repo.join('hgrc'))
317 316 kwmaps = dict(ui.configitems('keywordmaps'))
318 317 elif opts.get('default'):
319 318 ui.status(_('\n\tconfiguration using default keyword template maps\n'))
320 319 kwmaps = kwtemplater.templates
321 320 if uikwmaps:
322 321 ui.status(_('\tdisabling current template maps\n'))
323 322 for k, v in kwmaps.iteritems():
324 323 ui.setconfig('keywordmaps', k, v)
325 324 else:
326 325 ui.status(_('\n\tconfiguration using current keyword template maps\n'))
327 326 kwmaps = dict(uikwmaps) or kwtemplater.templates
328 327
329 328 uisetup(ui)
330 329 reposetup(ui, repo)
331 330 for k, v in ui.configitems('extensions'):
332 331 if k.endswith('keyword'):
333 332 extension = '%s = %s' % (k, v)
334 333 break
335 334 ui.write('[extensions]\n%s\n' % extension)
336 335 demoitems('keyword', ui.configitems('keyword'))
337 336 demoitems('keywordmaps', kwmaps.iteritems())
338 337 keywords = '$' + '$\n$'.join(kwmaps.keys()) + '$\n'
339 338 repo.wopener(fn, 'w').write(keywords)
340 339 repo.add([fn])
341 340 path = repo.wjoin(fn)
342 341 ui.note(_('\nkeywords written to %s:\n') % path)
343 342 ui.note(keywords)
344 343 ui.note('\nhg -R "%s" branch "%s"\n' % (tmpdir, branchname))
345 344 # silence branch command if not verbose
346 345 quiet = ui.quiet
347 346 ui.quiet = not ui.verbose
348 347 commands.branch(ui, repo, branchname)
349 348 ui.quiet = quiet
350 349 for name, cmd in ui.configitems('hooks'):
351 350 if name.split('.', 1)[0].find('commit') > -1:
352 351 repo.ui.setconfig('hooks', name, '')
353 352 ui.note(_('unhooked all commit hooks\n'))
354 353 ui.note('hg -R "%s" ci -m "%s"\n' % (tmpdir, msg))
355 354 repo.commit(text=msg)
356 355 ui.status(_('\n\tkeywords expanded\n'))
357 356 ui.write(repo.wread(fn))
358 357 ui.debug(_('\nremoving temporary repository %s\n') % tmpdir)
359 358 shutil.rmtree(tmpdir, ignore_errors=True)
360 359
361 360 def expand(ui, repo, *pats, **opts):
362 361 '''expand keywords in the working directory
363 362
364 363 Run after (re)enabling keyword expansion.
365 364
366 365 kwexpand refuses to run if given files contain local changes.
367 366 '''
368 367 # 3rd argument sets expansion to True
369 368 _kwfwrite(ui, repo, True, *pats, **opts)
370 369
371 370 def files(ui, repo, *pats, **opts):
372 371 '''show files configured for keyword expansion
373 372
374 373 List which files in the working directory are matched by the
375 374 [keyword] configuration patterns.
376 375
377 376 Useful to prevent inadvertent keyword expansion and to speed up
378 377 execution by including only files that are actual candidates for
379 378 expansion.
380 379
381 380 See "hg help keyword" on how to construct patterns both for
382 381 inclusion and exclusion of files.
383 382
384 383 Use -u/--untracked to list untracked files as well.
385 384
386 385 With -a/--all and -v/--verbose the codes used to show the status
387 386 of files are::
388 387
389 388 K = keyword expansion candidate
390 389 k = keyword expansion candidate (untracked)
391 390 I = ignored
392 391 i = ignored (untracked)
393 392 '''
394 393 kwt = kwtools['templater']
395 394 status = _status(ui, repo, kwt, opts.get('untracked'), *pats, **opts)
396 395 modified, added, removed, deleted, unknown, ignored, clean = status
397 396 files = sorted(modified + added + clean)
398 397 wctx = repo[None]
399 398 kwfiles = [f for f in files if kwt.iskwfile(f, wctx.flags)]
400 399 kwuntracked = [f for f in unknown if kwt.iskwfile(f, wctx.flags)]
401 400 cwd = pats and repo.getcwd() or ''
402 401 kwfstats = (not opts.get('ignore') and
403 402 (('K', kwfiles), ('k', kwuntracked),) or ())
404 403 if opts.get('all') or opts.get('ignore'):
405 404 kwfstats += (('I', [f for f in files if f not in kwfiles]),
406 405 ('i', [f for f in unknown if f not in kwuntracked]),)
407 406 for char, filenames in kwfstats:
408 407 fmt = (opts.get('all') or ui.verbose) and '%s %%s\n' % char or '%s\n'
409 408 for f in filenames:
410 409 ui.write(fmt % repo.pathto(f, cwd))
411 410
412 411 def shrink(ui, repo, *pats, **opts):
413 412 '''revert expanded keywords in the working directory
414 413
415 414 Run before changing/disabling active keywords or if you experience
416 415 problems with "hg import" or "hg merge".
417 416
418 417 kwshrink refuses to run if given files contain local changes.
419 418 '''
420 419 # 3rd argument sets expansion to False
421 420 _kwfwrite(ui, repo, False, *pats, **opts)
422 421
423 422
424 423 def uisetup(ui):
425 424 '''Collects [keyword] config in kwtools.
426 425 Monkeypatches dispatch._parse if needed.'''
427 426
428 427 for pat, opt in ui.configitems('keyword'):
429 428 if opt != 'ignore':
430 429 kwtools['inc'].append(pat)
431 430 else:
432 431 kwtools['exc'].append(pat)
433 432
434 433 if kwtools['inc']:
435 434 def kwdispatch_parse(orig, ui, args):
436 435 '''Monkeypatch dispatch._parse to obtain running hg command.'''
437 436 cmd, func, args, options, cmdoptions = orig(ui, args)
438 437 kwtools['hgcmd'] = cmd
439 438 return cmd, func, args, options, cmdoptions
440 439
441 440 extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse)
442 441
443 442 def reposetup(ui, repo):
444 443 '''Sets up repo as kwrepo for keyword substitution.
445 444 Overrides file method to return kwfilelog instead of filelog
446 445 if file matches user configuration.
447 446 Wraps commit to overwrite configured files with updated
448 447 keyword substitutions.
449 448 Monkeypatches patch and webcommands.'''
450 449
451 450 try:
452 451 if (not repo.local() or not kwtools['inc']
453 452 or kwtools['hgcmd'] in nokwcommands.split()
454 453 or '.hg' in util.splitpath(repo.root)
455 454 or repo._url.startswith('bundle:')):
456 455 return
457 456 except AttributeError:
458 457 pass
459 458
460 459 kwtools['templater'] = kwt = kwtemplater(ui, repo)
461 460
462 461 class kwrepo(repo.__class__):
463 462 def file(self, f):
464 463 if f[0] == '/':
465 464 f = f[1:]
466 465 return kwfilelog(self.sopener, kwt, f)
467 466
468 467 def wread(self, filename):
469 468 data = super(kwrepo, self).wread(filename)
470 469 return kwt.wread(filename, data)
471 470
472 471 def commit(self, *args, **opts):
473 472 # use custom commitctx for user commands
474 473 # other extensions can still wrap repo.commitctx directly
475 474 self.commitctx = self.kwcommitctx
476 475 try:
477 476 return super(kwrepo, self).commit(*args, **opts)
478 477 finally:
479 478 del self.commitctx
480 479
481 480 def kwcommitctx(self, ctx, error=False):
482 481 wlock = lock = None
483 482 try:
484 483 wlock = self.wlock()
485 484 lock = self.lock()
486 485 # store and postpone commit hooks
487 486 commithooks = {}
488 487 for name, cmd in ui.configitems('hooks'):
489 488 if name.split('.', 1)[0] == 'commit':
490 489 commithooks[name] = cmd
491 490 ui.setconfig('hooks', name, None)
492 491 if commithooks:
493 492 # store parents for commit hooks
494 493 p1, p2 = ctx.p1(), ctx.p2()
495 494 xp1, xp2 = p1.hex(), p2 and p2.hex() or ''
496 495
497 496 n = super(kwrepo, self).commitctx(ctx, error)
498 497
499 498 kwt.overwrite(n, True, None)
500 499 if commithooks:
501 500 for name, cmd in commithooks.iteritems():
502 501 ui.setconfig('hooks', name, cmd)
503 502 self.hook('commit', node=n, parent1=xp1, parent2=xp2)
504 503 return n
505 504 finally:
506 505 release(lock, wlock)
507 506
508 507 # monkeypatches
509 508 def kwpatchfile_init(orig, self, ui, fname, opener,
510 509 missing=False, eol=None):
511 510 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
512 511 rejects or conflicts due to expanded keywords in working dir.'''
513 512 orig(self, ui, fname, opener, missing, eol)
514 513 # shrink keywords read from working dir
515 514 self.lines = kwt.shrinklines(self.fname, self.lines)
516 515
517 516 def kw_diff(orig, repo, node1=None, node2=None, match=None, changes=None,
518 517 opts=None):
519 518 '''Monkeypatch patch.diff to avoid expansion except when
520 519 comparing against working dir.'''
521 520 if node2 is not None:
522 521 kwt.match = util.never
523 522 elif node1 is not None and node1 != repo['.'].node():
524 523 kwt.restrict = True
525 524 return orig(repo, node1, node2, match, changes, opts)
526 525
527 526 def kwweb_skip(orig, web, req, tmpl):
528 527 '''Wraps webcommands.x turning off keyword expansion.'''
529 528 kwt.match = util.never
530 529 return orig(web, req, tmpl)
531 530
532 531 repo.__class__ = kwrepo
533 532
534 533 extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init)
535 534 extensions.wrapfunction(patch, 'diff', kw_diff)
536 535 for c in 'annotate changeset rev filediff diff'.split():
537 536 extensions.wrapfunction(webcommands, c, kwweb_skip)
538 537
539 538 cmdtable = {
540 539 'kwdemo':
541 540 (demo,
542 541 [('d', 'default', None, _('show default keyword template maps')),
543 542 ('f', 'rcfile', '', _('read maps from rcfile'))],
544 543 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')),
545 544 'kwexpand': (expand, commands.walkopts,
546 545 _('hg kwexpand [OPTION]... [FILE]...')),
547 546 'kwfiles':
548 547 (files,
549 548 [('a', 'all', None, _('show keyword status flags of all files')),
550 549 ('i', 'ignore', None, _('show files excluded from expansion')),
551 550 ('u', 'untracked', None, _('additionally show untracked files')),
552 551 ] + commands.walkopts,
553 552 _('hg kwfiles [OPTION]... [FILE]...')),
554 553 'kwshrink': (shrink, commands.walkopts,
555 554 _('hg kwshrink [OPTION]... [FILE]...')),
556 555 }
General Comments 0
You need to be logged in to leave comments. Login now