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