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