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