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