##// END OF EJS Templates
templatefilters: avoid traceback caused by bogus date input (issue3344)...
Christian Ebert -
r17755:bededd3f default
parent child Browse files
Show More
@@ -1,730 +1,730 b''
1 # keyword.py - $Keyword$ expansion for Mercurial
1 # keyword.py - $Keyword$ expansion for Mercurial
2 #
2 #
3 # Copyright 2007-2012 Christian Ebert <blacktrash@gmx.net>
3 # Copyright 2007-2012 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 The more specific you are in your filename patterns the less you
56 The more specific you are in your filename patterns the less you
57 lose speed in huge repositories.
57 lose speed in huge repositories.
58
58
59 For [keywordmaps] template mapping and expansion demonstration and
59 For [keywordmaps] template mapping and expansion demonstration and
60 control run :hg:`kwdemo`. See :hg:`help templates` for a list of
60 control run :hg:`kwdemo`. See :hg:`help templates` for a list of
61 available templates and filters.
61 available templates and filters.
62
62
63 Three additional date template filters are provided:
63 Three additional date template filters are provided:
64
64
65 :``utcdate``: "2006/09/18 15:13:13"
65 :``utcdate``: "2006/09/18 15:13:13"
66 :``svnutcdate``: "2006-09-18 15:13:13Z"
66 :``svnutcdate``: "2006-09-18 15:13:13Z"
67 :``svnisodate``: "2006-09-18 08:13:13 -700 (Mon, 18 Sep 2006)"
67 :``svnisodate``: "2006-09-18 08:13:13 -700 (Mon, 18 Sep 2006)"
68
68
69 The default template mappings (view with :hg:`kwdemo -d`) can be
69 The default template mappings (view with :hg:`kwdemo -d`) can be
70 replaced with customized keywords and templates. Again, run
70 replaced with customized keywords and templates. Again, run
71 :hg:`kwdemo` to control the results of your configuration changes.
71 :hg:`kwdemo` to control the results of your configuration changes.
72
72
73 Before changing/disabling active keywords, you must run :hg:`kwshrink`
73 Before changing/disabling active keywords, you must run :hg:`kwshrink`
74 to avoid storing expanded keywords in the change history.
74 to avoid storing expanded keywords in the change history.
75
75
76 To force expansion after enabling it, or a configuration change, run
76 To force expansion after enabling it, or a configuration change, run
77 :hg:`kwexpand`.
77 :hg:`kwexpand`.
78
78
79 Expansions spanning more than one line and incremental expansions,
79 Expansions spanning more than one line and incremental expansions,
80 like CVS' $Log$, are not supported. A keyword template map "Log =
80 like CVS' $Log$, are not supported. A keyword template map "Log =
81 {desc}" expands to the first line of the changeset description.
81 {desc}" expands to the first line of the changeset description.
82 '''
82 '''
83
83
84 from mercurial import commands, context, cmdutil, dispatch, filelog, extensions
84 from mercurial import commands, context, cmdutil, dispatch, filelog, extensions
85 from mercurial import localrepo, match, patch, templatefilters, templater, util
85 from mercurial import localrepo, match, patch, templatefilters, templater, util
86 from mercurial import scmutil
86 from mercurial import scmutil
87 from mercurial.hgweb import webcommands
87 from mercurial.hgweb import webcommands
88 from mercurial.i18n import _
88 from mercurial.i18n import _
89 import os, re, shutil, tempfile
89 import os, re, shutil, tempfile
90
90
91 commands.optionalrepo += ' kwdemo'
91 commands.optionalrepo += ' kwdemo'
92
92
93 cmdtable = {}
93 cmdtable = {}
94 command = cmdutil.command(cmdtable)
94 command = cmdutil.command(cmdtable)
95 testedwith = 'internal'
95 testedwith = 'internal'
96
96
97 # hg commands that do not act on keywords
97 # hg commands that do not act on keywords
98 nokwcommands = ('add addremove annotate bundle export grep incoming init log'
98 nokwcommands = ('add addremove annotate bundle export grep incoming init log'
99 ' outgoing push tip verify convert email glog')
99 ' outgoing push tip verify convert email glog')
100
100
101 # hg commands that trigger expansion only when writing to working dir,
101 # hg commands that trigger expansion only when writing to working dir,
102 # not when reading filelog, and unexpand when reading from working dir
102 # not when reading filelog, and unexpand when reading from working dir
103 restricted = 'merge kwexpand kwshrink record qrecord resolve transplant'
103 restricted = 'merge kwexpand kwshrink record qrecord resolve transplant'
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((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((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, added = wstatus[:2]
174 modified, added = wstatus[:2]
175 modified = [f for f in modified if f in changed]
175 modified = [f for f in modified if f in changed]
176 added = [f for f in added if f in changed]
176 added = [f for f in added if f in changed]
177 return modified, added
177 return modified, added
178
178
179
179
180 class kwtemplater(object):
180 class kwtemplater(object):
181 '''
181 '''
182 Sets up keyword templates, corresponding keyword regex, and
182 Sets up keyword templates, corresponding keyword regex, and
183 provides keyword substitution functions.
183 provides keyword substitution functions.
184 '''
184 '''
185
185
186 def __init__(self, ui, repo, inc, exc):
186 def __init__(self, ui, repo, inc, exc):
187 self.ui = ui
187 self.ui = ui
188 self.repo = repo
188 self.repo = repo
189 self.match = match.match(repo.root, '', [], inc, exc)
189 self.match = match.match(repo.root, '', [], inc, exc)
190 self.restrict = kwtools['hgcmd'] in restricted.split()
190 self.restrict = kwtools['hgcmd'] in restricted.split()
191 self.postcommit = False
191 self.postcommit = False
192
192
193 kwmaps = self.ui.configitems('keywordmaps')
193 kwmaps = self.ui.configitems('keywordmaps')
194 if kwmaps: # override default templates
194 if kwmaps: # override default templates
195 self.templates = dict((k, templater.parsestring(v, False))
195 self.templates = dict((k, templater.parsestring(v, False))
196 for k, v in kwmaps)
196 for k, v in kwmaps)
197 else:
197 else:
198 self.templates = _defaultkwmaps(self.ui)
198 self.templates = _defaultkwmaps(self.ui)
199
199
200 @util.propertycache
200 @util.propertycache
201 def escape(self):
201 def escape(self):
202 '''Returns bar-separated and escaped keywords.'''
202 '''Returns bar-separated and escaped keywords.'''
203 return '|'.join(map(re.escape, self.templates.keys()))
203 return '|'.join(map(re.escape, self.templates.keys()))
204
204
205 @util.propertycache
205 @util.propertycache
206 def rekw(self):
206 def rekw(self):
207 '''Returns regex for unexpanded keywords.'''
207 '''Returns regex for unexpanded keywords.'''
208 return re.compile(r'\$(%s)\$' % self.escape)
208 return re.compile(r'\$(%s)\$' % self.escape)
209
209
210 @util.propertycache
210 @util.propertycache
211 def rekwexp(self):
211 def rekwexp(self):
212 '''Returns regex for expanded keywords.'''
212 '''Returns regex for expanded keywords.'''
213 return re.compile(r'\$(%s): [^$\n\r]*? \$' % self.escape)
213 return re.compile(r'\$(%s): [^$\n\r]*? \$' % self.escape)
214
214
215 def substitute(self, data, path, ctx, subfunc):
215 def substitute(self, data, path, ctx, subfunc):
216 '''Replaces keywords in data with expanded template.'''
216 '''Replaces keywords in data with expanded template.'''
217 def kwsub(mobj):
217 def kwsub(mobj):
218 kw = mobj.group(1)
218 kw = mobj.group(1)
219 ct = cmdutil.changeset_templater(self.ui, self.repo,
219 ct = cmdutil.changeset_templater(self.ui, self.repo,
220 False, None, '', False)
220 False, None, '', False)
221 ct.use_template(self.templates[kw])
221 ct.use_template(self.templates[kw])
222 self.ui.pushbuffer()
222 self.ui.pushbuffer()
223 ct.show(ctx, root=self.repo.root, file=path)
223 ct.show(ctx, root=self.repo.root, file=path)
224 ekw = templatefilters.firstline(self.ui.popbuffer())
224 ekw = templatefilters.firstline(self.ui.popbuffer())
225 return '$%s: %s $' % (kw, ekw)
225 return '$%s: %s $' % (kw, ekw)
226 return subfunc(kwsub, data)
226 return subfunc(kwsub, data)
227
227
228 def linkctx(self, path, fileid):
228 def linkctx(self, path, fileid):
229 '''Similar to filelog.linkrev, but returns a changectx.'''
229 '''Similar to filelog.linkrev, but returns a changectx.'''
230 return self.repo.filectx(path, fileid=fileid).changectx()
230 return self.repo.filectx(path, fileid=fileid).changectx()
231
231
232 def expand(self, path, node, data):
232 def expand(self, path, node, data):
233 '''Returns data with keywords expanded.'''
233 '''Returns data with keywords expanded.'''
234 if not self.restrict and self.match(path) and not util.binary(data):
234 if not self.restrict and self.match(path) and not util.binary(data):
235 ctx = self.linkctx(path, node)
235 ctx = self.linkctx(path, node)
236 return self.substitute(data, path, ctx, self.rekw.sub)
236 return self.substitute(data, path, ctx, self.rekw.sub)
237 return data
237 return data
238
238
239 def iskwfile(self, cand, ctx):
239 def iskwfile(self, cand, ctx):
240 '''Returns subset of candidates which are configured for keyword
240 '''Returns subset of candidates which are configured for keyword
241 expansion but are not symbolic links.'''
241 expansion but are not symbolic links.'''
242 return [f for f in cand if self.match(f) and 'l' not in ctx.flags(f)]
242 return [f for f in cand if self.match(f) and 'l' not in ctx.flags(f)]
243
243
244 def overwrite(self, ctx, candidates, lookup, expand, rekw=False):
244 def overwrite(self, ctx, candidates, lookup, expand, rekw=False):
245 '''Overwrites selected files expanding/shrinking keywords.'''
245 '''Overwrites selected files expanding/shrinking keywords.'''
246 if self.restrict or lookup or self.postcommit: # exclude kw_copy
246 if self.restrict or lookup or self.postcommit: # exclude kw_copy
247 candidates = self.iskwfile(candidates, ctx)
247 candidates = self.iskwfile(candidates, ctx)
248 if not candidates:
248 if not candidates:
249 return
249 return
250 kwcmd = self.restrict and lookup # kwexpand/kwshrink
250 kwcmd = self.restrict and lookup # kwexpand/kwshrink
251 if self.restrict or expand and lookup:
251 if self.restrict or expand and lookup:
252 mf = ctx.manifest()
252 mf = ctx.manifest()
253 if self.restrict or rekw:
253 if self.restrict or rekw:
254 re_kw = self.rekw
254 re_kw = self.rekw
255 else:
255 else:
256 re_kw = self.rekwexp
256 re_kw = self.rekwexp
257 if expand:
257 if expand:
258 msg = _('overwriting %s expanding keywords\n')
258 msg = _('overwriting %s expanding keywords\n')
259 else:
259 else:
260 msg = _('overwriting %s shrinking keywords\n')
260 msg = _('overwriting %s shrinking keywords\n')
261 for f in candidates:
261 for f in candidates:
262 if self.restrict:
262 if self.restrict:
263 data = self.repo.file(f).read(mf[f])
263 data = self.repo.file(f).read(mf[f])
264 else:
264 else:
265 data = self.repo.wread(f)
265 data = self.repo.wread(f)
266 if util.binary(data):
266 if util.binary(data):
267 continue
267 continue
268 if expand:
268 if expand:
269 if lookup:
269 if lookup:
270 ctx = self.linkctx(f, mf[f])
270 ctx = self.linkctx(f, mf[f])
271 data, found = self.substitute(data, f, ctx, re_kw.subn)
271 data, found = self.substitute(data, f, ctx, re_kw.subn)
272 elif self.restrict:
272 elif self.restrict:
273 found = re_kw.search(data)
273 found = re_kw.search(data)
274 else:
274 else:
275 data, found = _shrinktext(data, re_kw.subn)
275 data, found = _shrinktext(data, re_kw.subn)
276 if found:
276 if found:
277 self.ui.note(msg % f)
277 self.ui.note(msg % f)
278 fp = self.repo.wopener(f, "wb", atomictemp=True)
278 fp = self.repo.wopener(f, "wb", atomictemp=True)
279 fp.write(data)
279 fp.write(data)
280 fp.close()
280 fp.close()
281 if kwcmd:
281 if kwcmd:
282 self.repo.dirstate.normal(f)
282 self.repo.dirstate.normal(f)
283 elif self.postcommit:
283 elif self.postcommit:
284 self.repo.dirstate.normallookup(f)
284 self.repo.dirstate.normallookup(f)
285
285
286 def shrink(self, fname, text):
286 def shrink(self, fname, text):
287 '''Returns text with all keyword substitutions removed.'''
287 '''Returns text with all keyword substitutions removed.'''
288 if self.match(fname) and not util.binary(text):
288 if self.match(fname) and not util.binary(text):
289 return _shrinktext(text, self.rekwexp.sub)
289 return _shrinktext(text, self.rekwexp.sub)
290 return text
290 return text
291
291
292 def shrinklines(self, fname, lines):
292 def shrinklines(self, fname, lines):
293 '''Returns lines with keyword substitutions removed.'''
293 '''Returns lines with keyword substitutions removed.'''
294 if self.match(fname):
294 if self.match(fname):
295 text = ''.join(lines)
295 text = ''.join(lines)
296 if not util.binary(text):
296 if not util.binary(text):
297 return _shrinktext(text, self.rekwexp.sub).splitlines(True)
297 return _shrinktext(text, self.rekwexp.sub).splitlines(True)
298 return lines
298 return lines
299
299
300 def wread(self, fname, data):
300 def wread(self, fname, data):
301 '''If in restricted mode returns data read from wdir with
301 '''If in restricted mode returns data read from wdir with
302 keyword substitutions removed.'''
302 keyword substitutions removed.'''
303 if self.restrict:
303 if self.restrict:
304 return self.shrink(fname, data)
304 return self.shrink(fname, data)
305 return data
305 return data
306
306
307 class kwfilelog(filelog.filelog):
307 class kwfilelog(filelog.filelog):
308 '''
308 '''
309 Subclass of filelog to hook into its read, add, cmp methods.
309 Subclass of filelog to hook into its read, add, cmp methods.
310 Keywords are "stored" unexpanded, and processed on reading.
310 Keywords are "stored" unexpanded, and processed on reading.
311 '''
311 '''
312 def __init__(self, opener, kwt, path):
312 def __init__(self, opener, kwt, path):
313 super(kwfilelog, self).__init__(opener, path)
313 super(kwfilelog, self).__init__(opener, path)
314 self.kwt = kwt
314 self.kwt = kwt
315 self.path = path
315 self.path = path
316
316
317 def read(self, node):
317 def read(self, node):
318 '''Expands keywords when reading filelog.'''
318 '''Expands keywords when reading filelog.'''
319 data = super(kwfilelog, self).read(node)
319 data = super(kwfilelog, self).read(node)
320 if self.renamed(node):
320 if self.renamed(node):
321 return data
321 return data
322 return self.kwt.expand(self.path, node, data)
322 return self.kwt.expand(self.path, node, data)
323
323
324 def add(self, text, meta, tr, link, p1=None, p2=None):
324 def add(self, text, meta, tr, link, p1=None, p2=None):
325 '''Removes keyword substitutions when adding to filelog.'''
325 '''Removes keyword substitutions when adding to filelog.'''
326 text = self.kwt.shrink(self.path, text)
326 text = self.kwt.shrink(self.path, text)
327 return super(kwfilelog, self).add(text, meta, tr, link, p1, p2)
327 return super(kwfilelog, self).add(text, meta, tr, link, p1, p2)
328
328
329 def cmp(self, node, text):
329 def cmp(self, node, text):
330 '''Removes keyword substitutions for comparison.'''
330 '''Removes keyword substitutions for comparison.'''
331 text = self.kwt.shrink(self.path, text)
331 text = self.kwt.shrink(self.path, text)
332 return super(kwfilelog, self).cmp(node, text)
332 return super(kwfilelog, self).cmp(node, text)
333
333
334 def _status(ui, repo, wctx, kwt, *pats, **opts):
334 def _status(ui, repo, wctx, kwt, *pats, **opts):
335 '''Bails out if [keyword] configuration is not active.
335 '''Bails out if [keyword] configuration is not active.
336 Returns status of working directory.'''
336 Returns status of working directory.'''
337 if kwt:
337 if kwt:
338 return repo.status(match=scmutil.match(wctx, pats, opts), clean=True,
338 return repo.status(match=scmutil.match(wctx, pats, opts), clean=True,
339 unknown=opts.get('unknown') or opts.get('all'))
339 unknown=opts.get('unknown') or opts.get('all'))
340 if ui.configitems('keyword'):
340 if ui.configitems('keyword'):
341 raise util.Abort(_('[keyword] patterns cannot match'))
341 raise util.Abort(_('[keyword] patterns cannot match'))
342 raise util.Abort(_('no [keyword] patterns configured'))
342 raise util.Abort(_('no [keyword] patterns configured'))
343
343
344 def _kwfwrite(ui, repo, expand, *pats, **opts):
344 def _kwfwrite(ui, repo, expand, *pats, **opts):
345 '''Selects files and passes them to kwtemplater.overwrite.'''
345 '''Selects files and passes them to kwtemplater.overwrite.'''
346 wctx = repo[None]
346 wctx = repo[None]
347 if len(wctx.parents()) > 1:
347 if len(wctx.parents()) > 1:
348 raise util.Abort(_('outstanding uncommitted merge'))
348 raise util.Abort(_('outstanding uncommitted merge'))
349 kwt = kwtools['templater']
349 kwt = kwtools['templater']
350 wlock = repo.wlock()
350 wlock = repo.wlock()
351 try:
351 try:
352 status = _status(ui, repo, wctx, kwt, *pats, **opts)
352 status = _status(ui, repo, wctx, kwt, *pats, **opts)
353 modified, added, removed, deleted, unknown, ignored, clean = status
353 modified, added, removed, deleted, unknown, ignored, clean = status
354 if modified or added or removed or deleted:
354 if modified or added or removed or deleted:
355 raise util.Abort(_('outstanding uncommitted changes'))
355 raise util.Abort(_('outstanding uncommitted changes'))
356 kwt.overwrite(wctx, clean, True, expand)
356 kwt.overwrite(wctx, clean, True, expand)
357 finally:
357 finally:
358 wlock.release()
358 wlock.release()
359
359
360 @command('kwdemo',
360 @command('kwdemo',
361 [('d', 'default', None, _('show default keyword template maps')),
361 [('d', 'default', None, _('show default keyword template maps')),
362 ('f', 'rcfile', '',
362 ('f', 'rcfile', '',
363 _('read maps from rcfile'), _('FILE'))],
363 _('read maps from rcfile'), _('FILE'))],
364 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...'))
364 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...'))
365 def demo(ui, repo, *args, **opts):
365 def demo(ui, repo, *args, **opts):
366 '''print [keywordmaps] configuration and an expansion example
366 '''print [keywordmaps] configuration and an expansion example
367
367
368 Show current, custom, or default keyword template maps and their
368 Show current, custom, or default keyword template maps and their
369 expansions.
369 expansions.
370
370
371 Extend the current configuration by specifying maps as arguments
371 Extend the current configuration by specifying maps as arguments
372 and using -f/--rcfile to source an external hgrc file.
372 and using -f/--rcfile to source an external hgrc file.
373
373
374 Use -d/--default to disable current configuration.
374 Use -d/--default to disable current configuration.
375
375
376 See :hg:`help templates` for information on templates and filters.
376 See :hg:`help templates` for information on templates and filters.
377 '''
377 '''
378 def demoitems(section, items):
378 def demoitems(section, items):
379 ui.write('[%s]\n' % section)
379 ui.write('[%s]\n' % section)
380 for k, v in sorted(items):
380 for k, v in sorted(items):
381 ui.write('%s = %s\n' % (k, v))
381 ui.write('%s = %s\n' % (k, v))
382
382
383 fn = 'demo.txt'
383 fn = 'demo.txt'
384 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
384 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
385 ui.note(_('creating temporary repository at %s\n') % tmpdir)
385 ui.note(_('creating temporary repository at %s\n') % tmpdir)
386 repo = localrepo.localrepository(ui, tmpdir, True)
386 repo = localrepo.localrepository(ui, tmpdir, True)
387 ui.setconfig('keyword', fn, '')
387 ui.setconfig('keyword', fn, '')
388 svn = ui.configbool('keywordset', 'svn')
388 svn = ui.configbool('keywordset', 'svn')
389 # explicitly set keywordset for demo output
389 # explicitly set keywordset for demo output
390 ui.setconfig('keywordset', 'svn', svn)
390 ui.setconfig('keywordset', 'svn', svn)
391
391
392 uikwmaps = ui.configitems('keywordmaps')
392 uikwmaps = ui.configitems('keywordmaps')
393 if args or opts.get('rcfile'):
393 if args or opts.get('rcfile'):
394 ui.status(_('\n\tconfiguration using custom keyword template maps\n'))
394 ui.status(_('\n\tconfiguration using custom keyword template maps\n'))
395 if uikwmaps:
395 if uikwmaps:
396 ui.status(_('\textending current template maps\n'))
396 ui.status(_('\textending current template maps\n'))
397 if opts.get('default') or not uikwmaps:
397 if opts.get('default') or not uikwmaps:
398 if svn:
398 if svn:
399 ui.status(_('\toverriding default svn keywordset\n'))
399 ui.status(_('\toverriding default svn keywordset\n'))
400 else:
400 else:
401 ui.status(_('\toverriding default cvs keywordset\n'))
401 ui.status(_('\toverriding default cvs keywordset\n'))
402 if opts.get('rcfile'):
402 if opts.get('rcfile'):
403 ui.readconfig(opts.get('rcfile'))
403 ui.readconfig(opts.get('rcfile'))
404 if args:
404 if args:
405 # simulate hgrc parsing
405 # simulate hgrc parsing
406 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
406 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
407 fp = repo.opener('hgrc', 'w')
407 fp = repo.opener('hgrc', 'w')
408 fp.writelines(rcmaps)
408 fp.writelines(rcmaps)
409 fp.close()
409 fp.close()
410 ui.readconfig(repo.join('hgrc'))
410 ui.readconfig(repo.join('hgrc'))
411 kwmaps = dict(ui.configitems('keywordmaps'))
411 kwmaps = dict(ui.configitems('keywordmaps'))
412 elif opts.get('default'):
412 elif opts.get('default'):
413 if svn:
413 if svn:
414 ui.status(_('\n\tconfiguration using default svn keywordset\n'))
414 ui.status(_('\n\tconfiguration using default svn keywordset\n'))
415 else:
415 else:
416 ui.status(_('\n\tconfiguration using default cvs keywordset\n'))
416 ui.status(_('\n\tconfiguration using default cvs keywordset\n'))
417 kwmaps = _defaultkwmaps(ui)
417 kwmaps = _defaultkwmaps(ui)
418 if uikwmaps:
418 if uikwmaps:
419 ui.status(_('\tdisabling current template maps\n'))
419 ui.status(_('\tdisabling current template maps\n'))
420 for k, v in kwmaps.iteritems():
420 for k, v in kwmaps.iteritems():
421 ui.setconfig('keywordmaps', k, v)
421 ui.setconfig('keywordmaps', k, v)
422 else:
422 else:
423 ui.status(_('\n\tconfiguration using current keyword template maps\n'))
423 ui.status(_('\n\tconfiguration using current keyword template maps\n'))
424 if uikwmaps:
424 if uikwmaps:
425 kwmaps = dict(uikwmaps)
425 kwmaps = dict(uikwmaps)
426 else:
426 else:
427 kwmaps = _defaultkwmaps(ui)
427 kwmaps = _defaultkwmaps(ui)
428
428
429 uisetup(ui)
429 uisetup(ui)
430 reposetup(ui, repo)
430 reposetup(ui, repo)
431 ui.write('[extensions]\nkeyword =\n')
431 ui.write('[extensions]\nkeyword =\n')
432 demoitems('keyword', ui.configitems('keyword'))
432 demoitems('keyword', ui.configitems('keyword'))
433 demoitems('keywordset', ui.configitems('keywordset'))
433 demoitems('keywordset', ui.configitems('keywordset'))
434 demoitems('keywordmaps', kwmaps.iteritems())
434 demoitems('keywordmaps', kwmaps.iteritems())
435 keywords = '$' + '$\n$'.join(sorted(kwmaps.keys())) + '$\n'
435 keywords = '$' + '$\n$'.join(sorted(kwmaps.keys())) + '$\n'
436 repo.wopener.write(fn, keywords)
436 repo.wopener.write(fn, keywords)
437 repo[None].add([fn])
437 repo[None].add([fn])
438 ui.note(_('\nkeywords written to %s:\n') % fn)
438 ui.note(_('\nkeywords written to %s:\n') % fn)
439 ui.note(keywords)
439 ui.note(keywords)
440 repo.dirstate.setbranch('demobranch')
440 repo.dirstate.setbranch('demobranch')
441 for name, cmd in ui.configitems('hooks'):
441 for name, cmd in ui.configitems('hooks'):
442 if name.split('.', 1)[0].find('commit') > -1:
442 if name.split('.', 1)[0].find('commit') > -1:
443 repo.ui.setconfig('hooks', name, '')
443 repo.ui.setconfig('hooks', name, '')
444 msg = _('hg keyword configuration and expansion example')
444 msg = _('hg keyword configuration and expansion example')
445 ui.note("hg ci -m '%s'\n" % msg) # check-code-ignore
445 ui.note("hg ci -m '%s'\n" % msg) # check-code-ignore
446 repo.commit(text=msg)
446 repo.commit(text=msg)
447 ui.status(_('\n\tkeywords expanded\n'))
447 ui.status(_('\n\tkeywords expanded\n'))
448 ui.write(repo.wread(fn))
448 ui.write(repo.wread(fn))
449 shutil.rmtree(tmpdir, ignore_errors=True)
449 shutil.rmtree(tmpdir, ignore_errors=True)
450
450
451 @command('kwexpand', commands.walkopts, _('hg kwexpand [OPTION]... [FILE]...'))
451 @command('kwexpand', commands.walkopts, _('hg kwexpand [OPTION]... [FILE]...'))
452 def expand(ui, repo, *pats, **opts):
452 def expand(ui, repo, *pats, **opts):
453 '''expand keywords in the working directory
453 '''expand keywords in the working directory
454
454
455 Run after (re)enabling keyword expansion.
455 Run after (re)enabling keyword expansion.
456
456
457 kwexpand refuses to run if given files contain local changes.
457 kwexpand refuses to run if given files contain local changes.
458 '''
458 '''
459 # 3rd argument sets expansion to True
459 # 3rd argument sets expansion to True
460 _kwfwrite(ui, repo, True, *pats, **opts)
460 _kwfwrite(ui, repo, True, *pats, **opts)
461
461
462 @command('kwfiles',
462 @command('kwfiles',
463 [('A', 'all', None, _('show keyword status flags of all files')),
463 [('A', 'all', None, _('show keyword status flags of all files')),
464 ('i', 'ignore', None, _('show files excluded from expansion')),
464 ('i', 'ignore', None, _('show files excluded from expansion')),
465 ('u', 'unknown', None, _('only show unknown (not tracked) files')),
465 ('u', 'unknown', None, _('only show unknown (not tracked) files')),
466 ] + commands.walkopts,
466 ] + commands.walkopts,
467 _('hg kwfiles [OPTION]... [FILE]...'))
467 _('hg kwfiles [OPTION]... [FILE]...'))
468 def files(ui, repo, *pats, **opts):
468 def files(ui, repo, *pats, **opts):
469 '''show files configured for keyword expansion
469 '''show files configured for keyword expansion
470
470
471 List which files in the working directory are matched by the
471 List which files in the working directory are matched by the
472 [keyword] configuration patterns.
472 [keyword] configuration patterns.
473
473
474 Useful to prevent inadvertent keyword expansion and to speed up
474 Useful to prevent inadvertent keyword expansion and to speed up
475 execution by including only files that are actual candidates for
475 execution by including only files that are actual candidates for
476 expansion.
476 expansion.
477
477
478 See :hg:`help keyword` on how to construct patterns both for
478 See :hg:`help keyword` on how to construct patterns both for
479 inclusion and exclusion of files.
479 inclusion and exclusion of files.
480
480
481 With -A/--all and -v/--verbose the codes used to show the status
481 With -A/--all and -v/--verbose the codes used to show the status
482 of files are::
482 of files are::
483
483
484 K = keyword expansion candidate
484 K = keyword expansion candidate
485 k = keyword expansion candidate (not tracked)
485 k = keyword expansion candidate (not tracked)
486 I = ignored
486 I = ignored
487 i = ignored (not tracked)
487 i = ignored (not tracked)
488 '''
488 '''
489 kwt = kwtools['templater']
489 kwt = kwtools['templater']
490 wctx = repo[None]
490 wctx = repo[None]
491 status = _status(ui, repo, wctx, kwt, *pats, **opts)
491 status = _status(ui, repo, wctx, kwt, *pats, **opts)
492 cwd = pats and repo.getcwd() or ''
492 cwd = pats and repo.getcwd() or ''
493 modified, added, removed, deleted, unknown, ignored, clean = status
493 modified, added, removed, deleted, unknown, ignored, clean = status
494 files = []
494 files = []
495 if not opts.get('unknown') or opts.get('all'):
495 if not opts.get('unknown') or opts.get('all'):
496 files = sorted(modified + added + clean)
496 files = sorted(modified + added + clean)
497 kwfiles = kwt.iskwfile(files, wctx)
497 kwfiles = kwt.iskwfile(files, wctx)
498 kwdeleted = kwt.iskwfile(deleted, wctx)
498 kwdeleted = kwt.iskwfile(deleted, wctx)
499 kwunknown = kwt.iskwfile(unknown, wctx)
499 kwunknown = kwt.iskwfile(unknown, wctx)
500 if not opts.get('ignore') or opts.get('all'):
500 if not opts.get('ignore') or opts.get('all'):
501 showfiles = kwfiles, kwdeleted, kwunknown
501 showfiles = kwfiles, kwdeleted, kwunknown
502 else:
502 else:
503 showfiles = [], [], []
503 showfiles = [], [], []
504 if opts.get('all') or opts.get('ignore'):
504 if opts.get('all') or opts.get('ignore'):
505 showfiles += ([f for f in files if f not in kwfiles],
505 showfiles += ([f for f in files if f not in kwfiles],
506 [f for f in unknown if f not in kwunknown])
506 [f for f in unknown if f not in kwunknown])
507 kwlabels = 'enabled deleted enabledunknown ignored ignoredunknown'.split()
507 kwlabels = 'enabled deleted enabledunknown ignored ignoredunknown'.split()
508 kwstates = zip(kwlabels, 'K!kIi', showfiles)
508 kwstates = zip(kwlabels, 'K!kIi', showfiles)
509 fm = ui.formatter('kwfiles', opts)
509 fm = ui.formatter('kwfiles', opts)
510 fmt = '%.0s%s\n'
510 fmt = '%.0s%s\n'
511 if opts.get('all') or ui.verbose:
511 if opts.get('all') or ui.verbose:
512 fmt = '%s %s\n'
512 fmt = '%s %s\n'
513 for kwstate, char, filenames in kwstates:
513 for kwstate, char, filenames in kwstates:
514 label = 'kwfiles.' + kwstate
514 label = 'kwfiles.' + kwstate
515 for f in filenames:
515 for f in filenames:
516 fm.startitem()
516 fm.startitem()
517 fm.write('kwstatus path', fmt, char,
517 fm.write('kwstatus path', fmt, char,
518 repo.pathto(f, cwd), label=label)
518 repo.pathto(f, cwd), label=label)
519 fm.end()
519 fm.end()
520
520
521 @command('kwshrink', commands.walkopts, _('hg kwshrink [OPTION]... [FILE]...'))
521 @command('kwshrink', commands.walkopts, _('hg kwshrink [OPTION]... [FILE]...'))
522 def shrink(ui, repo, *pats, **opts):
522 def shrink(ui, repo, *pats, **opts):
523 '''revert expanded keywords in the working directory
523 '''revert expanded keywords in the working directory
524
524
525 Must be run before changing/disabling active keywords.
525 Must be run before changing/disabling active keywords.
526
526
527 kwshrink refuses to run if given files contain local changes.
527 kwshrink refuses to run if given files contain local changes.
528 '''
528 '''
529 # 3rd argument sets expansion to False
529 # 3rd argument sets expansion to False
530 _kwfwrite(ui, repo, False, *pats, **opts)
530 _kwfwrite(ui, repo, False, *pats, **opts)
531
531
532
532
533 def uisetup(ui):
533 def uisetup(ui):
534 ''' Monkeypatches dispatch._parse to retrieve user command.'''
534 ''' Monkeypatches dispatch._parse to retrieve user command.'''
535
535
536 def kwdispatch_parse(orig, ui, args):
536 def kwdispatch_parse(orig, ui, args):
537 '''Monkeypatch dispatch._parse to obtain running hg command.'''
537 '''Monkeypatch dispatch._parse to obtain running hg command.'''
538 cmd, func, args, options, cmdoptions = orig(ui, args)
538 cmd, func, args, options, cmdoptions = orig(ui, args)
539 kwtools['hgcmd'] = cmd
539 kwtools['hgcmd'] = cmd
540 return cmd, func, args, options, cmdoptions
540 return cmd, func, args, options, cmdoptions
541
541
542 extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse)
542 extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse)
543
543
544 def reposetup(ui, repo):
544 def reposetup(ui, repo):
545 '''Sets up repo as kwrepo for keyword substitution.
545 '''Sets up repo as kwrepo for keyword substitution.
546 Overrides file method to return kwfilelog instead of filelog
546 Overrides file method to return kwfilelog instead of filelog
547 if file matches user configuration.
547 if file matches user configuration.
548 Wraps commit to overwrite configured files with updated
548 Wraps commit to overwrite configured files with updated
549 keyword substitutions.
549 keyword substitutions.
550 Monkeypatches patch and webcommands.'''
550 Monkeypatches patch and webcommands.'''
551
551
552 try:
552 try:
553 if (not repo.local() or kwtools['hgcmd'] in nokwcommands.split()
553 if (not repo.local() or kwtools['hgcmd'] in nokwcommands.split()
554 or '.hg' in util.splitpath(repo.root)
554 or '.hg' in util.splitpath(repo.root)
555 or repo._url.startswith('bundle:')):
555 or repo._url.startswith('bundle:')):
556 return
556 return
557 except AttributeError:
557 except AttributeError:
558 pass
558 pass
559
559
560 inc, exc = [], ['.hg*']
560 inc, exc = [], ['.hg*']
561 for pat, opt in ui.configitems('keyword'):
561 for pat, opt in ui.configitems('keyword'):
562 if opt != 'ignore':
562 if opt != 'ignore':
563 inc.append(pat)
563 inc.append(pat)
564 else:
564 else:
565 exc.append(pat)
565 exc.append(pat)
566 if not inc:
566 if not inc:
567 return
567 return
568
568
569 kwtools['templater'] = kwt = kwtemplater(ui, repo, inc, exc)
569 kwtools['templater'] = kwt = kwtemplater(ui, repo, inc, exc)
570
570
571 class kwrepo(repo.__class__):
571 class kwrepo(repo.__class__):
572 def file(self, f):
572 def file(self, f):
573 if f[0] == '/':
573 if f[0] == '/':
574 f = f[1:]
574 f = f[1:]
575 return kwfilelog(self.sopener, kwt, f)
575 return kwfilelog(self.sopener, kwt, f)
576
576
577 def wread(self, filename):
577 def wread(self, filename):
578 data = super(kwrepo, self).wread(filename)
578 data = super(kwrepo, self).wread(filename)
579 return kwt.wread(filename, data)
579 return kwt.wread(filename, data)
580
580
581 def commit(self, *args, **opts):
581 def commit(self, *args, **opts):
582 # use custom commitctx for user commands
582 # use custom commitctx for user commands
583 # other extensions can still wrap repo.commitctx directly
583 # other extensions can still wrap repo.commitctx directly
584 self.commitctx = self.kwcommitctx
584 self.commitctx = self.kwcommitctx
585 try:
585 try:
586 return super(kwrepo, self).commit(*args, **opts)
586 return super(kwrepo, self).commit(*args, **opts)
587 finally:
587 finally:
588 del self.commitctx
588 del self.commitctx
589
589
590 def kwcommitctx(self, ctx, error=False):
590 def kwcommitctx(self, ctx, error=False):
591 n = super(kwrepo, self).commitctx(ctx, error)
591 n = super(kwrepo, self).commitctx(ctx, error)
592 # no lock needed, only called from repo.commit() which already locks
592 # no lock needed, only called from repo.commit() which already locks
593 if not kwt.postcommit:
593 if not kwt.postcommit:
594 restrict = kwt.restrict
594 restrict = kwt.restrict
595 kwt.restrict = True
595 kwt.restrict = True
596 kwt.overwrite(self[n], sorted(ctx.added() + ctx.modified()),
596 kwt.overwrite(self[n], sorted(ctx.added() + ctx.modified()),
597 False, True)
597 False, True)
598 kwt.restrict = restrict
598 kwt.restrict = restrict
599 return n
599 return n
600
600
601 def rollback(self, dryrun=False, force=False):
601 def rollback(self, dryrun=False, force=False):
602 wlock = self.wlock()
602 wlock = self.wlock()
603 try:
603 try:
604 if not dryrun:
604 if not dryrun:
605 changed = self['.'].files()
605 changed = self['.'].files()
606 ret = super(kwrepo, self).rollback(dryrun, force)
606 ret = super(kwrepo, self).rollback(dryrun, force)
607 if not dryrun:
607 if not dryrun:
608 ctx = self['.']
608 ctx = self['.']
609 modified, added = _preselect(self[None].status(), changed)
609 modified, added = _preselect(self[None].status(), changed)
610 kwt.overwrite(ctx, modified, True, True)
610 kwt.overwrite(ctx, modified, True, True)
611 kwt.overwrite(ctx, added, True, False)
611 kwt.overwrite(ctx, added, True, False)
612 return ret
612 return ret
613 finally:
613 finally:
614 wlock.release()
614 wlock.release()
615
615
616 # monkeypatches
616 # monkeypatches
617 def kwpatchfile_init(orig, self, ui, gp, backend, store, eolmode=None):
617 def kwpatchfile_init(orig, self, ui, gp, backend, store, eolmode=None):
618 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
618 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
619 rejects or conflicts due to expanded keywords in working dir.'''
619 rejects or conflicts due to expanded keywords in working dir.'''
620 orig(self, ui, gp, backend, store, eolmode)
620 orig(self, ui, gp, backend, store, eolmode)
621 # shrink keywords read from working dir
621 # shrink keywords read from working dir
622 self.lines = kwt.shrinklines(self.fname, self.lines)
622 self.lines = kwt.shrinklines(self.fname, self.lines)
623
623
624 def kw_diff(orig, repo, node1=None, node2=None, match=None, changes=None,
624 def kw_diff(orig, repo, node1=None, node2=None, match=None, changes=None,
625 opts=None, prefix=''):
625 opts=None, prefix=''):
626 '''Monkeypatch patch.diff to avoid expansion.'''
626 '''Monkeypatch patch.diff to avoid expansion.'''
627 kwt.restrict = True
627 kwt.restrict = True
628 return orig(repo, node1, node2, match, changes, opts, prefix)
628 return orig(repo, node1, node2, match, changes, opts, prefix)
629
629
630 def kwweb_skip(orig, web, req, tmpl):
630 def kwweb_skip(orig, web, req, tmpl):
631 '''Wraps webcommands.x turning off keyword expansion.'''
631 '''Wraps webcommands.x turning off keyword expansion.'''
632 kwt.match = util.never
632 kwt.match = util.never
633 return orig(web, req, tmpl)
633 return orig(web, req, tmpl)
634
634
635 def kw_amend(orig, ui, repo, commitfunc, old, extra, pats, opts):
635 def kw_amend(orig, ui, repo, commitfunc, old, extra, pats, opts):
636 '''Wraps cmdutil.amend expanding keywords after amend.'''
636 '''Wraps cmdutil.amend expanding keywords after amend.'''
637 wlock = repo.wlock()
637 wlock = repo.wlock()
638 try:
638 try:
639 kwt.postcommit = True
639 kwt.postcommit = True
640 newid = orig(ui, repo, commitfunc, old, extra, pats, opts)
640 newid = orig(ui, repo, commitfunc, old, extra, pats, opts)
641 if newid != old.node():
641 if newid != old.node():
642 ctx = repo[newid]
642 ctx = repo[newid]
643 kwt.restrict = True
643 kwt.restrict = True
644 kwt.overwrite(ctx, ctx.files(), False, True)
644 kwt.overwrite(ctx, ctx.files(), False, True)
645 kwt.restrict = False
645 kwt.restrict = False
646 return newid
646 return newid
647 finally:
647 finally:
648 wlock.release()
648 wlock.release()
649
649
650 def kw_copy(orig, ui, repo, pats, opts, rename=False):
650 def kw_copy(orig, ui, repo, pats, opts, rename=False):
651 '''Wraps cmdutil.copy so that copy/rename destinations do not
651 '''Wraps cmdutil.copy so that copy/rename destinations do not
652 contain expanded keywords.
652 contain expanded keywords.
653 Note that the source of a regular file destination may also be a
653 Note that the source of a regular file destination may also be a
654 symlink:
654 symlink:
655 hg cp sym x -> x is symlink
655 hg cp sym x -> x is symlink
656 cp sym x; hg cp -A sym x -> x is file (maybe expanded keywords)
656 cp sym x; hg cp -A sym x -> x is file (maybe expanded keywords)
657 For the latter we have to follow the symlink to find out whether its
657 For the latter we have to follow the symlink to find out whether its
658 target is configured for expansion and we therefore must unexpand the
658 target is configured for expansion and we therefore must unexpand the
659 keywords in the destination.'''
659 keywords in the destination.'''
660 wlock = repo.wlock()
660 wlock = repo.wlock()
661 try:
661 try:
662 orig(ui, repo, pats, opts, rename)
662 orig(ui, repo, pats, opts, rename)
663 if opts.get('dry_run'):
663 if opts.get('dry_run'):
664 return
664 return
665 wctx = repo[None]
665 wctx = repo[None]
666 cwd = repo.getcwd()
666 cwd = repo.getcwd()
667
667
668 def haskwsource(dest):
668 def haskwsource(dest):
669 '''Returns true if dest is a regular file and configured for
669 '''Returns true if dest is a regular file and configured for
670 expansion or a symlink which points to a file configured for
670 expansion or a symlink which points to a file configured for
671 expansion. '''
671 expansion. '''
672 source = repo.dirstate.copied(dest)
672 source = repo.dirstate.copied(dest)
673 if 'l' in wctx.flags(source):
673 if 'l' in wctx.flags(source):
674 source = scmutil.canonpath(repo.root, cwd,
674 source = scmutil.canonpath(repo.root, cwd,
675 os.path.realpath(source))
675 os.path.realpath(source))
676 return kwt.match(source)
676 return kwt.match(source)
677
677
678 candidates = [f for f in repo.dirstate.copies() if
678 candidates = [f for f in repo.dirstate.copies() if
679 'l' not in wctx.flags(f) and haskwsource(f)]
679 'l' not in wctx.flags(f) and haskwsource(f)]
680 kwt.overwrite(wctx, candidates, False, False)
680 kwt.overwrite(wctx, candidates, False, False)
681 finally:
681 finally:
682 wlock.release()
682 wlock.release()
683
683
684 def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts):
684 def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts):
685 '''Wraps record.dorecord expanding keywords after recording.'''
685 '''Wraps record.dorecord expanding keywords after recording.'''
686 wlock = repo.wlock()
686 wlock = repo.wlock()
687 try:
687 try:
688 # record returns 0 even when nothing has changed
688 # record returns 0 even when nothing has changed
689 # therefore compare nodes before and after
689 # therefore compare nodes before and after
690 kwt.postcommit = True
690 kwt.postcommit = True
691 ctx = repo['.']
691 ctx = repo['.']
692 wstatus = repo[None].status()
692 wstatus = repo[None].status()
693 ret = orig(ui, repo, commitfunc, *pats, **opts)
693 ret = orig(ui, repo, commitfunc, *pats, **opts)
694 recctx = repo['.']
694 recctx = repo['.']
695 if ctx != recctx:
695 if ctx != recctx:
696 modified, added = _preselect(wstatus, recctx.files())
696 modified, added = _preselect(wstatus, recctx.files())
697 kwt.restrict = False
697 kwt.restrict = False
698 kwt.overwrite(recctx, modified, False, True)
698 kwt.overwrite(recctx, modified, False, True)
699 kwt.overwrite(recctx, added, False, True, True)
699 kwt.overwrite(recctx, added, False, True, True)
700 kwt.restrict = True
700 kwt.restrict = True
701 return ret
701 return ret
702 finally:
702 finally:
703 wlock.release()
703 wlock.release()
704
704
705 def kwfilectx_cmp(orig, self, fctx):
705 def kwfilectx_cmp(orig, self, fctx):
706 # keyword affects data size, comparing wdir and filelog size does
706 # keyword affects data size, comparing wdir and filelog size does
707 # not make sense
707 # not make sense
708 if (fctx._filerev is None and
708 if (fctx._filerev is None and
709 (self._repo._encodefilterpats or
709 (self._repo._encodefilterpats or
710 kwt.match(fctx.path()) and 'l' not in fctx.flags() or
710 kwt.match(fctx.path()) and 'l' not in fctx.flags() or
711 self.size() - 4 == fctx.size()) or
711 self.size() - 4 == fctx.size()) or
712 self.size() == fctx.size()):
712 self.size() == fctx.size()):
713 return self._filelog.cmp(self._filenode, fctx.data())
713 return self._filelog.cmp(self._filenode, fctx.data())
714 return True
714 return True
715
715
716 extensions.wrapfunction(context.filectx, 'cmp', kwfilectx_cmp)
716 extensions.wrapfunction(context.filectx, 'cmp', kwfilectx_cmp)
717 extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init)
717 extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init)
718 extensions.wrapfunction(patch, 'diff', kw_diff)
718 extensions.wrapfunction(patch, 'diff', kw_diff)
719 extensions.wrapfunction(cmdutil, 'amend', kw_amend)
719 extensions.wrapfunction(cmdutil, 'amend', kw_amend)
720 extensions.wrapfunction(cmdutil, 'copy', kw_copy)
720 extensions.wrapfunction(cmdutil, 'copy', kw_copy)
721 for c in 'annotate changeset rev filediff diff'.split():
721 for c in 'annotate changeset rev filediff diff'.split():
722 extensions.wrapfunction(webcommands, c, kwweb_skip)
722 extensions.wrapfunction(webcommands, c, kwweb_skip)
723 for name in recordextensions.split():
723 for name in recordextensions.split():
724 try:
724 try:
725 record = extensions.find(name)
725 record = extensions.find(name)
726 extensions.wrapfunction(record, 'dorecord', kw_dorecord)
726 extensions.wrapfunction(record, 'dorecord', kw_dorecord)
727 except KeyError:
727 except KeyError:
728 pass
728 pass
729
729
730 repo.__class__ = kwrepo
730 repo.__class__ = kwrepo
@@ -1,424 +1,424 b''
1 # template-filters.py - common template expansion filters
1 # template-filters.py - common template expansion filters
2 #
2 #
3 # Copyright 2005-2008 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2008 Matt Mackall <mpm@selenic.com>
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 import cgi, re, os, time, urllib
8 import cgi, re, os, time, urllib
9 import encoding, node, util, error
9 import encoding, node, util, error
10 import hbisect
10 import hbisect
11
11
12 def addbreaks(text):
12 def addbreaks(text):
13 """:addbreaks: Any text. Add an XHTML "<br />" tag before the end of
13 """:addbreaks: Any text. Add an XHTML "<br />" tag before the end of
14 every line except the last.
14 every line except the last.
15 """
15 """
16 return text.replace('\n', '<br/>\n')
16 return text.replace('\n', '<br/>\n')
17
17
18 agescales = [("year", 3600 * 24 * 365),
18 agescales = [("year", 3600 * 24 * 365),
19 ("month", 3600 * 24 * 30),
19 ("month", 3600 * 24 * 30),
20 ("week", 3600 * 24 * 7),
20 ("week", 3600 * 24 * 7),
21 ("day", 3600 * 24),
21 ("day", 3600 * 24),
22 ("hour", 3600),
22 ("hour", 3600),
23 ("minute", 60),
23 ("minute", 60),
24 ("second", 1)]
24 ("second", 1)]
25
25
26 def age(date):
26 def age(date):
27 """:age: Date. Returns a human-readable date/time difference between the
27 """:age: Date. Returns a human-readable date/time difference between the
28 given date/time and the current date/time.
28 given date/time and the current date/time.
29 """
29 """
30
30
31 def plural(t, c):
31 def plural(t, c):
32 if c == 1:
32 if c == 1:
33 return t
33 return t
34 return t + "s"
34 return t + "s"
35 def fmt(t, c):
35 def fmt(t, c):
36 return "%d %s" % (c, plural(t, c))
36 return "%d %s" % (c, plural(t, c))
37
37
38 now = time.time()
38 now = time.time()
39 then = date[0]
39 then = date[0]
40 future = False
40 future = False
41 if then > now:
41 if then > now:
42 future = True
42 future = True
43 delta = max(1, int(then - now))
43 delta = max(1, int(then - now))
44 if delta > agescales[0][1] * 30:
44 if delta > agescales[0][1] * 30:
45 return 'in the distant future'
45 return 'in the distant future'
46 else:
46 else:
47 delta = max(1, int(now - then))
47 delta = max(1, int(now - then))
48 if delta > agescales[0][1] * 2:
48 if delta > agescales[0][1] * 2:
49 return util.shortdate(date)
49 return util.shortdate(date)
50
50
51 for t, s in agescales:
51 for t, s in agescales:
52 n = delta // s
52 n = delta // s
53 if n >= 2 or s == 1:
53 if n >= 2 or s == 1:
54 if future:
54 if future:
55 return '%s from now' % fmt(t, n)
55 return '%s from now' % fmt(t, n)
56 return '%s ago' % fmt(t, n)
56 return '%s ago' % fmt(t, n)
57
57
58 def basename(path):
58 def basename(path):
59 """:basename: Any text. Treats the text as a path, and returns the last
59 """:basename: Any text. Treats the text as a path, and returns the last
60 component of the path after splitting by the path separator
60 component of the path after splitting by the path separator
61 (ignoring trailing separators). For example, "foo/bar/baz" becomes
61 (ignoring trailing separators). For example, "foo/bar/baz" becomes
62 "baz" and "foo/bar//" becomes "bar".
62 "baz" and "foo/bar//" becomes "bar".
63 """
63 """
64 return os.path.basename(path)
64 return os.path.basename(path)
65
65
66 def datefilter(text):
66 def datefilter(text):
67 """:date: Date. Returns a date in a Unix date format, including the
67 """:date: Date. Returns a date in a Unix date format, including the
68 timezone: "Mon Sep 04 15:13:13 2006 0700".
68 timezone: "Mon Sep 04 15:13:13 2006 0700".
69 """
69 """
70 return util.datestr(text)
70 return util.datestr(text)
71
71
72 def domain(author):
72 def domain(author):
73 """:domain: Any text. Finds the first string that looks like an email
73 """:domain: Any text. Finds the first string that looks like an email
74 address, and extracts just the domain component. Example: ``User
74 address, and extracts just the domain component. Example: ``User
75 <user@example.com>`` becomes ``example.com``.
75 <user@example.com>`` becomes ``example.com``.
76 """
76 """
77 f = author.find('@')
77 f = author.find('@')
78 if f == -1:
78 if f == -1:
79 return ''
79 return ''
80 author = author[f + 1:]
80 author = author[f + 1:]
81 f = author.find('>')
81 f = author.find('>')
82 if f >= 0:
82 if f >= 0:
83 author = author[:f]
83 author = author[:f]
84 return author
84 return author
85
85
86 def email(text):
86 def email(text):
87 """:email: Any text. Extracts the first string that looks like an email
87 """:email: Any text. Extracts the first string that looks like an email
88 address. Example: ``User <user@example.com>`` becomes
88 address. Example: ``User <user@example.com>`` becomes
89 ``user@example.com``.
89 ``user@example.com``.
90 """
90 """
91 return util.email(text)
91 return util.email(text)
92
92
93 def escape(text):
93 def escape(text):
94 """:escape: Any text. Replaces the special XML/XHTML characters "&", "<"
94 """:escape: Any text. Replaces the special XML/XHTML characters "&", "<"
95 and ">" with XML entities.
95 and ">" with XML entities.
96 """
96 """
97 return cgi.escape(text, True)
97 return cgi.escape(text, True)
98
98
99 para_re = None
99 para_re = None
100 space_re = None
100 space_re = None
101
101
102 def fill(text, width):
102 def fill(text, width):
103 '''fill many paragraphs.'''
103 '''fill many paragraphs.'''
104 global para_re, space_re
104 global para_re, space_re
105 if para_re is None:
105 if para_re is None:
106 para_re = re.compile('(\n\n|\n\\s*[-*]\\s*)', re.M)
106 para_re = re.compile('(\n\n|\n\\s*[-*]\\s*)', re.M)
107 space_re = re.compile(r' +')
107 space_re = re.compile(r' +')
108
108
109 def findparas():
109 def findparas():
110 start = 0
110 start = 0
111 while True:
111 while True:
112 m = para_re.search(text, start)
112 m = para_re.search(text, start)
113 if not m:
113 if not m:
114 uctext = unicode(text[start:], encoding.encoding)
114 uctext = unicode(text[start:], encoding.encoding)
115 w = len(uctext)
115 w = len(uctext)
116 while 0 < w and uctext[w - 1].isspace():
116 while 0 < w and uctext[w - 1].isspace():
117 w -= 1
117 w -= 1
118 yield (uctext[:w].encode(encoding.encoding),
118 yield (uctext[:w].encode(encoding.encoding),
119 uctext[w:].encode(encoding.encoding))
119 uctext[w:].encode(encoding.encoding))
120 break
120 break
121 yield text[start:m.start(0)], m.group(1)
121 yield text[start:m.start(0)], m.group(1)
122 start = m.end(1)
122 start = m.end(1)
123
123
124 return "".join([space_re.sub(' ', util.wrap(para, width=width)) + rest
124 return "".join([space_re.sub(' ', util.wrap(para, width=width)) + rest
125 for para, rest in findparas()])
125 for para, rest in findparas()])
126
126
127 def fill68(text):
127 def fill68(text):
128 """:fill68: Any text. Wraps the text to fit in 68 columns."""
128 """:fill68: Any text. Wraps the text to fit in 68 columns."""
129 return fill(text, 68)
129 return fill(text, 68)
130
130
131 def fill76(text):
131 def fill76(text):
132 """:fill76: Any text. Wraps the text to fit in 76 columns."""
132 """:fill76: Any text. Wraps the text to fit in 76 columns."""
133 return fill(text, 76)
133 return fill(text, 76)
134
134
135 def firstline(text):
135 def firstline(text):
136 """:firstline: Any text. Returns the first line of text."""
136 """:firstline: Any text. Returns the first line of text."""
137 try:
137 try:
138 return text.splitlines(True)[0].rstrip('\r\n')
138 return text.splitlines(True)[0].rstrip('\r\n')
139 except IndexError:
139 except IndexError:
140 return ''
140 return ''
141
141
142 def hexfilter(text):
142 def hexfilter(text):
143 """:hex: Any text. Convert a binary Mercurial node identifier into
143 """:hex: Any text. Convert a binary Mercurial node identifier into
144 its long hexadecimal representation.
144 its long hexadecimal representation.
145 """
145 """
146 return node.hex(text)
146 return node.hex(text)
147
147
148 def hgdate(text):
148 def hgdate(text):
149 """:hgdate: Date. Returns the date as a pair of numbers: "1157407993
149 """:hgdate: Date. Returns the date as a pair of numbers: "1157407993
150 25200" (Unix timestamp, timezone offset).
150 25200" (Unix timestamp, timezone offset).
151 """
151 """
152 return "%d %d" % text
152 return "%d %d" % text
153
153
154 def isodate(text):
154 def isodate(text):
155 """:isodate: Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
155 """:isodate: Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
156 +0200".
156 +0200".
157 """
157 """
158 return util.datestr(text, '%Y-%m-%d %H:%M %1%2')
158 return util.datestr(text, '%Y-%m-%d %H:%M %1%2')
159
159
160 def isodatesec(text):
160 def isodatesec(text):
161 """:isodatesec: Date. Returns the date in ISO 8601 format, including
161 """:isodatesec: Date. Returns the date in ISO 8601 format, including
162 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
162 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
163 filter.
163 filter.
164 """
164 """
165 return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2')
165 return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2')
166
166
167 def indent(text, prefix):
167 def indent(text, prefix):
168 '''indent each non-empty line of text after first with prefix.'''
168 '''indent each non-empty line of text after first with prefix.'''
169 lines = text.splitlines()
169 lines = text.splitlines()
170 num_lines = len(lines)
170 num_lines = len(lines)
171 endswithnewline = text[-1:] == '\n'
171 endswithnewline = text[-1:] == '\n'
172 def indenter():
172 def indenter():
173 for i in xrange(num_lines):
173 for i in xrange(num_lines):
174 l = lines[i]
174 l = lines[i]
175 if i and l.strip():
175 if i and l.strip():
176 yield prefix
176 yield prefix
177 yield l
177 yield l
178 if i < num_lines - 1 or endswithnewline:
178 if i < num_lines - 1 or endswithnewline:
179 yield '\n'
179 yield '\n'
180 return "".join(indenter())
180 return "".join(indenter())
181
181
182 def json(obj):
182 def json(obj):
183 if obj is None or obj is False or obj is True:
183 if obj is None or obj is False or obj is True:
184 return {None: 'null', False: 'false', True: 'true'}[obj]
184 return {None: 'null', False: 'false', True: 'true'}[obj]
185 elif isinstance(obj, int) or isinstance(obj, float):
185 elif isinstance(obj, int) or isinstance(obj, float):
186 return str(obj)
186 return str(obj)
187 elif isinstance(obj, str):
187 elif isinstance(obj, str):
188 u = unicode(obj, encoding.encoding, 'replace')
188 u = unicode(obj, encoding.encoding, 'replace')
189 return '"%s"' % jsonescape(u)
189 return '"%s"' % jsonescape(u)
190 elif isinstance(obj, unicode):
190 elif isinstance(obj, unicode):
191 return '"%s"' % jsonescape(obj)
191 return '"%s"' % jsonescape(obj)
192 elif util.safehasattr(obj, 'keys'):
192 elif util.safehasattr(obj, 'keys'):
193 out = []
193 out = []
194 for k, v in obj.iteritems():
194 for k, v in obj.iteritems():
195 s = '%s: %s' % (json(k), json(v))
195 s = '%s: %s' % (json(k), json(v))
196 out.append(s)
196 out.append(s)
197 return '{' + ', '.join(out) + '}'
197 return '{' + ', '.join(out) + '}'
198 elif util.safehasattr(obj, '__iter__'):
198 elif util.safehasattr(obj, '__iter__'):
199 out = []
199 out = []
200 for i in obj:
200 for i in obj:
201 out.append(json(i))
201 out.append(json(i))
202 return '[' + ', '.join(out) + ']'
202 return '[' + ', '.join(out) + ']'
203 else:
203 else:
204 raise TypeError('cannot encode type %s' % obj.__class__.__name__)
204 raise TypeError('cannot encode type %s' % obj.__class__.__name__)
205
205
206 def _uescape(c):
206 def _uescape(c):
207 if ord(c) < 0x80:
207 if ord(c) < 0x80:
208 return c
208 return c
209 else:
209 else:
210 return '\\u%04x' % ord(c)
210 return '\\u%04x' % ord(c)
211
211
212 _escapes = [
212 _escapes = [
213 ('\\', '\\\\'), ('"', '\\"'), ('\t', '\\t'), ('\n', '\\n'),
213 ('\\', '\\\\'), ('"', '\\"'), ('\t', '\\t'), ('\n', '\\n'),
214 ('\r', '\\r'), ('\f', '\\f'), ('\b', '\\b'),
214 ('\r', '\\r'), ('\f', '\\f'), ('\b', '\\b'),
215 ]
215 ]
216
216
217 def jsonescape(s):
217 def jsonescape(s):
218 for k, v in _escapes:
218 for k, v in _escapes:
219 s = s.replace(k, v)
219 s = s.replace(k, v)
220 return ''.join(_uescape(c) for c in s)
220 return ''.join(_uescape(c) for c in s)
221
221
222 def localdate(text):
222 def localdate(text):
223 """:localdate: Date. Converts a date to local date."""
223 """:localdate: Date. Converts a date to local date."""
224 return (text[0], util.makedate()[1])
224 return (util.parsedate(text)[0], util.makedate()[1])
225
225
226 def nonempty(str):
226 def nonempty(str):
227 """:nonempty: Any text. Returns '(none)' if the string is empty."""
227 """:nonempty: Any text. Returns '(none)' if the string is empty."""
228 return str or "(none)"
228 return str or "(none)"
229
229
230 def obfuscate(text):
230 def obfuscate(text):
231 """:obfuscate: Any text. Returns the input text rendered as a sequence of
231 """:obfuscate: Any text. Returns the input text rendered as a sequence of
232 XML entities.
232 XML entities.
233 """
233 """
234 text = unicode(text, encoding.encoding, 'replace')
234 text = unicode(text, encoding.encoding, 'replace')
235 return ''.join(['&#%d;' % ord(c) for c in text])
235 return ''.join(['&#%d;' % ord(c) for c in text])
236
236
237 def permissions(flags):
237 def permissions(flags):
238 if "l" in flags:
238 if "l" in flags:
239 return "lrwxrwxrwx"
239 return "lrwxrwxrwx"
240 if "x" in flags:
240 if "x" in flags:
241 return "-rwxr-xr-x"
241 return "-rwxr-xr-x"
242 return "-rw-r--r--"
242 return "-rw-r--r--"
243
243
244 def person(author):
244 def person(author):
245 """:person: Any text. Returns the name before an email address,
245 """:person: Any text. Returns the name before an email address,
246 interpreting it as per RFC 5322.
246 interpreting it as per RFC 5322.
247
247
248 >>> person('foo@bar')
248 >>> person('foo@bar')
249 'foo'
249 'foo'
250 >>> person('Foo Bar <foo@bar>')
250 >>> person('Foo Bar <foo@bar>')
251 'Foo Bar'
251 'Foo Bar'
252 >>> person('"Foo Bar" <foo@bar>')
252 >>> person('"Foo Bar" <foo@bar>')
253 'Foo Bar'
253 'Foo Bar'
254 >>> person('"Foo \"buz\" Bar" <foo@bar>')
254 >>> person('"Foo \"buz\" Bar" <foo@bar>')
255 'Foo "buz" Bar'
255 'Foo "buz" Bar'
256 >>> # The following are invalid, but do exist in real-life
256 >>> # The following are invalid, but do exist in real-life
257 ...
257 ...
258 >>> person('Foo "buz" Bar <foo@bar>')
258 >>> person('Foo "buz" Bar <foo@bar>')
259 'Foo "buz" Bar'
259 'Foo "buz" Bar'
260 >>> person('"Foo Bar <foo@bar>')
260 >>> person('"Foo Bar <foo@bar>')
261 'Foo Bar'
261 'Foo Bar'
262 """
262 """
263 if '@' not in author:
263 if '@' not in author:
264 return author
264 return author
265 f = author.find('<')
265 f = author.find('<')
266 if f != -1:
266 if f != -1:
267 return author[:f].strip(' "').replace('\\"', '"')
267 return author[:f].strip(' "').replace('\\"', '"')
268 f = author.find('@')
268 f = author.find('@')
269 return author[:f].replace('.', ' ')
269 return author[:f].replace('.', ' ')
270
270
271 def rfc3339date(text):
271 def rfc3339date(text):
272 """:rfc3339date: Date. Returns a date using the Internet date format
272 """:rfc3339date: Date. Returns a date using the Internet date format
273 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
273 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
274 """
274 """
275 return util.datestr(text, "%Y-%m-%dT%H:%M:%S%1:%2")
275 return util.datestr(text, "%Y-%m-%dT%H:%M:%S%1:%2")
276
276
277 def rfc822date(text):
277 def rfc822date(text):
278 """:rfc822date: Date. Returns a date using the same format used in email
278 """:rfc822date: Date. Returns a date using the same format used in email
279 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
279 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
280 """
280 """
281 return util.datestr(text, "%a, %d %b %Y %H:%M:%S %1%2")
281 return util.datestr(text, "%a, %d %b %Y %H:%M:%S %1%2")
282
282
283 def short(text):
283 def short(text):
284 """:short: Changeset hash. Returns the short form of a changeset hash,
284 """:short: Changeset hash. Returns the short form of a changeset hash,
285 i.e. a 12 hexadecimal digit string.
285 i.e. a 12 hexadecimal digit string.
286 """
286 """
287 return text[:12]
287 return text[:12]
288
288
289 def shortbisect(text):
289 def shortbisect(text):
290 """:shortbisect: Any text. Treats `text` as a bisection status, and
290 """:shortbisect: Any text. Treats `text` as a bisection status, and
291 returns a single-character representing the status (G: good, B: bad,
291 returns a single-character representing the status (G: good, B: bad,
292 S: skipped, U: untested, I: ignored). Returns single space if `text`
292 S: skipped, U: untested, I: ignored). Returns single space if `text`
293 is not a valid bisection status.
293 is not a valid bisection status.
294 """
294 """
295 return hbisect.shortlabel(text) or ' '
295 return hbisect.shortlabel(text) or ' '
296
296
297 def shortdate(text):
297 def shortdate(text):
298 """:shortdate: Date. Returns a date like "2006-09-18"."""
298 """:shortdate: Date. Returns a date like "2006-09-18"."""
299 return util.shortdate(text)
299 return util.shortdate(text)
300
300
301 def stringescape(text):
301 def stringescape(text):
302 return text.encode('string_escape')
302 return text.encode('string_escape')
303
303
304 def stringify(thing):
304 def stringify(thing):
305 """:stringify: Any type. Turns the value into text by converting values into
305 """:stringify: Any type. Turns the value into text by converting values into
306 text and concatenating them.
306 text and concatenating them.
307 """
307 """
308 if util.safehasattr(thing, '__iter__') and not isinstance(thing, str):
308 if util.safehasattr(thing, '__iter__') and not isinstance(thing, str):
309 return "".join([stringify(t) for t in thing if t is not None])
309 return "".join([stringify(t) for t in thing if t is not None])
310 return str(thing)
310 return str(thing)
311
311
312 def strip(text):
312 def strip(text):
313 """:strip: Any text. Strips all leading and trailing whitespace."""
313 """:strip: Any text. Strips all leading and trailing whitespace."""
314 return text.strip()
314 return text.strip()
315
315
316 def stripdir(text):
316 def stripdir(text):
317 """:stripdir: Treat the text as path and strip a directory level, if
317 """:stripdir: Treat the text as path and strip a directory level, if
318 possible. For example, "foo" and "foo/bar" becomes "foo".
318 possible. For example, "foo" and "foo/bar" becomes "foo".
319 """
319 """
320 dir = os.path.dirname(text)
320 dir = os.path.dirname(text)
321 if dir == "":
321 if dir == "":
322 return os.path.basename(text)
322 return os.path.basename(text)
323 else:
323 else:
324 return dir
324 return dir
325
325
326 def tabindent(text):
326 def tabindent(text):
327 """:tabindent: Any text. Returns the text, with every line except the
327 """:tabindent: Any text. Returns the text, with every line except the
328 first starting with a tab character.
328 first starting with a tab character.
329 """
329 """
330 return indent(text, '\t')
330 return indent(text, '\t')
331
331
332 def urlescape(text):
332 def urlescape(text):
333 """:urlescape: Any text. Escapes all "special" characters. For example,
333 """:urlescape: Any text. Escapes all "special" characters. For example,
334 "foo bar" becomes "foo%20bar".
334 "foo bar" becomes "foo%20bar".
335 """
335 """
336 return urllib.quote(text)
336 return urllib.quote(text)
337
337
338 def userfilter(text):
338 def userfilter(text):
339 """:user: Any text. Returns a short representation of a user name or email
339 """:user: Any text. Returns a short representation of a user name or email
340 address."""
340 address."""
341 return util.shortuser(text)
341 return util.shortuser(text)
342
342
343 def emailuser(text):
343 def emailuser(text):
344 """:emailuser: Any text. Returns the user portion of an email address."""
344 """:emailuser: Any text. Returns the user portion of an email address."""
345 return util.emailuser(text)
345 return util.emailuser(text)
346
346
347 def xmlescape(text):
347 def xmlescape(text):
348 text = (text
348 text = (text
349 .replace('&', '&amp;')
349 .replace('&', '&amp;')
350 .replace('<', '&lt;')
350 .replace('<', '&lt;')
351 .replace('>', '&gt;')
351 .replace('>', '&gt;')
352 .replace('"', '&quot;')
352 .replace('"', '&quot;')
353 .replace("'", '&#39;')) # &apos; invalid in HTML
353 .replace("'", '&#39;')) # &apos; invalid in HTML
354 return re.sub('[\x00-\x08\x0B\x0C\x0E-\x1F]', ' ', text)
354 return re.sub('[\x00-\x08\x0B\x0C\x0E-\x1F]', ' ', text)
355
355
356 filters = {
356 filters = {
357 "addbreaks": addbreaks,
357 "addbreaks": addbreaks,
358 "age": age,
358 "age": age,
359 "basename": basename,
359 "basename": basename,
360 "date": datefilter,
360 "date": datefilter,
361 "domain": domain,
361 "domain": domain,
362 "email": email,
362 "email": email,
363 "escape": escape,
363 "escape": escape,
364 "fill68": fill68,
364 "fill68": fill68,
365 "fill76": fill76,
365 "fill76": fill76,
366 "firstline": firstline,
366 "firstline": firstline,
367 "hex": hexfilter,
367 "hex": hexfilter,
368 "hgdate": hgdate,
368 "hgdate": hgdate,
369 "isodate": isodate,
369 "isodate": isodate,
370 "isodatesec": isodatesec,
370 "isodatesec": isodatesec,
371 "json": json,
371 "json": json,
372 "jsonescape": jsonescape,
372 "jsonescape": jsonescape,
373 "localdate": localdate,
373 "localdate": localdate,
374 "nonempty": nonempty,
374 "nonempty": nonempty,
375 "obfuscate": obfuscate,
375 "obfuscate": obfuscate,
376 "permissions": permissions,
376 "permissions": permissions,
377 "person": person,
377 "person": person,
378 "rfc3339date": rfc3339date,
378 "rfc3339date": rfc3339date,
379 "rfc822date": rfc822date,
379 "rfc822date": rfc822date,
380 "short": short,
380 "short": short,
381 "shortbisect": shortbisect,
381 "shortbisect": shortbisect,
382 "shortdate": shortdate,
382 "shortdate": shortdate,
383 "stringescape": stringescape,
383 "stringescape": stringescape,
384 "stringify": stringify,
384 "stringify": stringify,
385 "strip": strip,
385 "strip": strip,
386 "stripdir": stripdir,
386 "stripdir": stripdir,
387 "tabindent": tabindent,
387 "tabindent": tabindent,
388 "urlescape": urlescape,
388 "urlescape": urlescape,
389 "user": userfilter,
389 "user": userfilter,
390 "emailuser": emailuser,
390 "emailuser": emailuser,
391 "xmlescape": xmlescape,
391 "xmlescape": xmlescape,
392 }
392 }
393
393
394 def fillfunc(context, mapping, args):
394 def fillfunc(context, mapping, args):
395 if not (1 <= len(args) <= 2):
395 if not (1 <= len(args) <= 2):
396 raise error.ParseError(_("fill expects one or two arguments"))
396 raise error.ParseError(_("fill expects one or two arguments"))
397
397
398 text = stringify(args[0][0](context, mapping, args[0][1]))
398 text = stringify(args[0][0](context, mapping, args[0][1]))
399 width = 76
399 width = 76
400 if len(args) == 2:
400 if len(args) == 2:
401 try:
401 try:
402 width = int(stringify(args[1][0](context, mapping, args[1][1])))
402 width = int(stringify(args[1][0](context, mapping, args[1][1])))
403 except ValueError:
403 except ValueError:
404 raise error.ParseError(_("fill expects an integer width"))
404 raise error.ParseError(_("fill expects an integer width"))
405
405
406 return fill(text, width)
406 return fill(text, width)
407
407
408 def datefunc(context, mapping, args):
408 def datefunc(context, mapping, args):
409 if not (1 <= len(args) <= 2):
409 if not (1 <= len(args) <= 2):
410 raise error.ParseError(_("date expects one or two arguments"))
410 raise error.ParseError(_("date expects one or two arguments"))
411
411
412 date = args[0][0](context, mapping, args[0][1])
412 date = args[0][0](context, mapping, args[0][1])
413 if len(args) == 2:
413 if len(args) == 2:
414 fmt = stringify(args[1][0](context, mapping, args[1][1]))
414 fmt = stringify(args[1][0](context, mapping, args[1][1]))
415 return util.datestr(date, fmt)
415 return util.datestr(date, fmt)
416 return util.datestr(date)
416 return util.datestr(date)
417
417
418 funcs = {
418 funcs = {
419 "fill": fillfunc,
419 "fill": fillfunc,
420 "date": datefunc,
420 "date": datefunc,
421 }
421 }
422
422
423 # tell hggettext to extract docstrings from these functions:
423 # tell hggettext to extract docstrings from these functions:
424 i18nfunctions = filters.values()
424 i18nfunctions = filters.values()
General Comments 0
You need to be logged in to leave comments. Login now