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