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