##// END OF EJS Templates
keyword: code cleanup...
Christian Ebert -
r12723:eaa09d25 default
parent child Browse files
Show More
@@ -1,625 +1,630 b''
1 # keyword.py - $Keyword$ expansion for Mercurial
1 # keyword.py - $Keyword$ expansion for Mercurial
2 #
2 #
3 # Copyright 2007-2010 Christian Ebert <blacktrash@gmx.net>
3 # Copyright 2007-2010 Christian Ebert <blacktrash@gmx.net>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 #
7 #
8 # $Id$
8 # $Id$
9 #
9 #
10 # Keyword expansion hack against the grain of a DSCM
10 # Keyword expansion hack against the grain of a DSCM
11 #
11 #
12 # There are many good reasons why this is not needed in a distributed
12 # There are many good reasons why this is not needed in a distributed
13 # SCM, still it may be useful in very small projects based on single
13 # SCM, still it may be useful in very small projects based on single
14 # files (like LaTeX packages), that are mostly addressed to an
14 # files (like LaTeX packages), that are mostly addressed to an
15 # audience not running a version control system.
15 # audience not running a version control system.
16 #
16 #
17 # For in-depth discussion refer to
17 # For in-depth discussion refer to
18 # <http://mercurial.selenic.com/wiki/KeywordPlan>.
18 # <http://mercurial.selenic.com/wiki/KeywordPlan>.
19 #
19 #
20 # Keyword expansion is based on Mercurial's changeset template mappings.
20 # Keyword expansion is based on Mercurial's changeset template mappings.
21 #
21 #
22 # Binary files are not touched.
22 # Binary files are not touched.
23 #
23 #
24 # Files to act upon/ignore are specified in the [keyword] section.
24 # Files to act upon/ignore are specified in the [keyword] section.
25 # Customized keyword template mappings in the [keywordmaps] section.
25 # Customized keyword template mappings in the [keywordmaps] section.
26 #
26 #
27 # Run "hg help keyword" and "hg kwdemo" to get info on configuration.
27 # Run "hg help keyword" and "hg kwdemo" to get info on configuration.
28
28
29 '''expand keywords in tracked files
29 '''expand keywords in tracked files
30
30
31 This extension expands RCS/CVS-like or self-customized $Keywords$ in
31 This extension expands RCS/CVS-like or self-customized $Keywords$ in
32 tracked text files selected by your configuration.
32 tracked text files selected by your configuration.
33
33
34 Keywords are only expanded in local repositories and not stored in the
34 Keywords are only expanded in local repositories and not stored in the
35 change history. The mechanism can be regarded as a convenience for the
35 change history. The mechanism can be regarded as a convenience for the
36 current user or for archive distribution.
36 current user or for archive distribution.
37
37
38 Keywords expand to the changeset data pertaining to the latest change
38 Keywords expand to the changeset data pertaining to the latest change
39 relative to the working directory parent of each file.
39 relative to the working directory parent of each file.
40
40
41 Configuration is done in the [keyword], [keywordset] and [keywordmaps]
41 Configuration is done in the [keyword], [keywordset] and [keywordmaps]
42 sections of hgrc files.
42 sections of hgrc files.
43
43
44 Example::
44 Example::
45
45
46 [keyword]
46 [keyword]
47 # expand keywords in every python file except those matching "x*"
47 # expand keywords in every python file except those matching "x*"
48 **.py =
48 **.py =
49 x* = ignore
49 x* = ignore
50
50
51 [keywordset]
51 [keywordset]
52 # prefer svn- over cvs-like default keywordmaps
52 # prefer svn- over cvs-like default keywordmaps
53 svn = True
53 svn = True
54
54
55 .. note::
55 .. note::
56 The more specific you are in your filename patterns the less you
56 The more specific you are in your filename patterns the less you
57 lose speed in huge repositories.
57 lose speed in huge repositories.
58
58
59 For [keywordmaps] template mapping and expansion demonstration and
59 For [keywordmaps] template mapping and expansion demonstration and
60 control run :hg:`kwdemo`. See :hg:`help templates` for a list of
60 control run :hg:`kwdemo`. See :hg:`help templates` for a list of
61 available templates and filters.
61 available templates and filters.
62
62
63 Three additional date template filters are provided::
63 Three additional date template filters are provided::
64
64
65 utcdate "2006/09/18 15:13:13"
65 utcdate "2006/09/18 15:13:13"
66 svnutcdate "2006-09-18 15:13:13Z"
66 svnutcdate "2006-09-18 15:13:13Z"
67 svnisodate "2006-09-18 08:13:13 -700 (Mon, 18 Sep 2006)"
67 svnisodate "2006-09-18 08:13:13 -700 (Mon, 18 Sep 2006)"
68
68
69 The default template mappings (view with :hg:`kwdemo -d`) can be
69 The default template mappings (view with :hg:`kwdemo -d`) can be
70 replaced with customized keywords and templates. Again, run
70 replaced with customized keywords and templates. Again, run
71 :hg:`kwdemo` to control the results of your config changes.
71 :hg:`kwdemo` to control the results of your config changes.
72
72
73 Before changing/disabling active keywords, run :hg:`kwshrink` to avoid
73 Before changing/disabling active keywords, run :hg:`kwshrink` to avoid
74 the risk of inadvertently storing expanded keywords in the change
74 the risk of inadvertently storing expanded keywords in the change
75 history.
75 history.
76
76
77 To force expansion after enabling it, or a configuration change, run
77 To force expansion after enabling it, or a configuration change, run
78 :hg:`kwexpand`.
78 :hg:`kwexpand`.
79
79
80 Expansions spanning more than one line and incremental expansions,
80 Expansions spanning more than one line and incremental expansions,
81 like CVS' $Log$, are not supported. A keyword template map "Log =
81 like CVS' $Log$, are not supported. A keyword template map "Log =
82 {desc}" expands to the first line of the changeset description.
82 {desc}" expands to the first line of the changeset description.
83 '''
83 '''
84
84
85 from mercurial import commands, context, cmdutil, dispatch, filelog, extensions
85 from mercurial import commands, context, cmdutil, dispatch, filelog, extensions
86 from mercurial import localrepo, match, patch, templatefilters, templater, util
86 from mercurial import localrepo, match, patch, templatefilters, templater, util
87 from mercurial.hgweb import webcommands
87 from mercurial.hgweb import webcommands
88 from mercurial.i18n import _
88 from mercurial.i18n import _
89 import re, shutil, tempfile
89 import re, shutil, tempfile
90
90
91 commands.optionalrepo += ' kwdemo'
91 commands.optionalrepo += ' kwdemo'
92
92
93 # hg commands that do not act on keywords
93 # hg commands that do not act on keywords
94 nokwcommands = ('add addremove annotate bundle export grep incoming init log'
94 nokwcommands = ('add addremove annotate bundle export grep incoming init log'
95 ' outgoing push tip verify convert email glog')
95 ' outgoing push tip verify convert email glog')
96
96
97 # hg commands that trigger expansion only when writing to working dir,
97 # hg commands that trigger expansion only when writing to working dir,
98 # not when reading filelog, and unexpand when reading from working dir
98 # not when reading filelog, and unexpand when reading from working dir
99 restricted = 'merge kwexpand kwshrink record qrecord resolve transplant'
99 restricted = 'merge kwexpand kwshrink record qrecord resolve transplant'
100
100
101 # names of extensions using dorecord
101 # names of extensions using dorecord
102 recordextensions = 'record'
102 recordextensions = 'record'
103
103
104 # date like in cvs' $Date
104 # date like in cvs' $Date
105 utcdate = lambda x: util.datestr((x[0], 0), '%Y/%m/%d %H:%M:%S')
105 utcdate = lambda x: util.datestr((x[0], 0), '%Y/%m/%d %H:%M:%S')
106 # date like in svn's $Date
106 # date like in svn's $Date
107 svnisodate = lambda x: util.datestr(x, '%Y-%m-%d %H:%M:%S %1%2 (%a, %d %b %Y)')
107 svnisodate = lambda x: util.datestr(x, '%Y-%m-%d %H:%M:%S %1%2 (%a, %d %b %Y)')
108 # date like in svn's $Id
108 # date like in svn's $Id
109 svnutcdate = lambda x: util.datestr((x[0], 0), '%Y-%m-%d %H:%M:%SZ')
109 svnutcdate = lambda x: util.datestr((x[0], 0), '%Y-%m-%d %H:%M:%SZ')
110
110
111 # make keyword tools accessible
111 # make keyword tools accessible
112 kwtools = {'templater': None, 'hgcmd': ''}
112 kwtools = {'templater': None, 'hgcmd': ''}
113
113
114
114
115 def _defaultkwmaps(ui):
115 def _defaultkwmaps(ui):
116 '''Returns default keywordmaps according to keywordset configuration.'''
116 '''Returns default keywordmaps according to keywordset configuration.'''
117 templates = {
117 templates = {
118 'Revision': '{node|short}',
118 'Revision': '{node|short}',
119 'Author': '{author|user}',
119 'Author': '{author|user}',
120 }
120 }
121 kwsets = ({
121 kwsets = ({
122 'Date': '{date|utcdate}',
122 'Date': '{date|utcdate}',
123 'RCSfile': '{file|basename},v',
123 'RCSfile': '{file|basename},v',
124 'RCSFile': '{file|basename},v', # kept for backwards compatibility
124 'RCSFile': '{file|basename},v', # kept for backwards compatibility
125 # with hg-keyword
125 # with hg-keyword
126 'Source': '{root}/{file},v',
126 'Source': '{root}/{file},v',
127 'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
127 'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
128 'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
128 'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
129 }, {
129 }, {
130 'Date': '{date|svnisodate}',
130 'Date': '{date|svnisodate}',
131 'Id': '{file|basename},v {node|short} {date|svnutcdate} {author|user}',
131 'Id': '{file|basename},v {node|short} {date|svnutcdate} {author|user}',
132 'LastChangedRevision': '{node|short}',
132 'LastChangedRevision': '{node|short}',
133 'LastChangedBy': '{author|user}',
133 'LastChangedBy': '{author|user}',
134 'LastChangedDate': '{date|svnisodate}',
134 'LastChangedDate': '{date|svnisodate}',
135 })
135 })
136 templates.update(kwsets[ui.configbool('keywordset', 'svn')])
136 templates.update(kwsets[ui.configbool('keywordset', 'svn')])
137 return templates
137 return templates
138
138
139 def _shrinktext(text, subfunc):
139 def _shrinktext(text, subfunc):
140 '''Helper for keyword expansion removal in text.
140 '''Helper for keyword expansion removal in text.
141 Depending on subfunc also returns number of substitutions.'''
141 Depending on subfunc also returns number of substitutions.'''
142 return subfunc(r'$\1$', text)
142 return subfunc(r'$\1$', text)
143
143
144 def _preselect(wstatus, changed):
145 '''Retrieves modfied and added files from a working directory state
146 and returns the subset of each contained in given changed files
147 retrieved from a change context.'''
148 modified, added = wstatus[:2]
149 modified = [f for f in modified if f in changed]
150 added = [f for f in added if f in changed]
151 return modified, added
152
144
153
145 class kwtemplater(object):
154 class kwtemplater(object):
146 '''
155 '''
147 Sets up keyword templates, corresponding keyword regex, and
156 Sets up keyword templates, corresponding keyword regex, and
148 provides keyword substitution functions.
157 provides keyword substitution functions.
149 '''
158 '''
150
159
151 def __init__(self, ui, repo, inc, exc):
160 def __init__(self, ui, repo, inc, exc):
152 self.ui = ui
161 self.ui = ui
153 self.repo = repo
162 self.repo = repo
154 self.match = match.match(repo.root, '', [], inc, exc)
163 self.match = match.match(repo.root, '', [], inc, exc)
155 self.restrict = kwtools['hgcmd'] in restricted.split()
164 self.restrict = kwtools['hgcmd'] in restricted.split()
156 self.record = False
165 self.record = False
157
166
158 kwmaps = self.ui.configitems('keywordmaps')
167 kwmaps = self.ui.configitems('keywordmaps')
159 if kwmaps: # override default templates
168 if kwmaps: # override default templates
160 self.templates = dict((k, templater.parsestring(v, False))
169 self.templates = dict((k, templater.parsestring(v, False))
161 for k, v in kwmaps)
170 for k, v in kwmaps)
162 else:
171 else:
163 self.templates = _defaultkwmaps(self.ui)
172 self.templates = _defaultkwmaps(self.ui)
164 escaped = '|'.join(map(re.escape, self.templates.keys()))
173 escaped = '|'.join(map(re.escape, self.templates.keys()))
165 self.re_kw = re.compile(r'\$(%s)\$' % escaped)
174 self.re_kw = re.compile(r'\$(%s)\$' % escaped)
166 self.re_kwexp = re.compile(r'\$(%s): [^$\n\r]*? \$' % escaped)
175 self.re_kwexp = re.compile(r'\$(%s): [^$\n\r]*? \$' % escaped)
167
176
168 templatefilters.filters.update({'utcdate': utcdate,
177 templatefilters.filters.update({'utcdate': utcdate,
169 'svnisodate': svnisodate,
178 'svnisodate': svnisodate,
170 'svnutcdate': svnutcdate})
179 'svnutcdate': svnutcdate})
171
180
172 def substitute(self, data, path, ctx, subfunc):
181 def substitute(self, data, path, ctx, subfunc):
173 '''Replaces keywords in data with expanded template.'''
182 '''Replaces keywords in data with expanded template.'''
174 def kwsub(mobj):
183 def kwsub(mobj):
175 kw = mobj.group(1)
184 kw = mobj.group(1)
176 ct = cmdutil.changeset_templater(self.ui, self.repo,
185 ct = cmdutil.changeset_templater(self.ui, self.repo,
177 False, None, '', False)
186 False, None, '', False)
178 ct.use_template(self.templates[kw])
187 ct.use_template(self.templates[kw])
179 self.ui.pushbuffer()
188 self.ui.pushbuffer()
180 ct.show(ctx, root=self.repo.root, file=path)
189 ct.show(ctx, root=self.repo.root, file=path)
181 ekw = templatefilters.firstline(self.ui.popbuffer())
190 ekw = templatefilters.firstline(self.ui.popbuffer())
182 return '$%s: %s $' % (kw, ekw)
191 return '$%s: %s $' % (kw, ekw)
183 return subfunc(kwsub, data)
192 return subfunc(kwsub, data)
184
193
185 def expand(self, path, node, data):
194 def expand(self, path, node, data):
186 '''Returns data with keywords expanded.'''
195 '''Returns data with keywords expanded.'''
187 if not self.restrict and self.match(path) and not util.binary(data):
196 if not self.restrict and self.match(path) and not util.binary(data):
188 ctx = self.repo.filectx(path, fileid=node).changectx()
197 ctx = self.repo.filectx(path, fileid=node).changectx()
189 return self.substitute(data, path, ctx, self.re_kw.sub)
198 return self.substitute(data, path, ctx, self.re_kw.sub)
190 return data
199 return data
191
200
192 def iskwfile(self, cand, ctx):
201 def iskwfile(self, cand, ctx):
193 '''Returns subset of candidates which are configured for keyword
202 '''Returns subset of candidates which are configured for keyword
194 expansion are not symbolic links.'''
203 expansion are not symbolic links.'''
195 return [f for f in cand if self.match(f) and not 'l' in ctx.flags(f)]
204 return [f for f in cand if self.match(f) and not 'l' in ctx.flags(f)]
196
205
197 def overwrite(self, ctx, candidates, lookup, expand, rekw=False):
206 def overwrite(self, ctx, candidates, lookup, expand, rekw=False):
198 '''Overwrites selected files expanding/shrinking keywords.'''
207 '''Overwrites selected files expanding/shrinking keywords.'''
199 if self.restrict or lookup: # exclude kw_copy
208 if self.restrict or lookup: # exclude kw_copy
200 candidates = self.iskwfile(candidates, ctx)
209 candidates = self.iskwfile(candidates, ctx)
201 if not candidates:
210 if not candidates:
202 return
211 return
203 commit = self.restrict and not lookup
212 commit = self.restrict and not lookup
204 if self.restrict or expand and lookup:
213 if self.restrict or expand and lookup:
205 mf = ctx.manifest()
214 mf = ctx.manifest()
206 fctx = ctx
215 fctx = ctx
207 subn = (self.restrict or rekw) and self.re_kw.subn or self.re_kwexp.subn
216 subn = (self.restrict or rekw) and self.re_kw.subn or self.re_kwexp.subn
208 msg = (expand and _('overwriting %s expanding keywords\n')
217 msg = (expand and _('overwriting %s expanding keywords\n')
209 or _('overwriting %s shrinking keywords\n'))
218 or _('overwriting %s shrinking keywords\n'))
210 for f in candidates:
219 for f in candidates:
211 if self.restrict:
220 if self.restrict:
212 data = self.repo.file(f).read(mf[f])
221 data = self.repo.file(f).read(mf[f])
213 else:
222 else:
214 data = self.repo.wread(f)
223 data = self.repo.wread(f)
215 if util.binary(data):
224 if util.binary(data):
216 continue
225 continue
217 if expand:
226 if expand:
218 if lookup:
227 if lookup:
219 fctx = self.repo.filectx(f, fileid=mf[f]).changectx()
228 fctx = self.repo.filectx(f, fileid=mf[f]).changectx()
220 data, found = self.substitute(data, f, fctx, subn)
229 data, found = self.substitute(data, f, fctx, subn)
221 elif self.restrict:
230 elif self.restrict:
222 found = self.re_kw.search(data)
231 found = self.re_kw.search(data)
223 else:
232 else:
224 data, found = _shrinktext(data, subn)
233 data, found = _shrinktext(data, subn)
225 if found:
234 if found:
226 self.ui.note(msg % f)
235 self.ui.note(msg % f)
227 self.repo.wwrite(f, data, ctx.flags(f))
236 self.repo.wwrite(f, data, ctx.flags(f))
228 if commit:
237 if commit:
229 self.repo.dirstate.normal(f)
238 self.repo.dirstate.normal(f)
230 elif self.record:
239 elif self.record:
231 self.repo.dirstate.normallookup(f)
240 self.repo.dirstate.normallookup(f)
232
241
233 def shrink(self, fname, text):
242 def shrink(self, fname, text):
234 '''Returns text with all keyword substitutions removed.'''
243 '''Returns text with all keyword substitutions removed.'''
235 if self.match(fname) and not util.binary(text):
244 if self.match(fname) and not util.binary(text):
236 return _shrinktext(text, self.re_kwexp.sub)
245 return _shrinktext(text, self.re_kwexp.sub)
237 return text
246 return text
238
247
239 def shrinklines(self, fname, lines):
248 def shrinklines(self, fname, lines):
240 '''Returns lines with keyword substitutions removed.'''
249 '''Returns lines with keyword substitutions removed.'''
241 if self.match(fname):
250 if self.match(fname):
242 text = ''.join(lines)
251 text = ''.join(lines)
243 if not util.binary(text):
252 if not util.binary(text):
244 return _shrinktext(text, self.re_kwexp.sub).splitlines(True)
253 return _shrinktext(text, self.re_kwexp.sub).splitlines(True)
245 return lines
254 return lines
246
255
247 def wread(self, fname, data):
256 def wread(self, fname, data):
248 '''If in restricted mode returns data read from wdir with
257 '''If in restricted mode returns data read from wdir with
249 keyword substitutions removed.'''
258 keyword substitutions removed.'''
250 return self.restrict and self.shrink(fname, data) or data
259 return self.restrict and self.shrink(fname, data) or data
251
260
252 class kwfilelog(filelog.filelog):
261 class kwfilelog(filelog.filelog):
253 '''
262 '''
254 Subclass of filelog to hook into its read, add, cmp methods.
263 Subclass of filelog to hook into its read, add, cmp methods.
255 Keywords are "stored" unexpanded, and processed on reading.
264 Keywords are "stored" unexpanded, and processed on reading.
256 '''
265 '''
257 def __init__(self, opener, kwt, path):
266 def __init__(self, opener, kwt, path):
258 super(kwfilelog, self).__init__(opener, path)
267 super(kwfilelog, self).__init__(opener, path)
259 self.kwt = kwt
268 self.kwt = kwt
260 self.path = path
269 self.path = path
261
270
262 def read(self, node):
271 def read(self, node):
263 '''Expands keywords when reading filelog.'''
272 '''Expands keywords when reading filelog.'''
264 data = super(kwfilelog, self).read(node)
273 data = super(kwfilelog, self).read(node)
265 if self.renamed(node):
274 if self.renamed(node):
266 return data
275 return data
267 return self.kwt.expand(self.path, node, data)
276 return self.kwt.expand(self.path, node, data)
268
277
269 def add(self, text, meta, tr, link, p1=None, p2=None):
278 def add(self, text, meta, tr, link, p1=None, p2=None):
270 '''Removes keyword substitutions when adding to filelog.'''
279 '''Removes keyword substitutions when adding to filelog.'''
271 text = self.kwt.shrink(self.path, text)
280 text = self.kwt.shrink(self.path, text)
272 return super(kwfilelog, self).add(text, meta, tr, link, p1, p2)
281 return super(kwfilelog, self).add(text, meta, tr, link, p1, p2)
273
282
274 def cmp(self, node, text):
283 def cmp(self, node, text):
275 '''Removes keyword substitutions for comparison.'''
284 '''Removes keyword substitutions for comparison.'''
276 text = self.kwt.shrink(self.path, text)
285 text = self.kwt.shrink(self.path, text)
277 return super(kwfilelog, self).cmp(node, text)
286 return super(kwfilelog, self).cmp(node, text)
278
287
279 def _status(ui, repo, kwt, *pats, **opts):
288 def _status(ui, repo, kwt, *pats, **opts):
280 '''Bails out if [keyword] configuration is not active.
289 '''Bails out if [keyword] configuration is not active.
281 Returns status of working directory.'''
290 Returns status of working directory.'''
282 if kwt:
291 if kwt:
283 return repo.status(match=cmdutil.match(repo, pats, opts), clean=True,
292 return repo.status(match=cmdutil.match(repo, pats, opts), clean=True,
284 unknown=opts.get('unknown') or opts.get('all'))
293 unknown=opts.get('unknown') or opts.get('all'))
285 if ui.configitems('keyword'):
294 if ui.configitems('keyword'):
286 raise util.Abort(_('[keyword] patterns cannot match'))
295 raise util.Abort(_('[keyword] patterns cannot match'))
287 raise util.Abort(_('no [keyword] patterns configured'))
296 raise util.Abort(_('no [keyword] patterns configured'))
288
297
289 def _kwfwrite(ui, repo, expand, *pats, **opts):
298 def _kwfwrite(ui, repo, expand, *pats, **opts):
290 '''Selects files and passes them to kwtemplater.overwrite.'''
299 '''Selects files and passes them to kwtemplater.overwrite.'''
291 wctx = repo[None]
300 wctx = repo[None]
292 if len(wctx.parents()) > 1:
301 if len(wctx.parents()) > 1:
293 raise util.Abort(_('outstanding uncommitted merge'))
302 raise util.Abort(_('outstanding uncommitted merge'))
294 kwt = kwtools['templater']
303 kwt = kwtools['templater']
295 wlock = repo.wlock()
304 wlock = repo.wlock()
296 try:
305 try:
297 status = _status(ui, repo, kwt, *pats, **opts)
306 status = _status(ui, repo, kwt, *pats, **opts)
298 modified, added, removed, deleted, unknown, ignored, clean = status
307 modified, added, removed, deleted, unknown, ignored, clean = status
299 if modified or added or removed or deleted:
308 if modified or added or removed or deleted:
300 raise util.Abort(_('outstanding uncommitted changes'))
309 raise util.Abort(_('outstanding uncommitted changes'))
301 kwt.overwrite(wctx, clean, True, expand)
310 kwt.overwrite(wctx, clean, True, expand)
302 finally:
311 finally:
303 wlock.release()
312 wlock.release()
304
313
305 def demo(ui, repo, *args, **opts):
314 def demo(ui, repo, *args, **opts):
306 '''print [keywordmaps] configuration and an expansion example
315 '''print [keywordmaps] configuration and an expansion example
307
316
308 Show current, custom, or default keyword template maps and their
317 Show current, custom, or default keyword template maps and their
309 expansions.
318 expansions.
310
319
311 Extend the current configuration by specifying maps as arguments
320 Extend the current configuration by specifying maps as arguments
312 and using -f/--rcfile to source an external hgrc file.
321 and using -f/--rcfile to source an external hgrc file.
313
322
314 Use -d/--default to disable current configuration.
323 Use -d/--default to disable current configuration.
315
324
316 See :hg:`help templates` for information on templates and filters.
325 See :hg:`help templates` for information on templates and filters.
317 '''
326 '''
318 def demoitems(section, items):
327 def demoitems(section, items):
319 ui.write('[%s]\n' % section)
328 ui.write('[%s]\n' % section)
320 for k, v in sorted(items):
329 for k, v in sorted(items):
321 ui.write('%s = %s\n' % (k, v))
330 ui.write('%s = %s\n' % (k, v))
322
331
323 fn = 'demo.txt'
332 fn = 'demo.txt'
324 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
333 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
325 ui.note(_('creating temporary repository at %s\n') % tmpdir)
334 ui.note(_('creating temporary repository at %s\n') % tmpdir)
326 repo = localrepo.localrepository(ui, tmpdir, True)
335 repo = localrepo.localrepository(ui, tmpdir, True)
327 ui.setconfig('keyword', fn, '')
336 ui.setconfig('keyword', fn, '')
328
337
329 uikwmaps = ui.configitems('keywordmaps')
338 uikwmaps = ui.configitems('keywordmaps')
330 if args or opts.get('rcfile'):
339 if args or opts.get('rcfile'):
331 ui.status(_('\n\tconfiguration using custom keyword template maps\n'))
340 ui.status(_('\n\tconfiguration using custom keyword template maps\n'))
332 if uikwmaps:
341 if uikwmaps:
333 ui.status(_('\textending current template maps\n'))
342 ui.status(_('\textending current template maps\n'))
334 if opts.get('default') or not uikwmaps:
343 if opts.get('default') or not uikwmaps:
335 ui.status(_('\toverriding default template maps\n'))
344 ui.status(_('\toverriding default template maps\n'))
336 if opts.get('rcfile'):
345 if opts.get('rcfile'):
337 ui.readconfig(opts.get('rcfile'))
346 ui.readconfig(opts.get('rcfile'))
338 if args:
347 if args:
339 # simulate hgrc parsing
348 # simulate hgrc parsing
340 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
349 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
341 fp = repo.opener('hgrc', 'w')
350 fp = repo.opener('hgrc', 'w')
342 fp.writelines(rcmaps)
351 fp.writelines(rcmaps)
343 fp.close()
352 fp.close()
344 ui.readconfig(repo.join('hgrc'))
353 ui.readconfig(repo.join('hgrc'))
345 kwmaps = dict(ui.configitems('keywordmaps'))
354 kwmaps = dict(ui.configitems('keywordmaps'))
346 elif opts.get('default'):
355 elif opts.get('default'):
347 ui.status(_('\n\tconfiguration using default keyword template maps\n'))
356 ui.status(_('\n\tconfiguration using default keyword template maps\n'))
348 kwmaps = _defaultkwmaps(ui)
357 kwmaps = _defaultkwmaps(ui)
349 if uikwmaps:
358 if uikwmaps:
350 ui.status(_('\tdisabling current template maps\n'))
359 ui.status(_('\tdisabling current template maps\n'))
351 for k, v in kwmaps.iteritems():
360 for k, v in kwmaps.iteritems():
352 ui.setconfig('keywordmaps', k, v)
361 ui.setconfig('keywordmaps', k, v)
353 else:
362 else:
354 ui.status(_('\n\tconfiguration using current keyword template maps\n'))
363 ui.status(_('\n\tconfiguration using current keyword template maps\n'))
355 kwmaps = dict(uikwmaps) or _defaultkwmaps(ui)
364 kwmaps = dict(uikwmaps) or _defaultkwmaps(ui)
356
365
357 uisetup(ui)
366 uisetup(ui)
358 reposetup(ui, repo)
367 reposetup(ui, repo)
359 ui.write('[extensions]\nkeyword =\n')
368 ui.write('[extensions]\nkeyword =\n')
360 demoitems('keyword', ui.configitems('keyword'))
369 demoitems('keyword', ui.configitems('keyword'))
361 demoitems('keywordmaps', kwmaps.iteritems())
370 demoitems('keywordmaps', kwmaps.iteritems())
362 keywords = '$' + '$\n$'.join(sorted(kwmaps.keys())) + '$\n'
371 keywords = '$' + '$\n$'.join(sorted(kwmaps.keys())) + '$\n'
363 repo.wopener(fn, 'w').write(keywords)
372 repo.wopener(fn, 'w').write(keywords)
364 repo[None].add([fn])
373 repo[None].add([fn])
365 ui.note(_('\nkeywords written to %s:\n') % fn)
374 ui.note(_('\nkeywords written to %s:\n') % fn)
366 ui.note(keywords)
375 ui.note(keywords)
367 repo.dirstate.setbranch('demobranch')
376 repo.dirstate.setbranch('demobranch')
368 for name, cmd in ui.configitems('hooks'):
377 for name, cmd in ui.configitems('hooks'):
369 if name.split('.', 1)[0].find('commit') > -1:
378 if name.split('.', 1)[0].find('commit') > -1:
370 repo.ui.setconfig('hooks', name, '')
379 repo.ui.setconfig('hooks', name, '')
371 msg = _('hg keyword configuration and expansion example')
380 msg = _('hg keyword configuration and expansion example')
372 ui.note("hg ci -m '%s'\n" % msg)
381 ui.note("hg ci -m '%s'\n" % msg)
373 repo.commit(text=msg)
382 repo.commit(text=msg)
374 ui.status(_('\n\tkeywords expanded\n'))
383 ui.status(_('\n\tkeywords expanded\n'))
375 ui.write(repo.wread(fn))
384 ui.write(repo.wread(fn))
376 shutil.rmtree(tmpdir, ignore_errors=True)
385 shutil.rmtree(tmpdir, ignore_errors=True)
377
386
378 def expand(ui, repo, *pats, **opts):
387 def expand(ui, repo, *pats, **opts):
379 '''expand keywords in the working directory
388 '''expand keywords in the working directory
380
389
381 Run after (re)enabling keyword expansion.
390 Run after (re)enabling keyword expansion.
382
391
383 kwexpand refuses to run if given files contain local changes.
392 kwexpand refuses to run if given files contain local changes.
384 '''
393 '''
385 # 3rd argument sets expansion to True
394 # 3rd argument sets expansion to True
386 _kwfwrite(ui, repo, True, *pats, **opts)
395 _kwfwrite(ui, repo, True, *pats, **opts)
387
396
388 def files(ui, repo, *pats, **opts):
397 def files(ui, repo, *pats, **opts):
389 '''show files configured for keyword expansion
398 '''show files configured for keyword expansion
390
399
391 List which files in the working directory are matched by the
400 List which files in the working directory are matched by the
392 [keyword] configuration patterns.
401 [keyword] configuration patterns.
393
402
394 Useful to prevent inadvertent keyword expansion and to speed up
403 Useful to prevent inadvertent keyword expansion and to speed up
395 execution by including only files that are actual candidates for
404 execution by including only files that are actual candidates for
396 expansion.
405 expansion.
397
406
398 See :hg:`help keyword` on how to construct patterns both for
407 See :hg:`help keyword` on how to construct patterns both for
399 inclusion and exclusion of files.
408 inclusion and exclusion of files.
400
409
401 With -A/--all and -v/--verbose the codes used to show the status
410 With -A/--all and -v/--verbose the codes used to show the status
402 of files are::
411 of files are::
403
412
404 K = keyword expansion candidate
413 K = keyword expansion candidate
405 k = keyword expansion candidate (not tracked)
414 k = keyword expansion candidate (not tracked)
406 I = ignored
415 I = ignored
407 i = ignored (not tracked)
416 i = ignored (not tracked)
408 '''
417 '''
409 kwt = kwtools['templater']
418 kwt = kwtools['templater']
410 status = _status(ui, repo, kwt, *pats, **opts)
419 status = _status(ui, repo, kwt, *pats, **opts)
411 cwd = pats and repo.getcwd() or ''
420 cwd = pats and repo.getcwd() or ''
412 modified, added, removed, deleted, unknown, ignored, clean = status
421 modified, added, removed, deleted, unknown, ignored, clean = status
413 files = []
422 files = []
414 if not opts.get('unknown') or opts.get('all'):
423 if not opts.get('unknown') or opts.get('all'):
415 files = sorted(modified + added + clean)
424 files = sorted(modified + added + clean)
416 wctx = repo[None]
425 wctx = repo[None]
417 kwfiles = kwt.iskwfile(files, wctx)
426 kwfiles = kwt.iskwfile(files, wctx)
418 kwunknown = kwt.iskwfile(unknown, wctx)
427 kwunknown = kwt.iskwfile(unknown, wctx)
419 if not opts.get('ignore') or opts.get('all'):
428 if not opts.get('ignore') or opts.get('all'):
420 showfiles = kwfiles, kwunknown
429 showfiles = kwfiles, kwunknown
421 else:
430 else:
422 showfiles = [], []
431 showfiles = [], []
423 if opts.get('all') or opts.get('ignore'):
432 if opts.get('all') or opts.get('ignore'):
424 showfiles += ([f for f in files if f not in kwfiles],
433 showfiles += ([f for f in files if f not in kwfiles],
425 [f for f in unknown if f not in kwunknown])
434 [f for f in unknown if f not in kwunknown])
426 for char, filenames in zip('KkIi', showfiles):
435 for char, filenames in zip('KkIi', showfiles):
427 fmt = (opts.get('all') or ui.verbose) and '%s %%s\n' % char or '%s\n'
436 fmt = (opts.get('all') or ui.verbose) and '%s %%s\n' % char or '%s\n'
428 for f in filenames:
437 for f in filenames:
429 ui.write(fmt % repo.pathto(f, cwd))
438 ui.write(fmt % repo.pathto(f, cwd))
430
439
431 def shrink(ui, repo, *pats, **opts):
440 def shrink(ui, repo, *pats, **opts):
432 '''revert expanded keywords in the working directory
441 '''revert expanded keywords in the working directory
433
442
434 Run before changing/disabling active keywords or if you experience
443 Run before changing/disabling active keywords or if you experience
435 problems with :hg:`import` or :hg:`merge`.
444 problems with :hg:`import` or :hg:`merge`.
436
445
437 kwshrink refuses to run if given files contain local changes.
446 kwshrink refuses to run if given files contain local changes.
438 '''
447 '''
439 # 3rd argument sets expansion to False
448 # 3rd argument sets expansion to False
440 _kwfwrite(ui, repo, False, *pats, **opts)
449 _kwfwrite(ui, repo, False, *pats, **opts)
441
450
442
451
443 def uisetup(ui):
452 def uisetup(ui):
444 ''' Monkeypatches dispatch._parse to retrieve user command.'''
453 ''' Monkeypatches dispatch._parse to retrieve user command.'''
445
454
446 def kwdispatch_parse(orig, ui, args):
455 def kwdispatch_parse(orig, ui, args):
447 '''Monkeypatch dispatch._parse to obtain running hg command.'''
456 '''Monkeypatch dispatch._parse to obtain running hg command.'''
448 cmd, func, args, options, cmdoptions = orig(ui, args)
457 cmd, func, args, options, cmdoptions = orig(ui, args)
449 kwtools['hgcmd'] = cmd
458 kwtools['hgcmd'] = cmd
450 return cmd, func, args, options, cmdoptions
459 return cmd, func, args, options, cmdoptions
451
460
452 extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse)
461 extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse)
453
462
454 def reposetup(ui, repo):
463 def reposetup(ui, repo):
455 '''Sets up repo as kwrepo for keyword substitution.
464 '''Sets up repo as kwrepo for keyword substitution.
456 Overrides file method to return kwfilelog instead of filelog
465 Overrides file method to return kwfilelog instead of filelog
457 if file matches user configuration.
466 if file matches user configuration.
458 Wraps commit to overwrite configured files with updated
467 Wraps commit to overwrite configured files with updated
459 keyword substitutions.
468 keyword substitutions.
460 Monkeypatches patch and webcommands.'''
469 Monkeypatches patch and webcommands.'''
461
470
462 try:
471 try:
463 if (not repo.local() or kwtools['hgcmd'] in nokwcommands.split()
472 if (not repo.local() or kwtools['hgcmd'] in nokwcommands.split()
464 or '.hg' in util.splitpath(repo.root)
473 or '.hg' in util.splitpath(repo.root)
465 or repo._url.startswith('bundle:')):
474 or repo._url.startswith('bundle:')):
466 return
475 return
467 except AttributeError:
476 except AttributeError:
468 pass
477 pass
469
478
470 inc, exc = [], ['.hg*']
479 inc, exc = [], ['.hg*']
471 for pat, opt in ui.configitems('keyword'):
480 for pat, opt in ui.configitems('keyword'):
472 if opt != 'ignore':
481 if opt != 'ignore':
473 inc.append(pat)
482 inc.append(pat)
474 else:
483 else:
475 exc.append(pat)
484 exc.append(pat)
476 if not inc:
485 if not inc:
477 return
486 return
478
487
479 kwtools['templater'] = kwt = kwtemplater(ui, repo, inc, exc)
488 kwtools['templater'] = kwt = kwtemplater(ui, repo, inc, exc)
480
489
481 class kwrepo(repo.__class__):
490 class kwrepo(repo.__class__):
482 def file(self, f):
491 def file(self, f):
483 if f[0] == '/':
492 if f[0] == '/':
484 f = f[1:]
493 f = f[1:]
485 return kwfilelog(self.sopener, kwt, f)
494 return kwfilelog(self.sopener, kwt, f)
486
495
487 def wread(self, filename):
496 def wread(self, filename):
488 data = super(kwrepo, self).wread(filename)
497 data = super(kwrepo, self).wread(filename)
489 return kwt.wread(filename, data)
498 return kwt.wread(filename, data)
490
499
491 def commit(self, *args, **opts):
500 def commit(self, *args, **opts):
492 # use custom commitctx for user commands
501 # use custom commitctx for user commands
493 # other extensions can still wrap repo.commitctx directly
502 # other extensions can still wrap repo.commitctx directly
494 self.commitctx = self.kwcommitctx
503 self.commitctx = self.kwcommitctx
495 try:
504 try:
496 return super(kwrepo, self).commit(*args, **opts)
505 return super(kwrepo, self).commit(*args, **opts)
497 finally:
506 finally:
498 del self.commitctx
507 del self.commitctx
499
508
500 def kwcommitctx(self, ctx, error=False):
509 def kwcommitctx(self, ctx, error=False):
501 n = super(kwrepo, self).commitctx(ctx, error)
510 n = super(kwrepo, self).commitctx(ctx, error)
502 # no lock needed, only called from repo.commit() which already locks
511 # no lock needed, only called from repo.commit() which already locks
503 if not kwt.record:
512 if not kwt.record:
504 restrict = kwt.restrict
513 restrict = kwt.restrict
505 kwt.restrict = True
514 kwt.restrict = True
506 kwt.overwrite(self[n], sorted(ctx.added() + ctx.modified()),
515 kwt.overwrite(self[n], sorted(ctx.added() + ctx.modified()),
507 False, True)
516 False, True)
508 kwt.restrict = restrict
517 kwt.restrict = restrict
509 return n
518 return n
510
519
511 def rollback(self, dryrun=False):
520 def rollback(self, dryrun=False):
512 wlock = repo.wlock()
521 wlock = self.wlock()
513 try:
522 try:
514 if not dryrun:
523 if not dryrun:
515 changed = self['.'].files()
524 changed = self['.'].files()
516 ret = super(kwrepo, self).rollback(dryrun)
525 ret = super(kwrepo, self).rollback(dryrun)
517 if not dryrun:
526 if not dryrun:
518 ctx = self['.']
527 ctx = self['.']
519 modified, added = self[None].status()[:2]
528 modified, added = _preselect(self[None].status(), changed)
520 modified = [f for f in modified if f in changed]
529 kwt.overwrite(ctx, modified, True, True)
521 added = [f for f in added if f in changed]
522 kwt.overwrite(ctx, added, True, False)
530 kwt.overwrite(ctx, added, True, False)
523 kwt.overwrite(ctx, modified, True, True)
524 return ret
531 return ret
525 finally:
532 finally:
526 wlock.release()
533 wlock.release()
527
534
528 # monkeypatches
535 # monkeypatches
529 def kwpatchfile_init(orig, self, ui, fname, opener,
536 def kwpatchfile_init(orig, self, ui, fname, opener,
530 missing=False, eolmode=None):
537 missing=False, eolmode=None):
531 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
538 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
532 rejects or conflicts due to expanded keywords in working dir.'''
539 rejects or conflicts due to expanded keywords in working dir.'''
533 orig(self, ui, fname, opener, missing, eolmode)
540 orig(self, ui, fname, opener, missing, eolmode)
534 # shrink keywords read from working dir
541 # shrink keywords read from working dir
535 self.lines = kwt.shrinklines(self.fname, self.lines)
542 self.lines = kwt.shrinklines(self.fname, self.lines)
536
543
537 def kw_diff(orig, repo, node1=None, node2=None, match=None, changes=None,
544 def kw_diff(orig, repo, node1=None, node2=None, match=None, changes=None,
538 opts=None, prefix=''):
545 opts=None, prefix=''):
539 '''Monkeypatch patch.diff to avoid expansion.'''
546 '''Monkeypatch patch.diff to avoid expansion.'''
540 kwt.restrict = True
547 kwt.restrict = True
541 return orig(repo, node1, node2, match, changes, opts, prefix)
548 return orig(repo, node1, node2, match, changes, opts, prefix)
542
549
543 def kwweb_skip(orig, web, req, tmpl):
550 def kwweb_skip(orig, web, req, tmpl):
544 '''Wraps webcommands.x turning off keyword expansion.'''
551 '''Wraps webcommands.x turning off keyword expansion.'''
545 kwt.match = util.never
552 kwt.match = util.never
546 return orig(web, req, tmpl)
553 return orig(web, req, tmpl)
547
554
548 def kw_copy(orig, ui, repo, pats, opts, rename=False):
555 def kw_copy(orig, ui, repo, pats, opts, rename=False):
549 '''Wraps cmdutil.copy so that copy/rename destinations do not
556 '''Wraps cmdutil.copy so that copy/rename destinations do not
550 contain expanded keywords.
557 contain expanded keywords.
551 Note that the source may also be a symlink as:
558 Note that the source may also be a symlink as:
552 hg cp sym x -> x is symlink
559 hg cp sym x -> x is symlink
553 cp sym x; hg cp -A sym x -> x is file (maybe expanded keywords)
560 cp sym x; hg cp -A sym x -> x is file (maybe expanded keywords)
554 '''
561 '''
555 orig(ui, repo, pats, opts, rename)
562 orig(ui, repo, pats, opts, rename)
556 if opts.get('dry_run'):
563 if opts.get('dry_run'):
557 return
564 return
558 wctx = repo[None]
565 wctx = repo[None]
559 candidates = [f for f in repo.dirstate.copies() if
566 candidates = [f for f in repo.dirstate.copies() if
560 kwt.match(repo.dirstate.copied(f)) and
567 kwt.match(repo.dirstate.copied(f)) and
561 not 'l' in wctx.flags(f)]
568 not 'l' in wctx.flags(f)]
562 kwt.overwrite(wctx, candidates, False, False)
569 kwt.overwrite(wctx, candidates, False, False)
563
570
564 def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts):
571 def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts):
565 '''Wraps record.dorecord expanding keywords after recording.'''
572 '''Wraps record.dorecord expanding keywords after recording.'''
566 wlock = repo.wlock()
573 wlock = repo.wlock()
567 try:
574 try:
568 # record returns 0 even when nothing has changed
575 # record returns 0 even when nothing has changed
569 # therefore compare nodes before and after
576 # therefore compare nodes before and after
570 kwt.record = True
577 kwt.record = True
571 ctx = repo['.']
578 ctx = repo['.']
572 modified, added = repo[None].status()[:2]
579 wstatus = repo[None].status()
573 ret = orig(ui, repo, commitfunc, *pats, **opts)
580 ret = orig(ui, repo, commitfunc, *pats, **opts)
574 recctx = repo['.']
581 recctx = repo['.']
575 if ctx != recctx:
582 if ctx != recctx:
576 changed = recctx.files()
583 modified, added = _preselect(wstatus, recctx.files())
577 modified = [f for f in modified if f in changed]
578 added = [f for f in added if f in changed]
579 kwt.restrict = False
584 kwt.restrict = False
580 kwt.overwrite(recctx, modified, False, True)
585 kwt.overwrite(recctx, modified, False, True)
581 kwt.overwrite(recctx, added, False, True, True)
586 kwt.overwrite(recctx, added, False, True, True)
582 kwt.restrict = True
587 kwt.restrict = True
583 return ret
588 return ret
584 finally:
589 finally:
585 wlock.release()
590 wlock.release()
586
591
587 repo.__class__ = kwrepo
592 repo.__class__ = kwrepo
588
593
589 def kwfilectx_cmp(orig, self, fctx):
594 def kwfilectx_cmp(orig, self, fctx):
590 # keyword affects data size, comparing wdir and filelog size does
595 # keyword affects data size, comparing wdir and filelog size does
591 # not make sense
596 # not make sense
592 return self._filelog.cmp(self._filenode, fctx.data())
597 return self._filelog.cmp(self._filenode, fctx.data())
593 extensions.wrapfunction(context.filectx, 'cmp', kwfilectx_cmp)
598 extensions.wrapfunction(context.filectx, 'cmp', kwfilectx_cmp)
594
599
595 extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init)
600 extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init)
596 extensions.wrapfunction(patch, 'diff', kw_diff)
601 extensions.wrapfunction(patch, 'diff', kw_diff)
597 extensions.wrapfunction(cmdutil, 'copy', kw_copy)
602 extensions.wrapfunction(cmdutil, 'copy', kw_copy)
598 for c in 'annotate changeset rev filediff diff'.split():
603 for c in 'annotate changeset rev filediff diff'.split():
599 extensions.wrapfunction(webcommands, c, kwweb_skip)
604 extensions.wrapfunction(webcommands, c, kwweb_skip)
600 for name in recordextensions.split():
605 for name in recordextensions.split():
601 try:
606 try:
602 record = extensions.find(name)
607 record = extensions.find(name)
603 extensions.wrapfunction(record, 'dorecord', kw_dorecord)
608 extensions.wrapfunction(record, 'dorecord', kw_dorecord)
604 except KeyError:
609 except KeyError:
605 pass
610 pass
606
611
607 cmdtable = {
612 cmdtable = {
608 'kwdemo':
613 'kwdemo':
609 (demo,
614 (demo,
610 [('d', 'default', None, _('show default keyword template maps')),
615 [('d', 'default', None, _('show default keyword template maps')),
611 ('f', 'rcfile', '',
616 ('f', 'rcfile', '',
612 _('read maps from rcfile'), _('FILE'))],
617 _('read maps from rcfile'), _('FILE'))],
613 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')),
618 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')),
614 'kwexpand': (expand, commands.walkopts,
619 'kwexpand': (expand, commands.walkopts,
615 _('hg kwexpand [OPTION]... [FILE]...')),
620 _('hg kwexpand [OPTION]... [FILE]...')),
616 'kwfiles':
621 'kwfiles':
617 (files,
622 (files,
618 [('A', 'all', None, _('show keyword status flags of all files')),
623 [('A', 'all', None, _('show keyword status flags of all files')),
619 ('i', 'ignore', None, _('show files excluded from expansion')),
624 ('i', 'ignore', None, _('show files excluded from expansion')),
620 ('u', 'unknown', None, _('only show unknown (not tracked) files')),
625 ('u', 'unknown', None, _('only show unknown (not tracked) files')),
621 ] + commands.walkopts,
626 ] + commands.walkopts,
622 _('hg kwfiles [OPTION]... [FILE]...')),
627 _('hg kwfiles [OPTION]... [FILE]...')),
623 'kwshrink': (shrink, commands.walkopts,
628 'kwshrink': (shrink, commands.walkopts,
624 _('hg kwshrink [OPTION]... [FILE]...')),
629 _('hg kwshrink [OPTION]... [FILE]...')),
625 }
630 }
General Comments 0
You need to be logged in to leave comments. Login now