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