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