##// END OF EJS Templates
keyword: use parent of working copy as base for status...
Martin von Zweigbergk -
r23079:c4ce50a3 stable
parent child Browse files
Show More
@@ -1,743 +1,743 b''
1 # keyword.py - $Keyword$ expansion for Mercurial
1 # keyword.py - $Keyword$ expansion for Mercurial
2 #
2 #
3 # Copyright 2007-2014 Christian Ebert <blacktrash@gmx.net>
3 # Copyright 2007-2014 Christian Ebert <blacktrash@gmx.net>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 #
7 #
8 # $Id$
8 # $Id$
9 #
9 #
10 # Keyword expansion hack against the grain of a Distributed SCM
10 # Keyword expansion hack against the grain of a Distributed SCM
11 #
11 #
12 # There are many good reasons why this is not needed in a distributed
12 # There are many good reasons why this is not needed in a distributed
13 # SCM, still it may be useful in very small projects based on single
13 # SCM, still it may be useful in very small projects based on single
14 # files (like LaTeX packages), that are mostly addressed to an
14 # files (like LaTeX packages), that are mostly addressed to an
15 # audience not running a version control system.
15 # audience not running a version control system.
16 #
16 #
17 # For in-depth discussion refer to
17 # For in-depth discussion refer to
18 # <http://mercurial.selenic.com/wiki/KeywordPlan>.
18 # <http://mercurial.selenic.com/wiki/KeywordPlan>.
19 #
19 #
20 # Keyword expansion is based on Mercurial's changeset template mappings.
20 # Keyword expansion is based on Mercurial's changeset template mappings.
21 #
21 #
22 # Binary files are not touched.
22 # Binary files are not touched.
23 #
23 #
24 # Files to act upon/ignore are specified in the [keyword] section.
24 # Files to act upon/ignore are specified in the [keyword] section.
25 # Customized keyword template mappings in the [keywordmaps] section.
25 # Customized keyword template mappings in the [keywordmaps] section.
26 #
26 #
27 # Run "hg help keyword" and "hg kwdemo" to get info on configuration.
27 # Run "hg help keyword" and "hg kwdemo" to get info on configuration.
28
28
29 '''expand keywords in tracked files
29 '''expand keywords in tracked files
30
30
31 This extension expands RCS/CVS-like or self-customized $Keywords$ in
31 This extension expands RCS/CVS-like or self-customized $Keywords$ in
32 tracked text files selected by your configuration.
32 tracked text files selected by your configuration.
33
33
34 Keywords are only expanded in local repositories and not stored in the
34 Keywords are only expanded in local repositories and not stored in the
35 change history. The mechanism can be regarded as a convenience for the
35 change history. The mechanism can be regarded as a convenience for the
36 current user or for archive distribution.
36 current user or for archive distribution.
37
37
38 Keywords expand to the changeset data pertaining to the latest change
38 Keywords expand to the changeset data pertaining to the latest change
39 relative to the working directory parent of each file.
39 relative to the working directory parent of each file.
40
40
41 Configuration is done in the [keyword], [keywordset] and [keywordmaps]
41 Configuration is done in the [keyword], [keywordset] and [keywordmaps]
42 sections of hgrc files.
42 sections of hgrc files.
43
43
44 Example::
44 Example::
45
45
46 [keyword]
46 [keyword]
47 # expand keywords in every python file except those matching "x*"
47 # expand keywords in every python file except those matching "x*"
48 **.py =
48 **.py =
49 x* = ignore
49 x* = ignore
50
50
51 [keywordset]
51 [keywordset]
52 # prefer svn- over cvs-like default keywordmaps
52 # prefer svn- over cvs-like default keywordmaps
53 svn = True
53 svn = True
54
54
55 .. note::
55 .. note::
56
56
57 The more specific you are in your filename patterns the less you
57 The more specific you are in your filename patterns the less you
58 lose speed in huge repositories.
58 lose speed in huge repositories.
59
59
60 For [keywordmaps] template mapping and expansion demonstration and
60 For [keywordmaps] template mapping and expansion demonstration and
61 control run :hg:`kwdemo`. See :hg:`help templates` for a list of
61 control run :hg:`kwdemo`. See :hg:`help templates` for a list of
62 available templates and filters.
62 available templates and filters.
63
63
64 Three additional date template filters are provided:
64 Three additional date template filters are provided:
65
65
66 :``utcdate``: "2006/09/18 15:13:13"
66 :``utcdate``: "2006/09/18 15:13:13"
67 :``svnutcdate``: "2006-09-18 15:13:13Z"
67 :``svnutcdate``: "2006-09-18 15:13:13Z"
68 :``svnisodate``: "2006-09-18 08:13:13 -700 (Mon, 18 Sep 2006)"
68 :``svnisodate``: "2006-09-18 08:13:13 -700 (Mon, 18 Sep 2006)"
69
69
70 The default template mappings (view with :hg:`kwdemo -d`) can be
70 The default template mappings (view with :hg:`kwdemo -d`) can be
71 replaced with customized keywords and templates. Again, run
71 replaced with customized keywords and templates. Again, run
72 :hg:`kwdemo` to control the results of your configuration changes.
72 :hg:`kwdemo` to control the results of your configuration changes.
73
73
74 Before changing/disabling active keywords, you must run :hg:`kwshrink`
74 Before changing/disabling active keywords, you must run :hg:`kwshrink`
75 to avoid storing expanded keywords in the change history.
75 to avoid storing expanded keywords in the change history.
76
76
77 To force expansion after enabling it, or a configuration change, run
77 To force expansion after enabling it, or a configuration change, run
78 :hg:`kwexpand`.
78 :hg:`kwexpand`.
79
79
80 Expansions spanning more than one line and incremental expansions,
80 Expansions spanning more than one line and incremental expansions,
81 like CVS' $Log$, are not supported. A keyword template map "Log =
81 like CVS' $Log$, are not supported. A keyword template map "Log =
82 {desc}" expands to the first line of the changeset description.
82 {desc}" expands to the first line of the changeset description.
83 '''
83 '''
84
84
85 from mercurial import commands, context, cmdutil, dispatch, filelog, extensions
85 from mercurial import commands, context, cmdutil, dispatch, filelog, extensions
86 from mercurial import localrepo, match, patch, templatefilters, templater, util
86 from mercurial import localrepo, match, patch, templatefilters, templater, util
87 from mercurial import scmutil, pathutil
87 from mercurial import scmutil, pathutil
88 from mercurial.hgweb import webcommands
88 from mercurial.hgweb import webcommands
89 from mercurial.i18n import _
89 from mercurial.i18n import _
90 import os, re, tempfile
90 import os, re, tempfile
91
91
92 cmdtable = {}
92 cmdtable = {}
93 command = cmdutil.command(cmdtable)
93 command = cmdutil.command(cmdtable)
94 testedwith = 'internal'
94 testedwith = 'internal'
95
95
96 # hg commands that do not act on keywords
96 # hg commands that do not act on keywords
97 nokwcommands = ('add addremove annotate bundle export grep incoming init log'
97 nokwcommands = ('add addremove annotate bundle export grep incoming init log'
98 ' outgoing push tip verify convert email glog')
98 ' outgoing push tip verify convert email glog')
99
99
100 # hg commands that trigger expansion only when writing to working dir,
100 # hg commands that trigger expansion only when writing to working dir,
101 # not when reading filelog, and unexpand when reading from working dir
101 # not when reading filelog, and unexpand when reading from working dir
102 restricted = ('merge kwexpand kwshrink record qrecord resolve transplant'
102 restricted = ('merge kwexpand kwshrink record qrecord resolve transplant'
103 ' unshelve rebase graft backout histedit fetch')
103 ' unshelve rebase graft backout histedit fetch')
104
104
105 # names of extensions using dorecord
105 # names of extensions using dorecord
106 recordextensions = 'record'
106 recordextensions = 'record'
107
107
108 colortable = {
108 colortable = {
109 'kwfiles.enabled': 'green bold',
109 'kwfiles.enabled': 'green bold',
110 'kwfiles.deleted': 'cyan bold underline',
110 'kwfiles.deleted': 'cyan bold underline',
111 'kwfiles.enabledunknown': 'green',
111 'kwfiles.enabledunknown': 'green',
112 'kwfiles.ignored': 'bold',
112 'kwfiles.ignored': 'bold',
113 'kwfiles.ignoredunknown': 'none'
113 'kwfiles.ignoredunknown': 'none'
114 }
114 }
115
115
116 # date like in cvs' $Date
116 # date like in cvs' $Date
117 def utcdate(text):
117 def utcdate(text):
118 ''':utcdate: Date. Returns a UTC-date in this format: "2009/08/18 11:00:13".
118 ''':utcdate: Date. Returns a UTC-date in this format: "2009/08/18 11:00:13".
119 '''
119 '''
120 return util.datestr((util.parsedate(text)[0], 0), '%Y/%m/%d %H:%M:%S')
120 return util.datestr((util.parsedate(text)[0], 0), '%Y/%m/%d %H:%M:%S')
121 # date like in svn's $Date
121 # date like in svn's $Date
122 def svnisodate(text):
122 def svnisodate(text):
123 ''':svnisodate: Date. Returns a date in this format: "2009-08-18 13:00:13
123 ''':svnisodate: Date. Returns a date in this format: "2009-08-18 13:00:13
124 +0200 (Tue, 18 Aug 2009)".
124 +0200 (Tue, 18 Aug 2009)".
125 '''
125 '''
126 return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2 (%a, %d %b %Y)')
126 return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2 (%a, %d %b %Y)')
127 # date like in svn's $Id
127 # date like in svn's $Id
128 def svnutcdate(text):
128 def svnutcdate(text):
129 ''':svnutcdate: Date. Returns a UTC-date in this format: "2009-08-18
129 ''':svnutcdate: Date. Returns a UTC-date in this format: "2009-08-18
130 11:00:13Z".
130 11:00:13Z".
131 '''
131 '''
132 return util.datestr((util.parsedate(text)[0], 0), '%Y-%m-%d %H:%M:%SZ')
132 return util.datestr((util.parsedate(text)[0], 0), '%Y-%m-%d %H:%M:%SZ')
133
133
134 templatefilters.filters.update({'utcdate': utcdate,
134 templatefilters.filters.update({'utcdate': utcdate,
135 'svnisodate': svnisodate,
135 'svnisodate': svnisodate,
136 'svnutcdate': svnutcdate})
136 'svnutcdate': svnutcdate})
137
137
138 # make keyword tools accessible
138 # make keyword tools accessible
139 kwtools = {'templater': None, 'hgcmd': ''}
139 kwtools = {'templater': None, 'hgcmd': ''}
140
140
141 def _defaultkwmaps(ui):
141 def _defaultkwmaps(ui):
142 '''Returns default keywordmaps according to keywordset configuration.'''
142 '''Returns default keywordmaps according to keywordset configuration.'''
143 templates = {
143 templates = {
144 'Revision': '{node|short}',
144 'Revision': '{node|short}',
145 'Author': '{author|user}',
145 'Author': '{author|user}',
146 }
146 }
147 kwsets = ({
147 kwsets = ({
148 'Date': '{date|utcdate}',
148 'Date': '{date|utcdate}',
149 'RCSfile': '{file|basename},v',
149 'RCSfile': '{file|basename},v',
150 'RCSFile': '{file|basename},v', # kept for backwards compatibility
150 'RCSFile': '{file|basename},v', # kept for backwards compatibility
151 # with hg-keyword
151 # with hg-keyword
152 'Source': '{root}/{file},v',
152 'Source': '{root}/{file},v',
153 'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
153 'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
154 'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
154 'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
155 }, {
155 }, {
156 'Date': '{date|svnisodate}',
156 'Date': '{date|svnisodate}',
157 'Id': '{file|basename},v {node|short} {date|svnutcdate} {author|user}',
157 'Id': '{file|basename},v {node|short} {date|svnutcdate} {author|user}',
158 'LastChangedRevision': '{node|short}',
158 'LastChangedRevision': '{node|short}',
159 'LastChangedBy': '{author|user}',
159 'LastChangedBy': '{author|user}',
160 'LastChangedDate': '{date|svnisodate}',
160 'LastChangedDate': '{date|svnisodate}',
161 })
161 })
162 templates.update(kwsets[ui.configbool('keywordset', 'svn')])
162 templates.update(kwsets[ui.configbool('keywordset', 'svn')])
163 return templates
163 return templates
164
164
165 def _shrinktext(text, subfunc):
165 def _shrinktext(text, subfunc):
166 '''Helper for keyword expansion removal in text.
166 '''Helper for keyword expansion removal in text.
167 Depending on subfunc also returns number of substitutions.'''
167 Depending on subfunc also returns number of substitutions.'''
168 return subfunc(r'$\1$', text)
168 return subfunc(r'$\1$', text)
169
169
170 def _preselect(wstatus, changed):
170 def _preselect(wstatus, changed):
171 '''Retrieves modified and added files from a working directory state
171 '''Retrieves modified and added files from a working directory state
172 and returns the subset of each contained in given changed files
172 and returns the subset of each contained in given changed files
173 retrieved from a change context.'''
173 retrieved from a change context.'''
174 modified = [f for f in wstatus.modified if f in changed]
174 modified = [f for f in wstatus.modified if f in changed]
175 added = [f for f in wstatus.added if f in changed]
175 added = [f for f in wstatus.added if f in changed]
176 return modified, added
176 return modified, added
177
177
178
178
179 class kwtemplater(object):
179 class kwtemplater(object):
180 '''
180 '''
181 Sets up keyword templates, corresponding keyword regex, and
181 Sets up keyword templates, corresponding keyword regex, and
182 provides keyword substitution functions.
182 provides keyword substitution functions.
183 '''
183 '''
184
184
185 def __init__(self, ui, repo, inc, exc):
185 def __init__(self, ui, repo, inc, exc):
186 self.ui = ui
186 self.ui = ui
187 self.repo = repo
187 self.repo = repo
188 self.match = match.match(repo.root, '', [], inc, exc)
188 self.match = match.match(repo.root, '', [], inc, exc)
189 self.restrict = kwtools['hgcmd'] in restricted.split()
189 self.restrict = kwtools['hgcmd'] in restricted.split()
190 self.postcommit = False
190 self.postcommit = False
191
191
192 kwmaps = self.ui.configitems('keywordmaps')
192 kwmaps = self.ui.configitems('keywordmaps')
193 if kwmaps: # override default templates
193 if kwmaps: # override default templates
194 self.templates = dict((k, templater.parsestring(v, False))
194 self.templates = dict((k, templater.parsestring(v, False))
195 for k, v in kwmaps)
195 for k, v in kwmaps)
196 else:
196 else:
197 self.templates = _defaultkwmaps(self.ui)
197 self.templates = _defaultkwmaps(self.ui)
198
198
199 @util.propertycache
199 @util.propertycache
200 def escape(self):
200 def escape(self):
201 '''Returns bar-separated and escaped keywords.'''
201 '''Returns bar-separated and escaped keywords.'''
202 return '|'.join(map(re.escape, self.templates.keys()))
202 return '|'.join(map(re.escape, self.templates.keys()))
203
203
204 @util.propertycache
204 @util.propertycache
205 def rekw(self):
205 def rekw(self):
206 '''Returns regex for unexpanded keywords.'''
206 '''Returns regex for unexpanded keywords.'''
207 return re.compile(r'\$(%s)\$' % self.escape)
207 return re.compile(r'\$(%s)\$' % self.escape)
208
208
209 @util.propertycache
209 @util.propertycache
210 def rekwexp(self):
210 def rekwexp(self):
211 '''Returns regex for expanded keywords.'''
211 '''Returns regex for expanded keywords.'''
212 return re.compile(r'\$(%s): [^$\n\r]*? \$' % self.escape)
212 return re.compile(r'\$(%s): [^$\n\r]*? \$' % self.escape)
213
213
214 def substitute(self, data, path, ctx, subfunc):
214 def substitute(self, data, path, ctx, subfunc):
215 '''Replaces keywords in data with expanded template.'''
215 '''Replaces keywords in data with expanded template.'''
216 def kwsub(mobj):
216 def kwsub(mobj):
217 kw = mobj.group(1)
217 kw = mobj.group(1)
218 ct = cmdutil.changeset_templater(self.ui, self.repo, False, None,
218 ct = cmdutil.changeset_templater(self.ui, self.repo, False, None,
219 self.templates[kw], '', False)
219 self.templates[kw], '', False)
220 self.ui.pushbuffer()
220 self.ui.pushbuffer()
221 ct.show(ctx, root=self.repo.root, file=path)
221 ct.show(ctx, root=self.repo.root, file=path)
222 ekw = templatefilters.firstline(self.ui.popbuffer())
222 ekw = templatefilters.firstline(self.ui.popbuffer())
223 return '$%s: %s $' % (kw, ekw)
223 return '$%s: %s $' % (kw, ekw)
224 return subfunc(kwsub, data)
224 return subfunc(kwsub, data)
225
225
226 def linkctx(self, path, fileid):
226 def linkctx(self, path, fileid):
227 '''Similar to filelog.linkrev, but returns a changectx.'''
227 '''Similar to filelog.linkrev, but returns a changectx.'''
228 return self.repo.filectx(path, fileid=fileid).changectx()
228 return self.repo.filectx(path, fileid=fileid).changectx()
229
229
230 def expand(self, path, node, data):
230 def expand(self, path, node, data):
231 '''Returns data with keywords expanded.'''
231 '''Returns data with keywords expanded.'''
232 if not self.restrict and self.match(path) and not util.binary(data):
232 if not self.restrict and self.match(path) and not util.binary(data):
233 ctx = self.linkctx(path, node)
233 ctx = self.linkctx(path, node)
234 return self.substitute(data, path, ctx, self.rekw.sub)
234 return self.substitute(data, path, ctx, self.rekw.sub)
235 return data
235 return data
236
236
237 def iskwfile(self, cand, ctx):
237 def iskwfile(self, cand, ctx):
238 '''Returns subset of candidates which are configured for keyword
238 '''Returns subset of candidates which are configured for keyword
239 expansion but are not symbolic links.'''
239 expansion but are not symbolic links.'''
240 return [f for f in cand if self.match(f) and 'l' not in ctx.flags(f)]
240 return [f for f in cand if self.match(f) and 'l' not in ctx.flags(f)]
241
241
242 def overwrite(self, ctx, candidates, lookup, expand, rekw=False):
242 def overwrite(self, ctx, candidates, lookup, expand, rekw=False):
243 '''Overwrites selected files expanding/shrinking keywords.'''
243 '''Overwrites selected files expanding/shrinking keywords.'''
244 if self.restrict or lookup or self.postcommit: # exclude kw_copy
244 if self.restrict or lookup or self.postcommit: # exclude kw_copy
245 candidates = self.iskwfile(candidates, ctx)
245 candidates = self.iskwfile(candidates, ctx)
246 if not candidates:
246 if not candidates:
247 return
247 return
248 kwcmd = self.restrict and lookup # kwexpand/kwshrink
248 kwcmd = self.restrict and lookup # kwexpand/kwshrink
249 if self.restrict or expand and lookup:
249 if self.restrict or expand and lookup:
250 mf = ctx.manifest()
250 mf = ctx.manifest()
251 if self.restrict or rekw:
251 if self.restrict or rekw:
252 re_kw = self.rekw
252 re_kw = self.rekw
253 else:
253 else:
254 re_kw = self.rekwexp
254 re_kw = self.rekwexp
255 if expand:
255 if expand:
256 msg = _('overwriting %s expanding keywords\n')
256 msg = _('overwriting %s expanding keywords\n')
257 else:
257 else:
258 msg = _('overwriting %s shrinking keywords\n')
258 msg = _('overwriting %s shrinking keywords\n')
259 for f in candidates:
259 for f in candidates:
260 if self.restrict:
260 if self.restrict:
261 data = self.repo.file(f).read(mf[f])
261 data = self.repo.file(f).read(mf[f])
262 else:
262 else:
263 data = self.repo.wread(f)
263 data = self.repo.wread(f)
264 if util.binary(data):
264 if util.binary(data):
265 continue
265 continue
266 if expand:
266 if expand:
267 if lookup:
267 if lookup:
268 ctx = self.linkctx(f, mf[f])
268 ctx = self.linkctx(f, mf[f])
269 data, found = self.substitute(data, f, ctx, re_kw.subn)
269 data, found = self.substitute(data, f, ctx, re_kw.subn)
270 elif self.restrict:
270 elif self.restrict:
271 found = re_kw.search(data)
271 found = re_kw.search(data)
272 else:
272 else:
273 data, found = _shrinktext(data, re_kw.subn)
273 data, found = _shrinktext(data, re_kw.subn)
274 if found:
274 if found:
275 self.ui.note(msg % f)
275 self.ui.note(msg % f)
276 fp = self.repo.wopener(f, "wb", atomictemp=True)
276 fp = self.repo.wopener(f, "wb", atomictemp=True)
277 fp.write(data)
277 fp.write(data)
278 fp.close()
278 fp.close()
279 if kwcmd:
279 if kwcmd:
280 self.repo.dirstate.normal(f)
280 self.repo.dirstate.normal(f)
281 elif self.postcommit:
281 elif self.postcommit:
282 self.repo.dirstate.normallookup(f)
282 self.repo.dirstate.normallookup(f)
283
283
284 def shrink(self, fname, text):
284 def shrink(self, fname, text):
285 '''Returns text with all keyword substitutions removed.'''
285 '''Returns text with all keyword substitutions removed.'''
286 if self.match(fname) and not util.binary(text):
286 if self.match(fname) and not util.binary(text):
287 return _shrinktext(text, self.rekwexp.sub)
287 return _shrinktext(text, self.rekwexp.sub)
288 return text
288 return text
289
289
290 def shrinklines(self, fname, lines):
290 def shrinklines(self, fname, lines):
291 '''Returns lines with keyword substitutions removed.'''
291 '''Returns lines with keyword substitutions removed.'''
292 if self.match(fname):
292 if self.match(fname):
293 text = ''.join(lines)
293 text = ''.join(lines)
294 if not util.binary(text):
294 if not util.binary(text):
295 return _shrinktext(text, self.rekwexp.sub).splitlines(True)
295 return _shrinktext(text, self.rekwexp.sub).splitlines(True)
296 return lines
296 return lines
297
297
298 def wread(self, fname, data):
298 def wread(self, fname, data):
299 '''If in restricted mode returns data read from wdir with
299 '''If in restricted mode returns data read from wdir with
300 keyword substitutions removed.'''
300 keyword substitutions removed.'''
301 if self.restrict:
301 if self.restrict:
302 return self.shrink(fname, data)
302 return self.shrink(fname, data)
303 return data
303 return data
304
304
305 class kwfilelog(filelog.filelog):
305 class kwfilelog(filelog.filelog):
306 '''
306 '''
307 Subclass of filelog to hook into its read, add, cmp methods.
307 Subclass of filelog to hook into its read, add, cmp methods.
308 Keywords are "stored" unexpanded, and processed on reading.
308 Keywords are "stored" unexpanded, and processed on reading.
309 '''
309 '''
310 def __init__(self, opener, kwt, path):
310 def __init__(self, opener, kwt, path):
311 super(kwfilelog, self).__init__(opener, path)
311 super(kwfilelog, self).__init__(opener, path)
312 self.kwt = kwt
312 self.kwt = kwt
313 self.path = path
313 self.path = path
314
314
315 def read(self, node):
315 def read(self, node):
316 '''Expands keywords when reading filelog.'''
316 '''Expands keywords when reading filelog.'''
317 data = super(kwfilelog, self).read(node)
317 data = super(kwfilelog, self).read(node)
318 if self.renamed(node):
318 if self.renamed(node):
319 return data
319 return data
320 return self.kwt.expand(self.path, node, data)
320 return self.kwt.expand(self.path, node, data)
321
321
322 def add(self, text, meta, tr, link, p1=None, p2=None):
322 def add(self, text, meta, tr, link, p1=None, p2=None):
323 '''Removes keyword substitutions when adding to filelog.'''
323 '''Removes keyword substitutions when adding to filelog.'''
324 text = self.kwt.shrink(self.path, text)
324 text = self.kwt.shrink(self.path, text)
325 return super(kwfilelog, self).add(text, meta, tr, link, p1, p2)
325 return super(kwfilelog, self).add(text, meta, tr, link, p1, p2)
326
326
327 def cmp(self, node, text):
327 def cmp(self, node, text):
328 '''Removes keyword substitutions for comparison.'''
328 '''Removes keyword substitutions for comparison.'''
329 text = self.kwt.shrink(self.path, text)
329 text = self.kwt.shrink(self.path, text)
330 return super(kwfilelog, self).cmp(node, text)
330 return super(kwfilelog, self).cmp(node, text)
331
331
332 def _status(ui, repo, wctx, kwt, *pats, **opts):
332 def _status(ui, repo, wctx, kwt, *pats, **opts):
333 '''Bails out if [keyword] configuration is not active.
333 '''Bails out if [keyword] configuration is not active.
334 Returns status of working directory.'''
334 Returns status of working directory.'''
335 if kwt:
335 if kwt:
336 return repo.status(match=scmutil.match(wctx, pats, opts), clean=True,
336 return repo.status(match=scmutil.match(wctx, pats, opts), clean=True,
337 unknown=opts.get('unknown') or opts.get('all'))
337 unknown=opts.get('unknown') or opts.get('all'))
338 if ui.configitems('keyword'):
338 if ui.configitems('keyword'):
339 raise util.Abort(_('[keyword] patterns cannot match'))
339 raise util.Abort(_('[keyword] patterns cannot match'))
340 raise util.Abort(_('no [keyword] patterns configured'))
340 raise util.Abort(_('no [keyword] patterns configured'))
341
341
342 def _kwfwrite(ui, repo, expand, *pats, **opts):
342 def _kwfwrite(ui, repo, expand, *pats, **opts):
343 '''Selects files and passes them to kwtemplater.overwrite.'''
343 '''Selects files and passes them to kwtemplater.overwrite.'''
344 wctx = repo[None]
344 wctx = repo[None]
345 if len(wctx.parents()) > 1:
345 if len(wctx.parents()) > 1:
346 raise util.Abort(_('outstanding uncommitted merge'))
346 raise util.Abort(_('outstanding uncommitted merge'))
347 kwt = kwtools['templater']
347 kwt = kwtools['templater']
348 wlock = repo.wlock()
348 wlock = repo.wlock()
349 try:
349 try:
350 status = _status(ui, repo, wctx, kwt, *pats, **opts)
350 status = _status(ui, repo, wctx, kwt, *pats, **opts)
351 if status.modified or status.added or status.removed or status.deleted:
351 if status.modified or status.added or status.removed or status.deleted:
352 raise util.Abort(_('outstanding uncommitted changes'))
352 raise util.Abort(_('outstanding uncommitted changes'))
353 kwt.overwrite(wctx, status.clean, True, expand)
353 kwt.overwrite(wctx, status.clean, True, expand)
354 finally:
354 finally:
355 wlock.release()
355 wlock.release()
356
356
357 @command('kwdemo',
357 @command('kwdemo',
358 [('d', 'default', None, _('show default keyword template maps')),
358 [('d', 'default', None, _('show default keyword template maps')),
359 ('f', 'rcfile', '',
359 ('f', 'rcfile', '',
360 _('read maps from rcfile'), _('FILE'))],
360 _('read maps from rcfile'), _('FILE'))],
361 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...'),
361 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...'),
362 optionalrepo=True)
362 optionalrepo=True)
363 def demo(ui, repo, *args, **opts):
363 def demo(ui, repo, *args, **opts):
364 '''print [keywordmaps] configuration and an expansion example
364 '''print [keywordmaps] configuration and an expansion example
365
365
366 Show current, custom, or default keyword template maps and their
366 Show current, custom, or default keyword template maps and their
367 expansions.
367 expansions.
368
368
369 Extend the current configuration by specifying maps as arguments
369 Extend the current configuration by specifying maps as arguments
370 and using -f/--rcfile to source an external hgrc file.
370 and using -f/--rcfile to source an external hgrc file.
371
371
372 Use -d/--default to disable current configuration.
372 Use -d/--default to disable current configuration.
373
373
374 See :hg:`help templates` for information on templates and filters.
374 See :hg:`help templates` for information on templates and filters.
375 '''
375 '''
376 def demoitems(section, items):
376 def demoitems(section, items):
377 ui.write('[%s]\n' % section)
377 ui.write('[%s]\n' % section)
378 for k, v in sorted(items):
378 for k, v in sorted(items):
379 ui.write('%s = %s\n' % (k, v))
379 ui.write('%s = %s\n' % (k, v))
380
380
381 fn = 'demo.txt'
381 fn = 'demo.txt'
382 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
382 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
383 ui.note(_('creating temporary repository at %s\n') % tmpdir)
383 ui.note(_('creating temporary repository at %s\n') % tmpdir)
384 repo = localrepo.localrepository(repo.baseui, tmpdir, True)
384 repo = localrepo.localrepository(repo.baseui, tmpdir, True)
385 ui.setconfig('keyword', fn, '', 'keyword')
385 ui.setconfig('keyword', fn, '', 'keyword')
386 svn = ui.configbool('keywordset', 'svn')
386 svn = ui.configbool('keywordset', 'svn')
387 # explicitly set keywordset for demo output
387 # explicitly set keywordset for demo output
388 ui.setconfig('keywordset', 'svn', svn, 'keyword')
388 ui.setconfig('keywordset', 'svn', svn, 'keyword')
389
389
390 uikwmaps = ui.configitems('keywordmaps')
390 uikwmaps = ui.configitems('keywordmaps')
391 if args or opts.get('rcfile'):
391 if args or opts.get('rcfile'):
392 ui.status(_('\n\tconfiguration using custom keyword template maps\n'))
392 ui.status(_('\n\tconfiguration using custom keyword template maps\n'))
393 if uikwmaps:
393 if uikwmaps:
394 ui.status(_('\textending current template maps\n'))
394 ui.status(_('\textending current template maps\n'))
395 if opts.get('default') or not uikwmaps:
395 if opts.get('default') or not uikwmaps:
396 if svn:
396 if svn:
397 ui.status(_('\toverriding default svn keywordset\n'))
397 ui.status(_('\toverriding default svn keywordset\n'))
398 else:
398 else:
399 ui.status(_('\toverriding default cvs keywordset\n'))
399 ui.status(_('\toverriding default cvs keywordset\n'))
400 if opts.get('rcfile'):
400 if opts.get('rcfile'):
401 ui.readconfig(opts.get('rcfile'))
401 ui.readconfig(opts.get('rcfile'))
402 if args:
402 if args:
403 # simulate hgrc parsing
403 # simulate hgrc parsing
404 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
404 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
405 fp = repo.opener('hgrc', 'w')
405 fp = repo.opener('hgrc', 'w')
406 fp.writelines(rcmaps)
406 fp.writelines(rcmaps)
407 fp.close()
407 fp.close()
408 ui.readconfig(repo.join('hgrc'))
408 ui.readconfig(repo.join('hgrc'))
409 kwmaps = dict(ui.configitems('keywordmaps'))
409 kwmaps = dict(ui.configitems('keywordmaps'))
410 elif opts.get('default'):
410 elif opts.get('default'):
411 if svn:
411 if svn:
412 ui.status(_('\n\tconfiguration using default svn keywordset\n'))
412 ui.status(_('\n\tconfiguration using default svn keywordset\n'))
413 else:
413 else:
414 ui.status(_('\n\tconfiguration using default cvs keywordset\n'))
414 ui.status(_('\n\tconfiguration using default cvs keywordset\n'))
415 kwmaps = _defaultkwmaps(ui)
415 kwmaps = _defaultkwmaps(ui)
416 if uikwmaps:
416 if uikwmaps:
417 ui.status(_('\tdisabling current template maps\n'))
417 ui.status(_('\tdisabling current template maps\n'))
418 for k, v in kwmaps.iteritems():
418 for k, v in kwmaps.iteritems():
419 ui.setconfig('keywordmaps', k, v, 'keyword')
419 ui.setconfig('keywordmaps', k, v, 'keyword')
420 else:
420 else:
421 ui.status(_('\n\tconfiguration using current keyword template maps\n'))
421 ui.status(_('\n\tconfiguration using current keyword template maps\n'))
422 if uikwmaps:
422 if uikwmaps:
423 kwmaps = dict(uikwmaps)
423 kwmaps = dict(uikwmaps)
424 else:
424 else:
425 kwmaps = _defaultkwmaps(ui)
425 kwmaps = _defaultkwmaps(ui)
426
426
427 uisetup(ui)
427 uisetup(ui)
428 reposetup(ui, repo)
428 reposetup(ui, repo)
429 ui.write('[extensions]\nkeyword =\n')
429 ui.write('[extensions]\nkeyword =\n')
430 demoitems('keyword', ui.configitems('keyword'))
430 demoitems('keyword', ui.configitems('keyword'))
431 demoitems('keywordset', ui.configitems('keywordset'))
431 demoitems('keywordset', ui.configitems('keywordset'))
432 demoitems('keywordmaps', kwmaps.iteritems())
432 demoitems('keywordmaps', kwmaps.iteritems())
433 keywords = '$' + '$\n$'.join(sorted(kwmaps.keys())) + '$\n'
433 keywords = '$' + '$\n$'.join(sorted(kwmaps.keys())) + '$\n'
434 repo.wopener.write(fn, keywords)
434 repo.wopener.write(fn, keywords)
435 repo[None].add([fn])
435 repo[None].add([fn])
436 ui.note(_('\nkeywords written to %s:\n') % fn)
436 ui.note(_('\nkeywords written to %s:\n') % fn)
437 ui.note(keywords)
437 ui.note(keywords)
438 wlock = repo.wlock()
438 wlock = repo.wlock()
439 try:
439 try:
440 repo.dirstate.setbranch('demobranch')
440 repo.dirstate.setbranch('demobranch')
441 finally:
441 finally:
442 wlock.release()
442 wlock.release()
443 for name, cmd in ui.configitems('hooks'):
443 for name, cmd in ui.configitems('hooks'):
444 if name.split('.', 1)[0].find('commit') > -1:
444 if name.split('.', 1)[0].find('commit') > -1:
445 repo.ui.setconfig('hooks', name, '', 'keyword')
445 repo.ui.setconfig('hooks', name, '', 'keyword')
446 msg = _('hg keyword configuration and expansion example')
446 msg = _('hg keyword configuration and expansion example')
447 ui.note(("hg ci -m '%s'\n" % msg))
447 ui.note(("hg ci -m '%s'\n" % msg))
448 repo.commit(text=msg)
448 repo.commit(text=msg)
449 ui.status(_('\n\tkeywords expanded\n'))
449 ui.status(_('\n\tkeywords expanded\n'))
450 ui.write(repo.wread(fn))
450 ui.write(repo.wread(fn))
451 for root, dirs, files in os.walk(tmpdir, topdown=False):
451 for root, dirs, files in os.walk(tmpdir, topdown=False):
452 for f in files:
452 for f in files:
453 util.unlink(os.path.join(root, f))
453 util.unlink(os.path.join(root, f))
454 for d in dirs:
454 for d in dirs:
455 os.rmdir(os.path.join(root, d))
455 os.rmdir(os.path.join(root, d))
456 os.rmdir(tmpdir)
456 os.rmdir(tmpdir)
457
457
458 @command('kwexpand',
458 @command('kwexpand',
459 commands.walkopts,
459 commands.walkopts,
460 _('hg kwexpand [OPTION]... [FILE]...'),
460 _('hg kwexpand [OPTION]... [FILE]...'),
461 inferrepo=True)
461 inferrepo=True)
462 def expand(ui, repo, *pats, **opts):
462 def expand(ui, repo, *pats, **opts):
463 '''expand keywords in the working directory
463 '''expand keywords in the working directory
464
464
465 Run after (re)enabling keyword expansion.
465 Run after (re)enabling keyword expansion.
466
466
467 kwexpand refuses to run if given files contain local changes.
467 kwexpand refuses to run if given files contain local changes.
468 '''
468 '''
469 # 3rd argument sets expansion to True
469 # 3rd argument sets expansion to True
470 _kwfwrite(ui, repo, True, *pats, **opts)
470 _kwfwrite(ui, repo, True, *pats, **opts)
471
471
472 @command('kwfiles',
472 @command('kwfiles',
473 [('A', 'all', None, _('show keyword status flags of all files')),
473 [('A', 'all', None, _('show keyword status flags of all files')),
474 ('i', 'ignore', None, _('show files excluded from expansion')),
474 ('i', 'ignore', None, _('show files excluded from expansion')),
475 ('u', 'unknown', None, _('only show unknown (not tracked) files')),
475 ('u', 'unknown', None, _('only show unknown (not tracked) files')),
476 ] + commands.walkopts,
476 ] + commands.walkopts,
477 _('hg kwfiles [OPTION]... [FILE]...'),
477 _('hg kwfiles [OPTION]... [FILE]...'),
478 inferrepo=True)
478 inferrepo=True)
479 def files(ui, repo, *pats, **opts):
479 def files(ui, repo, *pats, **opts):
480 '''show files configured for keyword expansion
480 '''show files configured for keyword expansion
481
481
482 List which files in the working directory are matched by the
482 List which files in the working directory are matched by the
483 [keyword] configuration patterns.
483 [keyword] configuration patterns.
484
484
485 Useful to prevent inadvertent keyword expansion and to speed up
485 Useful to prevent inadvertent keyword expansion and to speed up
486 execution by including only files that are actual candidates for
486 execution by including only files that are actual candidates for
487 expansion.
487 expansion.
488
488
489 See :hg:`help keyword` on how to construct patterns both for
489 See :hg:`help keyword` on how to construct patterns both for
490 inclusion and exclusion of files.
490 inclusion and exclusion of files.
491
491
492 With -A/--all and -v/--verbose the codes used to show the status
492 With -A/--all and -v/--verbose the codes used to show the status
493 of files are::
493 of files are::
494
494
495 K = keyword expansion candidate
495 K = keyword expansion candidate
496 k = keyword expansion candidate (not tracked)
496 k = keyword expansion candidate (not tracked)
497 I = ignored
497 I = ignored
498 i = ignored (not tracked)
498 i = ignored (not tracked)
499 '''
499 '''
500 kwt = kwtools['templater']
500 kwt = kwtools['templater']
501 wctx = repo[None]
501 wctx = repo[None]
502 status = _status(ui, repo, wctx, kwt, *pats, **opts)
502 status = _status(ui, repo, wctx, kwt, *pats, **opts)
503 cwd = pats and repo.getcwd() or ''
503 cwd = pats and repo.getcwd() or ''
504 files = []
504 files = []
505 if not opts.get('unknown') or opts.get('all'):
505 if not opts.get('unknown') or opts.get('all'):
506 files = sorted(status.modified + status.added + status.clean)
506 files = sorted(status.modified + status.added + status.clean)
507 kwfiles = kwt.iskwfile(files, wctx)
507 kwfiles = kwt.iskwfile(files, wctx)
508 kwdeleted = kwt.iskwfile(status.deleted, wctx)
508 kwdeleted = kwt.iskwfile(status.deleted, wctx)
509 kwunknown = kwt.iskwfile(status.unknown, wctx)
509 kwunknown = kwt.iskwfile(status.unknown, wctx)
510 if not opts.get('ignore') or opts.get('all'):
510 if not opts.get('ignore') or opts.get('all'):
511 showfiles = kwfiles, kwdeleted, kwunknown
511 showfiles = kwfiles, kwdeleted, kwunknown
512 else:
512 else:
513 showfiles = [], [], []
513 showfiles = [], [], []
514 if opts.get('all') or opts.get('ignore'):
514 if opts.get('all') or opts.get('ignore'):
515 showfiles += ([f for f in files if f not in kwfiles],
515 showfiles += ([f for f in files if f not in kwfiles],
516 [f for f in status.unknown if f not in kwunknown])
516 [f for f in status.unknown if f not in kwunknown])
517 kwlabels = 'enabled deleted enabledunknown ignored ignoredunknown'.split()
517 kwlabels = 'enabled deleted enabledunknown ignored ignoredunknown'.split()
518 kwstates = zip(kwlabels, 'K!kIi', showfiles)
518 kwstates = zip(kwlabels, 'K!kIi', showfiles)
519 fm = ui.formatter('kwfiles', opts)
519 fm = ui.formatter('kwfiles', opts)
520 fmt = '%.0s%s\n'
520 fmt = '%.0s%s\n'
521 if opts.get('all') or ui.verbose:
521 if opts.get('all') or ui.verbose:
522 fmt = '%s %s\n'
522 fmt = '%s %s\n'
523 for kwstate, char, filenames in kwstates:
523 for kwstate, char, filenames in kwstates:
524 label = 'kwfiles.' + kwstate
524 label = 'kwfiles.' + kwstate
525 for f in filenames:
525 for f in filenames:
526 fm.startitem()
526 fm.startitem()
527 fm.write('kwstatus path', fmt, char,
527 fm.write('kwstatus path', fmt, char,
528 repo.pathto(f, cwd), label=label)
528 repo.pathto(f, cwd), label=label)
529 fm.end()
529 fm.end()
530
530
531 @command('kwshrink',
531 @command('kwshrink',
532 commands.walkopts,
532 commands.walkopts,
533 _('hg kwshrink [OPTION]... [FILE]...'),
533 _('hg kwshrink [OPTION]... [FILE]...'),
534 inferrepo=True)
534 inferrepo=True)
535 def shrink(ui, repo, *pats, **opts):
535 def shrink(ui, repo, *pats, **opts):
536 '''revert expanded keywords in the working directory
536 '''revert expanded keywords in the working directory
537
537
538 Must be run before changing/disabling active keywords.
538 Must be run before changing/disabling active keywords.
539
539
540 kwshrink refuses to run if given files contain local changes.
540 kwshrink refuses to run if given files contain local changes.
541 '''
541 '''
542 # 3rd argument sets expansion to False
542 # 3rd argument sets expansion to False
543 _kwfwrite(ui, repo, False, *pats, **opts)
543 _kwfwrite(ui, repo, False, *pats, **opts)
544
544
545
545
546 def uisetup(ui):
546 def uisetup(ui):
547 ''' Monkeypatches dispatch._parse to retrieve user command.'''
547 ''' Monkeypatches dispatch._parse to retrieve user command.'''
548
548
549 def kwdispatch_parse(orig, ui, args):
549 def kwdispatch_parse(orig, ui, args):
550 '''Monkeypatch dispatch._parse to obtain running hg command.'''
550 '''Monkeypatch dispatch._parse to obtain running hg command.'''
551 cmd, func, args, options, cmdoptions = orig(ui, args)
551 cmd, func, args, options, cmdoptions = orig(ui, args)
552 kwtools['hgcmd'] = cmd
552 kwtools['hgcmd'] = cmd
553 return cmd, func, args, options, cmdoptions
553 return cmd, func, args, options, cmdoptions
554
554
555 extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse)
555 extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse)
556
556
557 def reposetup(ui, repo):
557 def reposetup(ui, repo):
558 '''Sets up repo as kwrepo for keyword substitution.
558 '''Sets up repo as kwrepo for keyword substitution.
559 Overrides file method to return kwfilelog instead of filelog
559 Overrides file method to return kwfilelog instead of filelog
560 if file matches user configuration.
560 if file matches user configuration.
561 Wraps commit to overwrite configured files with updated
561 Wraps commit to overwrite configured files with updated
562 keyword substitutions.
562 keyword substitutions.
563 Monkeypatches patch and webcommands.'''
563 Monkeypatches patch and webcommands.'''
564
564
565 try:
565 try:
566 if (not repo.local() or kwtools['hgcmd'] in nokwcommands.split()
566 if (not repo.local() or kwtools['hgcmd'] in nokwcommands.split()
567 or '.hg' in util.splitpath(repo.root)
567 or '.hg' in util.splitpath(repo.root)
568 or repo._url.startswith('bundle:')):
568 or repo._url.startswith('bundle:')):
569 return
569 return
570 except AttributeError:
570 except AttributeError:
571 pass
571 pass
572
572
573 inc, exc = [], ['.hg*']
573 inc, exc = [], ['.hg*']
574 for pat, opt in ui.configitems('keyword'):
574 for pat, opt in ui.configitems('keyword'):
575 if opt != 'ignore':
575 if opt != 'ignore':
576 inc.append(pat)
576 inc.append(pat)
577 else:
577 else:
578 exc.append(pat)
578 exc.append(pat)
579 if not inc:
579 if not inc:
580 return
580 return
581
581
582 kwtools['templater'] = kwt = kwtemplater(ui, repo, inc, exc)
582 kwtools['templater'] = kwt = kwtemplater(ui, repo, inc, exc)
583
583
584 class kwrepo(repo.__class__):
584 class kwrepo(repo.__class__):
585 def file(self, f):
585 def file(self, f):
586 if f[0] == '/':
586 if f[0] == '/':
587 f = f[1:]
587 f = f[1:]
588 return kwfilelog(self.sopener, kwt, f)
588 return kwfilelog(self.sopener, kwt, f)
589
589
590 def wread(self, filename):
590 def wread(self, filename):
591 data = super(kwrepo, self).wread(filename)
591 data = super(kwrepo, self).wread(filename)
592 return kwt.wread(filename, data)
592 return kwt.wread(filename, data)
593
593
594 def commit(self, *args, **opts):
594 def commit(self, *args, **opts):
595 # use custom commitctx for user commands
595 # use custom commitctx for user commands
596 # other extensions can still wrap repo.commitctx directly
596 # other extensions can still wrap repo.commitctx directly
597 self.commitctx = self.kwcommitctx
597 self.commitctx = self.kwcommitctx
598 try:
598 try:
599 return super(kwrepo, self).commit(*args, **opts)
599 return super(kwrepo, self).commit(*args, **opts)
600 finally:
600 finally:
601 del self.commitctx
601 del self.commitctx
602
602
603 def kwcommitctx(self, ctx, error=False):
603 def kwcommitctx(self, ctx, error=False):
604 n = super(kwrepo, self).commitctx(ctx, error)
604 n = super(kwrepo, self).commitctx(ctx, error)
605 # no lock needed, only called from repo.commit() which already locks
605 # no lock needed, only called from repo.commit() which already locks
606 if not kwt.postcommit:
606 if not kwt.postcommit:
607 restrict = kwt.restrict
607 restrict = kwt.restrict
608 kwt.restrict = True
608 kwt.restrict = True
609 kwt.overwrite(self[n], sorted(ctx.added() + ctx.modified()),
609 kwt.overwrite(self[n], sorted(ctx.added() + ctx.modified()),
610 False, True)
610 False, True)
611 kwt.restrict = restrict
611 kwt.restrict = restrict
612 return n
612 return n
613
613
614 def rollback(self, dryrun=False, force=False):
614 def rollback(self, dryrun=False, force=False):
615 wlock = self.wlock()
615 wlock = self.wlock()
616 try:
616 try:
617 if not dryrun:
617 if not dryrun:
618 changed = self['.'].files()
618 changed = self['.'].files()
619 ret = super(kwrepo, self).rollback(dryrun, force)
619 ret = super(kwrepo, self).rollback(dryrun, force)
620 if not dryrun:
620 if not dryrun:
621 ctx = self['.']
621 ctx = self['.']
622 modified, added = _preselect(self[None].status(), changed)
622 modified, added = _preselect(ctx.status(), changed)
623 kwt.overwrite(ctx, modified, True, True)
623 kwt.overwrite(ctx, modified, True, True)
624 kwt.overwrite(ctx, added, True, False)
624 kwt.overwrite(ctx, added, True, False)
625 return ret
625 return ret
626 finally:
626 finally:
627 wlock.release()
627 wlock.release()
628
628
629 # monkeypatches
629 # monkeypatches
630 def kwpatchfile_init(orig, self, ui, gp, backend, store, eolmode=None):
630 def kwpatchfile_init(orig, self, ui, gp, backend, store, eolmode=None):
631 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
631 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
632 rejects or conflicts due to expanded keywords in working dir.'''
632 rejects or conflicts due to expanded keywords in working dir.'''
633 orig(self, ui, gp, backend, store, eolmode)
633 orig(self, ui, gp, backend, store, eolmode)
634 # shrink keywords read from working dir
634 # shrink keywords read from working dir
635 self.lines = kwt.shrinklines(self.fname, self.lines)
635 self.lines = kwt.shrinklines(self.fname, self.lines)
636
636
637 def kw_diff(orig, repo, node1=None, node2=None, match=None, changes=None,
637 def kw_diff(orig, repo, node1=None, node2=None, match=None, changes=None,
638 opts=None, prefix=''):
638 opts=None, prefix=''):
639 '''Monkeypatch patch.diff to avoid expansion.'''
639 '''Monkeypatch patch.diff to avoid expansion.'''
640 kwt.restrict = True
640 kwt.restrict = True
641 return orig(repo, node1, node2, match, changes, opts, prefix)
641 return orig(repo, node1, node2, match, changes, opts, prefix)
642
642
643 def kwweb_skip(orig, web, req, tmpl):
643 def kwweb_skip(orig, web, req, tmpl):
644 '''Wraps webcommands.x turning off keyword expansion.'''
644 '''Wraps webcommands.x turning off keyword expansion.'''
645 kwt.match = util.never
645 kwt.match = util.never
646 return orig(web, req, tmpl)
646 return orig(web, req, tmpl)
647
647
648 def kw_amend(orig, ui, repo, commitfunc, old, extra, pats, opts):
648 def kw_amend(orig, ui, repo, commitfunc, old, extra, pats, opts):
649 '''Wraps cmdutil.amend expanding keywords after amend.'''
649 '''Wraps cmdutil.amend expanding keywords after amend.'''
650 wlock = repo.wlock()
650 wlock = repo.wlock()
651 try:
651 try:
652 kwt.postcommit = True
652 kwt.postcommit = True
653 newid = orig(ui, repo, commitfunc, old, extra, pats, opts)
653 newid = orig(ui, repo, commitfunc, old, extra, pats, opts)
654 if newid != old.node():
654 if newid != old.node():
655 ctx = repo[newid]
655 ctx = repo[newid]
656 kwt.restrict = True
656 kwt.restrict = True
657 kwt.overwrite(ctx, ctx.files(), False, True)
657 kwt.overwrite(ctx, ctx.files(), False, True)
658 kwt.restrict = False
658 kwt.restrict = False
659 return newid
659 return newid
660 finally:
660 finally:
661 wlock.release()
661 wlock.release()
662
662
663 def kw_copy(orig, ui, repo, pats, opts, rename=False):
663 def kw_copy(orig, ui, repo, pats, opts, rename=False):
664 '''Wraps cmdutil.copy so that copy/rename destinations do not
664 '''Wraps cmdutil.copy so that copy/rename destinations do not
665 contain expanded keywords.
665 contain expanded keywords.
666 Note that the source of a regular file destination may also be a
666 Note that the source of a regular file destination may also be a
667 symlink:
667 symlink:
668 hg cp sym x -> x is symlink
668 hg cp sym x -> x is symlink
669 cp sym x; hg cp -A sym x -> x is file (maybe expanded keywords)
669 cp sym x; hg cp -A sym x -> x is file (maybe expanded keywords)
670 For the latter we have to follow the symlink to find out whether its
670 For the latter we have to follow the symlink to find out whether its
671 target is configured for expansion and we therefore must unexpand the
671 target is configured for expansion and we therefore must unexpand the
672 keywords in the destination.'''
672 keywords in the destination.'''
673 wlock = repo.wlock()
673 wlock = repo.wlock()
674 try:
674 try:
675 orig(ui, repo, pats, opts, rename)
675 orig(ui, repo, pats, opts, rename)
676 if opts.get('dry_run'):
676 if opts.get('dry_run'):
677 return
677 return
678 wctx = repo[None]
678 wctx = repo[None]
679 cwd = repo.getcwd()
679 cwd = repo.getcwd()
680
680
681 def haskwsource(dest):
681 def haskwsource(dest):
682 '''Returns true if dest is a regular file and configured for
682 '''Returns true if dest is a regular file and configured for
683 expansion or a symlink which points to a file configured for
683 expansion or a symlink which points to a file configured for
684 expansion. '''
684 expansion. '''
685 source = repo.dirstate.copied(dest)
685 source = repo.dirstate.copied(dest)
686 if 'l' in wctx.flags(source):
686 if 'l' in wctx.flags(source):
687 source = pathutil.canonpath(repo.root, cwd,
687 source = pathutil.canonpath(repo.root, cwd,
688 os.path.realpath(source))
688 os.path.realpath(source))
689 return kwt.match(source)
689 return kwt.match(source)
690
690
691 candidates = [f for f in repo.dirstate.copies() if
691 candidates = [f for f in repo.dirstate.copies() if
692 'l' not in wctx.flags(f) and haskwsource(f)]
692 'l' not in wctx.flags(f) and haskwsource(f)]
693 kwt.overwrite(wctx, candidates, False, False)
693 kwt.overwrite(wctx, candidates, False, False)
694 finally:
694 finally:
695 wlock.release()
695 wlock.release()
696
696
697 def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts):
697 def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts):
698 '''Wraps record.dorecord expanding keywords after recording.'''
698 '''Wraps record.dorecord expanding keywords after recording.'''
699 wlock = repo.wlock()
699 wlock = repo.wlock()
700 try:
700 try:
701 # record returns 0 even when nothing has changed
701 # record returns 0 even when nothing has changed
702 # therefore compare nodes before and after
702 # therefore compare nodes before and after
703 kwt.postcommit = True
703 kwt.postcommit = True
704 ctx = repo['.']
704 ctx = repo['.']
705 wstatus = repo[None].status()
705 wstatus = ctx.status()
706 ret = orig(ui, repo, commitfunc, *pats, **opts)
706 ret = orig(ui, repo, commitfunc, *pats, **opts)
707 recctx = repo['.']
707 recctx = repo['.']
708 if ctx != recctx:
708 if ctx != recctx:
709 modified, added = _preselect(wstatus, recctx.files())
709 modified, added = _preselect(wstatus, recctx.files())
710 kwt.restrict = False
710 kwt.restrict = False
711 kwt.overwrite(recctx, modified, False, True)
711 kwt.overwrite(recctx, modified, False, True)
712 kwt.overwrite(recctx, added, False, True, True)
712 kwt.overwrite(recctx, added, False, True, True)
713 kwt.restrict = True
713 kwt.restrict = True
714 return ret
714 return ret
715 finally:
715 finally:
716 wlock.release()
716 wlock.release()
717
717
718 def kwfilectx_cmp(orig, self, fctx):
718 def kwfilectx_cmp(orig, self, fctx):
719 # keyword affects data size, comparing wdir and filelog size does
719 # keyword affects data size, comparing wdir and filelog size does
720 # not make sense
720 # not make sense
721 if (fctx._filerev is None and
721 if (fctx._filerev is None and
722 (self._repo._encodefilterpats or
722 (self._repo._encodefilterpats or
723 kwt.match(fctx.path()) and 'l' not in fctx.flags() or
723 kwt.match(fctx.path()) and 'l' not in fctx.flags() or
724 self.size() - 4 == fctx.size()) or
724 self.size() - 4 == fctx.size()) or
725 self.size() == fctx.size()):
725 self.size() == fctx.size()):
726 return self._filelog.cmp(self._filenode, fctx.data())
726 return self._filelog.cmp(self._filenode, fctx.data())
727 return True
727 return True
728
728
729 extensions.wrapfunction(context.filectx, 'cmp', kwfilectx_cmp)
729 extensions.wrapfunction(context.filectx, 'cmp', kwfilectx_cmp)
730 extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init)
730 extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init)
731 extensions.wrapfunction(patch, 'diff', kw_diff)
731 extensions.wrapfunction(patch, 'diff', kw_diff)
732 extensions.wrapfunction(cmdutil, 'amend', kw_amend)
732 extensions.wrapfunction(cmdutil, 'amend', kw_amend)
733 extensions.wrapfunction(cmdutil, 'copy', kw_copy)
733 extensions.wrapfunction(cmdutil, 'copy', kw_copy)
734 for c in 'annotate changeset rev filediff diff'.split():
734 for c in 'annotate changeset rev filediff diff'.split():
735 extensions.wrapfunction(webcommands, c, kwweb_skip)
735 extensions.wrapfunction(webcommands, c, kwweb_skip)
736 for name in recordextensions.split():
736 for name in recordextensions.split():
737 try:
737 try:
738 record = extensions.find(name)
738 record = extensions.find(name)
739 extensions.wrapfunction(record, 'dorecord', kw_dorecord)
739 extensions.wrapfunction(record, 'dorecord', kw_dorecord)
740 except KeyError:
740 except KeyError:
741 pass
741 pass
742
742
743 repo.__class__ = kwrepo
743 repo.__class__ = kwrepo
General Comments 0
You need to be logged in to leave comments. Login now