##// END OF EJS Templates
keyword: only use expensive fctx.cmp when needed...
Christian Ebert -
r12732:4bca87c2 default
parent child Browse files
Show More
@@ -1,630 +1,635 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):
144 def _preselect(wstatus, changed):
145 '''Retrieves modfied and added files from a working directory state
145 '''Retrieves modfied and added files from a working directory state
146 and returns the subset of each contained in given changed files
146 and returns the subset of each contained in given changed files
147 retrieved from a change context.'''
147 retrieved from a change context.'''
148 modified, added = wstatus[:2]
148 modified, added = wstatus[:2]
149 modified = [f for f in modified if f in changed]
149 modified = [f for f in modified if f in changed]
150 added = [f for f in added if f in changed]
150 added = [f for f in added if f in changed]
151 return modified, added
151 return modified, added
152
152
153
153
154 class kwtemplater(object):
154 class kwtemplater(object):
155 '''
155 '''
156 Sets up keyword templates, corresponding keyword regex, and
156 Sets up keyword templates, corresponding keyword regex, and
157 provides keyword substitution functions.
157 provides keyword substitution functions.
158 '''
158 '''
159
159
160 def __init__(self, ui, repo, inc, exc):
160 def __init__(self, ui, repo, inc, exc):
161 self.ui = ui
161 self.ui = ui
162 self.repo = repo
162 self.repo = repo
163 self.match = match.match(repo.root, '', [], inc, exc)
163 self.match = match.match(repo.root, '', [], inc, exc)
164 self.restrict = kwtools['hgcmd'] in restricted.split()
164 self.restrict = kwtools['hgcmd'] in restricted.split()
165 self.record = False
165 self.record = False
166
166
167 kwmaps = self.ui.configitems('keywordmaps')
167 kwmaps = self.ui.configitems('keywordmaps')
168 if kwmaps: # override default templates
168 if kwmaps: # override default templates
169 self.templates = dict((k, templater.parsestring(v, False))
169 self.templates = dict((k, templater.parsestring(v, False))
170 for k, v in kwmaps)
170 for k, v in kwmaps)
171 else:
171 else:
172 self.templates = _defaultkwmaps(self.ui)
172 self.templates = _defaultkwmaps(self.ui)
173 escaped = '|'.join(map(re.escape, self.templates.keys()))
173 escaped = '|'.join(map(re.escape, self.templates.keys()))
174 self.re_kw = re.compile(r'\$(%s)\$' % escaped)
174 self.re_kw = re.compile(r'\$(%s)\$' % escaped)
175 self.re_kwexp = re.compile(r'\$(%s): [^$\n\r]*? \$' % escaped)
175 self.re_kwexp = re.compile(r'\$(%s): [^$\n\r]*? \$' % escaped)
176
176
177 templatefilters.filters.update({'utcdate': utcdate,
177 templatefilters.filters.update({'utcdate': utcdate,
178 'svnisodate': svnisodate,
178 'svnisodate': svnisodate,
179 'svnutcdate': svnutcdate})
179 'svnutcdate': svnutcdate})
180
180
181 def substitute(self, data, path, ctx, subfunc):
181 def substitute(self, data, path, ctx, subfunc):
182 '''Replaces keywords in data with expanded template.'''
182 '''Replaces keywords in data with expanded template.'''
183 def kwsub(mobj):
183 def kwsub(mobj):
184 kw = mobj.group(1)
184 kw = mobj.group(1)
185 ct = cmdutil.changeset_templater(self.ui, self.repo,
185 ct = cmdutil.changeset_templater(self.ui, self.repo,
186 False, None, '', False)
186 False, None, '', False)
187 ct.use_template(self.templates[kw])
187 ct.use_template(self.templates[kw])
188 self.ui.pushbuffer()
188 self.ui.pushbuffer()
189 ct.show(ctx, root=self.repo.root, file=path)
189 ct.show(ctx, root=self.repo.root, file=path)
190 ekw = templatefilters.firstline(self.ui.popbuffer())
190 ekw = templatefilters.firstline(self.ui.popbuffer())
191 return '$%s: %s $' % (kw, ekw)
191 return '$%s: %s $' % (kw, ekw)
192 return subfunc(kwsub, data)
192 return subfunc(kwsub, data)
193
193
194 def expand(self, path, node, data):
194 def expand(self, path, node, data):
195 '''Returns data with keywords expanded.'''
195 '''Returns data with keywords expanded.'''
196 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):
197 ctx = self.repo.filectx(path, fileid=node).changectx()
197 ctx = self.repo.filectx(path, fileid=node).changectx()
198 return self.substitute(data, path, ctx, self.re_kw.sub)
198 return self.substitute(data, path, ctx, self.re_kw.sub)
199 return data
199 return data
200
200
201 def iskwfile(self, cand, ctx):
201 def iskwfile(self, cand, ctx):
202 '''Returns subset of candidates which are configured for keyword
202 '''Returns subset of candidates which are configured for keyword
203 expansion are not symbolic links.'''
203 expansion are not symbolic links.'''
204 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)]
205
205
206 def overwrite(self, ctx, candidates, lookup, expand, rekw=False):
206 def overwrite(self, ctx, candidates, lookup, expand, rekw=False):
207 '''Overwrites selected files expanding/shrinking keywords.'''
207 '''Overwrites selected files expanding/shrinking keywords.'''
208 if self.restrict or lookup: # exclude kw_copy
208 if self.restrict or lookup: # exclude kw_copy
209 candidates = self.iskwfile(candidates, ctx)
209 candidates = self.iskwfile(candidates, ctx)
210 if not candidates:
210 if not candidates:
211 return
211 return
212 commit = self.restrict and not lookup
212 commit = self.restrict and not lookup
213 if self.restrict or expand and lookup:
213 if self.restrict or expand and lookup:
214 mf = ctx.manifest()
214 mf = ctx.manifest()
215 fctx = ctx
215 fctx = ctx
216 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
217 msg = (expand and _('overwriting %s expanding keywords\n')
217 msg = (expand and _('overwriting %s expanding keywords\n')
218 or _('overwriting %s shrinking keywords\n'))
218 or _('overwriting %s shrinking keywords\n'))
219 for f in candidates:
219 for f in candidates:
220 if self.restrict:
220 if self.restrict:
221 data = self.repo.file(f).read(mf[f])
221 data = self.repo.file(f).read(mf[f])
222 else:
222 else:
223 data = self.repo.wread(f)
223 data = self.repo.wread(f)
224 if util.binary(data):
224 if util.binary(data):
225 continue
225 continue
226 if expand:
226 if expand:
227 if lookup:
227 if lookup:
228 fctx = self.repo.filectx(f, fileid=mf[f]).changectx()
228 fctx = self.repo.filectx(f, fileid=mf[f]).changectx()
229 data, found = self.substitute(data, f, fctx, subn)
229 data, found = self.substitute(data, f, fctx, subn)
230 elif self.restrict:
230 elif self.restrict:
231 found = self.re_kw.search(data)
231 found = self.re_kw.search(data)
232 else:
232 else:
233 data, found = _shrinktext(data, subn)
233 data, found = _shrinktext(data, subn)
234 if found:
234 if found:
235 self.ui.note(msg % f)
235 self.ui.note(msg % f)
236 self.repo.wwrite(f, data, ctx.flags(f))
236 self.repo.wwrite(f, data, ctx.flags(f))
237 if commit:
237 if commit:
238 self.repo.dirstate.normal(f)
238 self.repo.dirstate.normal(f)
239 elif self.record:
239 elif self.record:
240 self.repo.dirstate.normallookup(f)
240 self.repo.dirstate.normallookup(f)
241
241
242 def shrink(self, fname, text):
242 def shrink(self, fname, text):
243 '''Returns text with all keyword substitutions removed.'''
243 '''Returns text with all keyword substitutions removed.'''
244 if self.match(fname) and not util.binary(text):
244 if self.match(fname) and not util.binary(text):
245 return _shrinktext(text, self.re_kwexp.sub)
245 return _shrinktext(text, self.re_kwexp.sub)
246 return text
246 return text
247
247
248 def shrinklines(self, fname, lines):
248 def shrinklines(self, fname, lines):
249 '''Returns lines with keyword substitutions removed.'''
249 '''Returns lines with keyword substitutions removed.'''
250 if self.match(fname):
250 if self.match(fname):
251 text = ''.join(lines)
251 text = ''.join(lines)
252 if not util.binary(text):
252 if not util.binary(text):
253 return _shrinktext(text, self.re_kwexp.sub).splitlines(True)
253 return _shrinktext(text, self.re_kwexp.sub).splitlines(True)
254 return lines
254 return lines
255
255
256 def wread(self, fname, data):
256 def wread(self, fname, data):
257 '''If in restricted mode returns data read from wdir with
257 '''If in restricted mode returns data read from wdir with
258 keyword substitutions removed.'''
258 keyword substitutions removed.'''
259 return self.restrict and self.shrink(fname, data) or data
259 return self.restrict and self.shrink(fname, data) or data
260
260
261 class kwfilelog(filelog.filelog):
261 class kwfilelog(filelog.filelog):
262 '''
262 '''
263 Subclass of filelog to hook into its read, add, cmp methods.
263 Subclass of filelog to hook into its read, add, cmp methods.
264 Keywords are "stored" unexpanded, and processed on reading.
264 Keywords are "stored" unexpanded, and processed on reading.
265 '''
265 '''
266 def __init__(self, opener, kwt, path):
266 def __init__(self, opener, kwt, path):
267 super(kwfilelog, self).__init__(opener, path)
267 super(kwfilelog, self).__init__(opener, path)
268 self.kwt = kwt
268 self.kwt = kwt
269 self.path = path
269 self.path = path
270
270
271 def read(self, node):
271 def read(self, node):
272 '''Expands keywords when reading filelog.'''
272 '''Expands keywords when reading filelog.'''
273 data = super(kwfilelog, self).read(node)
273 data = super(kwfilelog, self).read(node)
274 if self.renamed(node):
274 if self.renamed(node):
275 return data
275 return data
276 return self.kwt.expand(self.path, node, data)
276 return self.kwt.expand(self.path, node, data)
277
277
278 def add(self, text, meta, tr, link, p1=None, p2=None):
278 def add(self, text, meta, tr, link, p1=None, p2=None):
279 '''Removes keyword substitutions when adding to filelog.'''
279 '''Removes keyword substitutions when adding to filelog.'''
280 text = self.kwt.shrink(self.path, text)
280 text = self.kwt.shrink(self.path, text)
281 return super(kwfilelog, self).add(text, meta, tr, link, p1, p2)
281 return super(kwfilelog, self).add(text, meta, tr, link, p1, p2)
282
282
283 def cmp(self, node, text):
283 def cmp(self, node, text):
284 '''Removes keyword substitutions for comparison.'''
284 '''Removes keyword substitutions for comparison.'''
285 text = self.kwt.shrink(self.path, text)
285 text = self.kwt.shrink(self.path, text)
286 return super(kwfilelog, self).cmp(node, text)
286 return super(kwfilelog, self).cmp(node, text)
287
287
288 def _status(ui, repo, kwt, *pats, **opts):
288 def _status(ui, repo, kwt, *pats, **opts):
289 '''Bails out if [keyword] configuration is not active.
289 '''Bails out if [keyword] configuration is not active.
290 Returns status of working directory.'''
290 Returns status of working directory.'''
291 if kwt:
291 if kwt:
292 return repo.status(match=cmdutil.match(repo, pats, opts), clean=True,
292 return repo.status(match=cmdutil.match(repo, pats, opts), clean=True,
293 unknown=opts.get('unknown') or opts.get('all'))
293 unknown=opts.get('unknown') or opts.get('all'))
294 if ui.configitems('keyword'):
294 if ui.configitems('keyword'):
295 raise util.Abort(_('[keyword] patterns cannot match'))
295 raise util.Abort(_('[keyword] patterns cannot match'))
296 raise util.Abort(_('no [keyword] patterns configured'))
296 raise util.Abort(_('no [keyword] patterns configured'))
297
297
298 def _kwfwrite(ui, repo, expand, *pats, **opts):
298 def _kwfwrite(ui, repo, expand, *pats, **opts):
299 '''Selects files and passes them to kwtemplater.overwrite.'''
299 '''Selects files and passes them to kwtemplater.overwrite.'''
300 wctx = repo[None]
300 wctx = repo[None]
301 if len(wctx.parents()) > 1:
301 if len(wctx.parents()) > 1:
302 raise util.Abort(_('outstanding uncommitted merge'))
302 raise util.Abort(_('outstanding uncommitted merge'))
303 kwt = kwtools['templater']
303 kwt = kwtools['templater']
304 wlock = repo.wlock()
304 wlock = repo.wlock()
305 try:
305 try:
306 status = _status(ui, repo, kwt, *pats, **opts)
306 status = _status(ui, repo, kwt, *pats, **opts)
307 modified, added, removed, deleted, unknown, ignored, clean = status
307 modified, added, removed, deleted, unknown, ignored, clean = status
308 if modified or added or removed or deleted:
308 if modified or added or removed or deleted:
309 raise util.Abort(_('outstanding uncommitted changes'))
309 raise util.Abort(_('outstanding uncommitted changes'))
310 kwt.overwrite(wctx, clean, True, expand)
310 kwt.overwrite(wctx, clean, True, expand)
311 finally:
311 finally:
312 wlock.release()
312 wlock.release()
313
313
314 def demo(ui, repo, *args, **opts):
314 def demo(ui, repo, *args, **opts):
315 '''print [keywordmaps] configuration and an expansion example
315 '''print [keywordmaps] configuration and an expansion example
316
316
317 Show current, custom, or default keyword template maps and their
317 Show current, custom, or default keyword template maps and their
318 expansions.
318 expansions.
319
319
320 Extend the current configuration by specifying maps as arguments
320 Extend the current configuration by specifying maps as arguments
321 and using -f/--rcfile to source an external hgrc file.
321 and using -f/--rcfile to source an external hgrc file.
322
322
323 Use -d/--default to disable current configuration.
323 Use -d/--default to disable current configuration.
324
324
325 See :hg:`help templates` for information on templates and filters.
325 See :hg:`help templates` for information on templates and filters.
326 '''
326 '''
327 def demoitems(section, items):
327 def demoitems(section, items):
328 ui.write('[%s]\n' % section)
328 ui.write('[%s]\n' % section)
329 for k, v in sorted(items):
329 for k, v in sorted(items):
330 ui.write('%s = %s\n' % (k, v))
330 ui.write('%s = %s\n' % (k, v))
331
331
332 fn = 'demo.txt'
332 fn = 'demo.txt'
333 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
333 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
334 ui.note(_('creating temporary repository at %s\n') % tmpdir)
334 ui.note(_('creating temporary repository at %s\n') % tmpdir)
335 repo = localrepo.localrepository(ui, tmpdir, True)
335 repo = localrepo.localrepository(ui, tmpdir, True)
336 ui.setconfig('keyword', fn, '')
336 ui.setconfig('keyword', fn, '')
337
337
338 uikwmaps = ui.configitems('keywordmaps')
338 uikwmaps = ui.configitems('keywordmaps')
339 if args or opts.get('rcfile'):
339 if args or opts.get('rcfile'):
340 ui.status(_('\n\tconfiguration using custom keyword template maps\n'))
340 ui.status(_('\n\tconfiguration using custom keyword template maps\n'))
341 if uikwmaps:
341 if uikwmaps:
342 ui.status(_('\textending current template maps\n'))
342 ui.status(_('\textending current template maps\n'))
343 if opts.get('default') or not uikwmaps:
343 if opts.get('default') or not uikwmaps:
344 ui.status(_('\toverriding default template maps\n'))
344 ui.status(_('\toverriding default template maps\n'))
345 if opts.get('rcfile'):
345 if opts.get('rcfile'):
346 ui.readconfig(opts.get('rcfile'))
346 ui.readconfig(opts.get('rcfile'))
347 if args:
347 if args:
348 # simulate hgrc parsing
348 # simulate hgrc parsing
349 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
349 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
350 fp = repo.opener('hgrc', 'w')
350 fp = repo.opener('hgrc', 'w')
351 fp.writelines(rcmaps)
351 fp.writelines(rcmaps)
352 fp.close()
352 fp.close()
353 ui.readconfig(repo.join('hgrc'))
353 ui.readconfig(repo.join('hgrc'))
354 kwmaps = dict(ui.configitems('keywordmaps'))
354 kwmaps = dict(ui.configitems('keywordmaps'))
355 elif opts.get('default'):
355 elif opts.get('default'):
356 ui.status(_('\n\tconfiguration using default keyword template maps\n'))
356 ui.status(_('\n\tconfiguration using default keyword template maps\n'))
357 kwmaps = _defaultkwmaps(ui)
357 kwmaps = _defaultkwmaps(ui)
358 if uikwmaps:
358 if uikwmaps:
359 ui.status(_('\tdisabling current template maps\n'))
359 ui.status(_('\tdisabling current template maps\n'))
360 for k, v in kwmaps.iteritems():
360 for k, v in kwmaps.iteritems():
361 ui.setconfig('keywordmaps', k, v)
361 ui.setconfig('keywordmaps', k, v)
362 else:
362 else:
363 ui.status(_('\n\tconfiguration using current keyword template maps\n'))
363 ui.status(_('\n\tconfiguration using current keyword template maps\n'))
364 kwmaps = dict(uikwmaps) or _defaultkwmaps(ui)
364 kwmaps = dict(uikwmaps) or _defaultkwmaps(ui)
365
365
366 uisetup(ui)
366 uisetup(ui)
367 reposetup(ui, repo)
367 reposetup(ui, repo)
368 ui.write('[extensions]\nkeyword =\n')
368 ui.write('[extensions]\nkeyword =\n')
369 demoitems('keyword', ui.configitems('keyword'))
369 demoitems('keyword', ui.configitems('keyword'))
370 demoitems('keywordmaps', kwmaps.iteritems())
370 demoitems('keywordmaps', kwmaps.iteritems())
371 keywords = '$' + '$\n$'.join(sorted(kwmaps.keys())) + '$\n'
371 keywords = '$' + '$\n$'.join(sorted(kwmaps.keys())) + '$\n'
372 repo.wopener(fn, 'w').write(keywords)
372 repo.wopener(fn, 'w').write(keywords)
373 repo[None].add([fn])
373 repo[None].add([fn])
374 ui.note(_('\nkeywords written to %s:\n') % fn)
374 ui.note(_('\nkeywords written to %s:\n') % fn)
375 ui.note(keywords)
375 ui.note(keywords)
376 repo.dirstate.setbranch('demobranch')
376 repo.dirstate.setbranch('demobranch')
377 for name, cmd in ui.configitems('hooks'):
377 for name, cmd in ui.configitems('hooks'):
378 if name.split('.', 1)[0].find('commit') > -1:
378 if name.split('.', 1)[0].find('commit') > -1:
379 repo.ui.setconfig('hooks', name, '')
379 repo.ui.setconfig('hooks', name, '')
380 msg = _('hg keyword configuration and expansion example')
380 msg = _('hg keyword configuration and expansion example')
381 ui.note("hg ci -m '%s'\n" % msg)
381 ui.note("hg ci -m '%s'\n" % msg)
382 repo.commit(text=msg)
382 repo.commit(text=msg)
383 ui.status(_('\n\tkeywords expanded\n'))
383 ui.status(_('\n\tkeywords expanded\n'))
384 ui.write(repo.wread(fn))
384 ui.write(repo.wread(fn))
385 shutil.rmtree(tmpdir, ignore_errors=True)
385 shutil.rmtree(tmpdir, ignore_errors=True)
386
386
387 def expand(ui, repo, *pats, **opts):
387 def expand(ui, repo, *pats, **opts):
388 '''expand keywords in the working directory
388 '''expand keywords in the working directory
389
389
390 Run after (re)enabling keyword expansion.
390 Run after (re)enabling keyword expansion.
391
391
392 kwexpand refuses to run if given files contain local changes.
392 kwexpand refuses to run if given files contain local changes.
393 '''
393 '''
394 # 3rd argument sets expansion to True
394 # 3rd argument sets expansion to True
395 _kwfwrite(ui, repo, True, *pats, **opts)
395 _kwfwrite(ui, repo, True, *pats, **opts)
396
396
397 def files(ui, repo, *pats, **opts):
397 def files(ui, repo, *pats, **opts):
398 '''show files configured for keyword expansion
398 '''show files configured for keyword expansion
399
399
400 List which files in the working directory are matched by the
400 List which files in the working directory are matched by the
401 [keyword] configuration patterns.
401 [keyword] configuration patterns.
402
402
403 Useful to prevent inadvertent keyword expansion and to speed up
403 Useful to prevent inadvertent keyword expansion and to speed up
404 execution by including only files that are actual candidates for
404 execution by including only files that are actual candidates for
405 expansion.
405 expansion.
406
406
407 See :hg:`help keyword` on how to construct patterns both for
407 See :hg:`help keyword` on how to construct patterns both for
408 inclusion and exclusion of files.
408 inclusion and exclusion of files.
409
409
410 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
411 of files are::
411 of files are::
412
412
413 K = keyword expansion candidate
413 K = keyword expansion candidate
414 k = keyword expansion candidate (not tracked)
414 k = keyword expansion candidate (not tracked)
415 I = ignored
415 I = ignored
416 i = ignored (not tracked)
416 i = ignored (not tracked)
417 '''
417 '''
418 kwt = kwtools['templater']
418 kwt = kwtools['templater']
419 status = _status(ui, repo, kwt, *pats, **opts)
419 status = _status(ui, repo, kwt, *pats, **opts)
420 cwd = pats and repo.getcwd() or ''
420 cwd = pats and repo.getcwd() or ''
421 modified, added, removed, deleted, unknown, ignored, clean = status
421 modified, added, removed, deleted, unknown, ignored, clean = status
422 files = []
422 files = []
423 if not opts.get('unknown') or opts.get('all'):
423 if not opts.get('unknown') or opts.get('all'):
424 files = sorted(modified + added + clean)
424 files = sorted(modified + added + clean)
425 wctx = repo[None]
425 wctx = repo[None]
426 kwfiles = kwt.iskwfile(files, wctx)
426 kwfiles = kwt.iskwfile(files, wctx)
427 kwunknown = kwt.iskwfile(unknown, wctx)
427 kwunknown = kwt.iskwfile(unknown, wctx)
428 if not opts.get('ignore') or opts.get('all'):
428 if not opts.get('ignore') or opts.get('all'):
429 showfiles = kwfiles, kwunknown
429 showfiles = kwfiles, kwunknown
430 else:
430 else:
431 showfiles = [], []
431 showfiles = [], []
432 if opts.get('all') or opts.get('ignore'):
432 if opts.get('all') or opts.get('ignore'):
433 showfiles += ([f for f in files if f not in kwfiles],
433 showfiles += ([f for f in files if f not in kwfiles],
434 [f for f in unknown if f not in kwunknown])
434 [f for f in unknown if f not in kwunknown])
435 for char, filenames in zip('KkIi', showfiles):
435 for char, filenames in zip('KkIi', showfiles):
436 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'
437 for f in filenames:
437 for f in filenames:
438 ui.write(fmt % repo.pathto(f, cwd))
438 ui.write(fmt % repo.pathto(f, cwd))
439
439
440 def shrink(ui, repo, *pats, **opts):
440 def shrink(ui, repo, *pats, **opts):
441 '''revert expanded keywords in the working directory
441 '''revert expanded keywords in the working directory
442
442
443 Run before changing/disabling active keywords or if you experience
443 Run before changing/disabling active keywords or if you experience
444 problems with :hg:`import` or :hg:`merge`.
444 problems with :hg:`import` or :hg:`merge`.
445
445
446 kwshrink refuses to run if given files contain local changes.
446 kwshrink refuses to run if given files contain local changes.
447 '''
447 '''
448 # 3rd argument sets expansion to False
448 # 3rd argument sets expansion to False
449 _kwfwrite(ui, repo, False, *pats, **opts)
449 _kwfwrite(ui, repo, False, *pats, **opts)
450
450
451
451
452 def uisetup(ui):
452 def uisetup(ui):
453 ''' Monkeypatches dispatch._parse to retrieve user command.'''
453 ''' Monkeypatches dispatch._parse to retrieve user command.'''
454
454
455 def kwdispatch_parse(orig, ui, args):
455 def kwdispatch_parse(orig, ui, args):
456 '''Monkeypatch dispatch._parse to obtain running hg command.'''
456 '''Monkeypatch dispatch._parse to obtain running hg command.'''
457 cmd, func, args, options, cmdoptions = orig(ui, args)
457 cmd, func, args, options, cmdoptions = orig(ui, args)
458 kwtools['hgcmd'] = cmd
458 kwtools['hgcmd'] = cmd
459 return cmd, func, args, options, cmdoptions
459 return cmd, func, args, options, cmdoptions
460
460
461 extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse)
461 extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse)
462
462
463 def reposetup(ui, repo):
463 def reposetup(ui, repo):
464 '''Sets up repo as kwrepo for keyword substitution.
464 '''Sets up repo as kwrepo for keyword substitution.
465 Overrides file method to return kwfilelog instead of filelog
465 Overrides file method to return kwfilelog instead of filelog
466 if file matches user configuration.
466 if file matches user configuration.
467 Wraps commit to overwrite configured files with updated
467 Wraps commit to overwrite configured files with updated
468 keyword substitutions.
468 keyword substitutions.
469 Monkeypatches patch and webcommands.'''
469 Monkeypatches patch and webcommands.'''
470
470
471 try:
471 try:
472 if (not repo.local() or kwtools['hgcmd'] in nokwcommands.split()
472 if (not repo.local() or kwtools['hgcmd'] in nokwcommands.split()
473 or '.hg' in util.splitpath(repo.root)
473 or '.hg' in util.splitpath(repo.root)
474 or repo._url.startswith('bundle:')):
474 or repo._url.startswith('bundle:')):
475 return
475 return
476 except AttributeError:
476 except AttributeError:
477 pass
477 pass
478
478
479 inc, exc = [], ['.hg*']
479 inc, exc = [], ['.hg*']
480 for pat, opt in ui.configitems('keyword'):
480 for pat, opt in ui.configitems('keyword'):
481 if opt != 'ignore':
481 if opt != 'ignore':
482 inc.append(pat)
482 inc.append(pat)
483 else:
483 else:
484 exc.append(pat)
484 exc.append(pat)
485 if not inc:
485 if not inc:
486 return
486 return
487
487
488 kwtools['templater'] = kwt = kwtemplater(ui, repo, inc, exc)
488 kwtools['templater'] = kwt = kwtemplater(ui, repo, inc, exc)
489
489
490 class kwrepo(repo.__class__):
490 class kwrepo(repo.__class__):
491 def file(self, f):
491 def file(self, f):
492 if f[0] == '/':
492 if f[0] == '/':
493 f = f[1:]
493 f = f[1:]
494 return kwfilelog(self.sopener, kwt, f)
494 return kwfilelog(self.sopener, kwt, f)
495
495
496 def wread(self, filename):
496 def wread(self, filename):
497 data = super(kwrepo, self).wread(filename)
497 data = super(kwrepo, self).wread(filename)
498 return kwt.wread(filename, data)
498 return kwt.wread(filename, data)
499
499
500 def commit(self, *args, **opts):
500 def commit(self, *args, **opts):
501 # use custom commitctx for user commands
501 # use custom commitctx for user commands
502 # other extensions can still wrap repo.commitctx directly
502 # other extensions can still wrap repo.commitctx directly
503 self.commitctx = self.kwcommitctx
503 self.commitctx = self.kwcommitctx
504 try:
504 try:
505 return super(kwrepo, self).commit(*args, **opts)
505 return super(kwrepo, self).commit(*args, **opts)
506 finally:
506 finally:
507 del self.commitctx
507 del self.commitctx
508
508
509 def kwcommitctx(self, ctx, error=False):
509 def kwcommitctx(self, ctx, error=False):
510 n = super(kwrepo, self).commitctx(ctx, error)
510 n = super(kwrepo, self).commitctx(ctx, error)
511 # no lock needed, only called from repo.commit() which already locks
511 # no lock needed, only called from repo.commit() which already locks
512 if not kwt.record:
512 if not kwt.record:
513 restrict = kwt.restrict
513 restrict = kwt.restrict
514 kwt.restrict = True
514 kwt.restrict = True
515 kwt.overwrite(self[n], sorted(ctx.added() + ctx.modified()),
515 kwt.overwrite(self[n], sorted(ctx.added() + ctx.modified()),
516 False, True)
516 False, True)
517 kwt.restrict = restrict
517 kwt.restrict = restrict
518 return n
518 return n
519
519
520 def rollback(self, dryrun=False):
520 def rollback(self, dryrun=False):
521 wlock = self.wlock()
521 wlock = self.wlock()
522 try:
522 try:
523 if not dryrun:
523 if not dryrun:
524 changed = self['.'].files()
524 changed = self['.'].files()
525 ret = super(kwrepo, self).rollback(dryrun)
525 ret = super(kwrepo, self).rollback(dryrun)
526 if not dryrun:
526 if not dryrun:
527 ctx = self['.']
527 ctx = self['.']
528 modified, added = _preselect(self[None].status(), changed)
528 modified, added = _preselect(self[None].status(), changed)
529 kwt.overwrite(ctx, modified, True, True)
529 kwt.overwrite(ctx, modified, True, True)
530 kwt.overwrite(ctx, added, True, False)
530 kwt.overwrite(ctx, added, True, False)
531 return ret
531 return ret
532 finally:
532 finally:
533 wlock.release()
533 wlock.release()
534
534
535 # monkeypatches
535 # monkeypatches
536 def kwpatchfile_init(orig, self, ui, fname, opener,
536 def kwpatchfile_init(orig, self, ui, fname, opener,
537 missing=False, eolmode=None):
537 missing=False, eolmode=None):
538 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
538 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
539 rejects or conflicts due to expanded keywords in working dir.'''
539 rejects or conflicts due to expanded keywords in working dir.'''
540 orig(self, ui, fname, opener, missing, eolmode)
540 orig(self, ui, fname, opener, missing, eolmode)
541 # shrink keywords read from working dir
541 # shrink keywords read from working dir
542 self.lines = kwt.shrinklines(self.fname, self.lines)
542 self.lines = kwt.shrinklines(self.fname, self.lines)
543
543
544 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,
545 opts=None, prefix=''):
545 opts=None, prefix=''):
546 '''Monkeypatch patch.diff to avoid expansion.'''
546 '''Monkeypatch patch.diff to avoid expansion.'''
547 kwt.restrict = True
547 kwt.restrict = True
548 return orig(repo, node1, node2, match, changes, opts, prefix)
548 return orig(repo, node1, node2, match, changes, opts, prefix)
549
549
550 def kwweb_skip(orig, web, req, tmpl):
550 def kwweb_skip(orig, web, req, tmpl):
551 '''Wraps webcommands.x turning off keyword expansion.'''
551 '''Wraps webcommands.x turning off keyword expansion.'''
552 kwt.match = util.never
552 kwt.match = util.never
553 return orig(web, req, tmpl)
553 return orig(web, req, tmpl)
554
554
555 def kw_copy(orig, ui, repo, pats, opts, rename=False):
555 def kw_copy(orig, ui, repo, pats, opts, rename=False):
556 '''Wraps cmdutil.copy so that copy/rename destinations do not
556 '''Wraps cmdutil.copy so that copy/rename destinations do not
557 contain expanded keywords.
557 contain expanded keywords.
558 Note that the source may also be a symlink as:
558 Note that the source may also be a symlink as:
559 hg cp sym x -> x is symlink
559 hg cp sym x -> x is symlink
560 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)
561 '''
561 '''
562 orig(ui, repo, pats, opts, rename)
562 orig(ui, repo, pats, opts, rename)
563 if opts.get('dry_run'):
563 if opts.get('dry_run'):
564 return
564 return
565 wctx = repo[None]
565 wctx = repo[None]
566 candidates = [f for f in repo.dirstate.copies() if
566 candidates = [f for f in repo.dirstate.copies() if
567 kwt.match(repo.dirstate.copied(f)) and
567 kwt.match(repo.dirstate.copied(f)) and
568 not 'l' in wctx.flags(f)]
568 not 'l' in wctx.flags(f)]
569 kwt.overwrite(wctx, candidates, False, False)
569 kwt.overwrite(wctx, candidates, False, False)
570
570
571 def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts):
571 def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts):
572 '''Wraps record.dorecord expanding keywords after recording.'''
572 '''Wraps record.dorecord expanding keywords after recording.'''
573 wlock = repo.wlock()
573 wlock = repo.wlock()
574 try:
574 try:
575 # record returns 0 even when nothing has changed
575 # record returns 0 even when nothing has changed
576 # therefore compare nodes before and after
576 # therefore compare nodes before and after
577 kwt.record = True
577 kwt.record = True
578 ctx = repo['.']
578 ctx = repo['.']
579 wstatus = repo[None].status()
579 wstatus = repo[None].status()
580 ret = orig(ui, repo, commitfunc, *pats, **opts)
580 ret = orig(ui, repo, commitfunc, *pats, **opts)
581 recctx = repo['.']
581 recctx = repo['.']
582 if ctx != recctx:
582 if ctx != recctx:
583 modified, added = _preselect(wstatus, recctx.files())
583 modified, added = _preselect(wstatus, recctx.files())
584 kwt.restrict = False
584 kwt.restrict = False
585 kwt.overwrite(recctx, modified, False, True)
585 kwt.overwrite(recctx, modified, False, True)
586 kwt.overwrite(recctx, added, False, True, True)
586 kwt.overwrite(recctx, added, False, True, True)
587 kwt.restrict = True
587 kwt.restrict = True
588 return ret
588 return ret
589 finally:
589 finally:
590 wlock.release()
590 wlock.release()
591
591
592 repo.__class__ = kwrepo
592 repo.__class__ = kwrepo
593
593
594 def kwfilectx_cmp(orig, self, fctx):
594 def kwfilectx_cmp(orig, self, fctx):
595 # keyword affects data size, comparing wdir and filelog size does
595 # keyword affects data size, comparing wdir and filelog size does
596 # not make sense
596 # not make sense
597 return self._filelog.cmp(self._filenode, fctx.data())
597 if (fctx._filerev is None and
598 (self._repo._encodefilterpats or
599 kwt.match(fctx.path()) and not 'l' in fctx.flags()) or
600 self.size() == fctx.size()):
601 return self._filelog.cmp(self._filenode, fctx.data())
602 return True
603
598 extensions.wrapfunction(context.filectx, 'cmp', kwfilectx_cmp)
604 extensions.wrapfunction(context.filectx, 'cmp', kwfilectx_cmp)
599
600 extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init)
605 extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init)
601 extensions.wrapfunction(patch, 'diff', kw_diff)
606 extensions.wrapfunction(patch, 'diff', kw_diff)
602 extensions.wrapfunction(cmdutil, 'copy', kw_copy)
607 extensions.wrapfunction(cmdutil, 'copy', kw_copy)
603 for c in 'annotate changeset rev filediff diff'.split():
608 for c in 'annotate changeset rev filediff diff'.split():
604 extensions.wrapfunction(webcommands, c, kwweb_skip)
609 extensions.wrapfunction(webcommands, c, kwweb_skip)
605 for name in recordextensions.split():
610 for name in recordextensions.split():
606 try:
611 try:
607 record = extensions.find(name)
612 record = extensions.find(name)
608 extensions.wrapfunction(record, 'dorecord', kw_dorecord)
613 extensions.wrapfunction(record, 'dorecord', kw_dorecord)
609 except KeyError:
614 except KeyError:
610 pass
615 pass
611
616
612 cmdtable = {
617 cmdtable = {
613 'kwdemo':
618 'kwdemo':
614 (demo,
619 (demo,
615 [('d', 'default', None, _('show default keyword template maps')),
620 [('d', 'default', None, _('show default keyword template maps')),
616 ('f', 'rcfile', '',
621 ('f', 'rcfile', '',
617 _('read maps from rcfile'), _('FILE'))],
622 _('read maps from rcfile'), _('FILE'))],
618 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')),
623 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')),
619 'kwexpand': (expand, commands.walkopts,
624 'kwexpand': (expand, commands.walkopts,
620 _('hg kwexpand [OPTION]... [FILE]...')),
625 _('hg kwexpand [OPTION]... [FILE]...')),
621 'kwfiles':
626 'kwfiles':
622 (files,
627 (files,
623 [('A', 'all', None, _('show keyword status flags of all files')),
628 [('A', 'all', None, _('show keyword status flags of all files')),
624 ('i', 'ignore', None, _('show files excluded from expansion')),
629 ('i', 'ignore', None, _('show files excluded from expansion')),
625 ('u', 'unknown', None, _('only show unknown (not tracked) files')),
630 ('u', 'unknown', None, _('only show unknown (not tracked) files')),
626 ] + commands.walkopts,
631 ] + commands.walkopts,
627 _('hg kwfiles [OPTION]... [FILE]...')),
632 _('hg kwfiles [OPTION]... [FILE]...')),
628 'kwshrink': (shrink, commands.walkopts,
633 'kwshrink': (shrink, commands.walkopts,
629 _('hg kwshrink [OPTION]... [FILE]...')),
634 _('hg kwshrink [OPTION]... [FILE]...')),
630 }
635 }
General Comments 0
You need to be logged in to leave comments. Login now