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