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