##// END OF EJS Templates
templatefilters: inline hbisect.shortlabel()...
Yuya Nishihara -
r36848:71f18994 default
parent child Browse files
Show More
@@ -1,300 +1,294
1 # changelog bisection for mercurial
1 # changelog bisection for mercurial
2 #
2 #
3 # Copyright 2007 Matt Mackall
3 # Copyright 2007 Matt Mackall
4 # Copyright 2005, 2006 Benoit Boissinot <benoit.boissinot@ens-lyon.org>
4 # Copyright 2005, 2006 Benoit Boissinot <benoit.boissinot@ens-lyon.org>
5 #
5 #
6 # Inspired by git bisect, extension skeleton taken from mq.py.
6 # Inspired by git bisect, extension skeleton taken from mq.py.
7 #
7 #
8 # This software may be used and distributed according to the terms of the
8 # This software may be used and distributed according to the terms of the
9 # GNU General Public License version 2 or any later version.
9 # GNU General Public License version 2 or any later version.
10
10
11 from __future__ import absolute_import
11 from __future__ import absolute_import
12
12
13 import collections
13 import collections
14
14
15 from .i18n import _
15 from .i18n import _
16 from .node import (
16 from .node import (
17 hex,
17 hex,
18 short,
18 short,
19 )
19 )
20 from . import (
20 from . import (
21 error,
21 error,
22 )
22 )
23
23
24 def bisect(repo, state):
24 def bisect(repo, state):
25 """find the next node (if any) for testing during a bisect search.
25 """find the next node (if any) for testing during a bisect search.
26 returns a (nodes, number, good) tuple.
26 returns a (nodes, number, good) tuple.
27
27
28 'nodes' is the final result of the bisect if 'number' is 0.
28 'nodes' is the final result of the bisect if 'number' is 0.
29 Otherwise 'number' indicates the remaining possible candidates for
29 Otherwise 'number' indicates the remaining possible candidates for
30 the search and 'nodes' contains the next bisect target.
30 the search and 'nodes' contains the next bisect target.
31 'good' is True if bisect is searching for a first good changeset, False
31 'good' is True if bisect is searching for a first good changeset, False
32 if searching for a first bad one.
32 if searching for a first bad one.
33 """
33 """
34
34
35 changelog = repo.changelog
35 changelog = repo.changelog
36 clparents = changelog.parentrevs
36 clparents = changelog.parentrevs
37 skip = set([changelog.rev(n) for n in state['skip']])
37 skip = set([changelog.rev(n) for n in state['skip']])
38
38
39 def buildancestors(bad, good):
39 def buildancestors(bad, good):
40 badrev = min([changelog.rev(n) for n in bad])
40 badrev = min([changelog.rev(n) for n in bad])
41 ancestors = collections.defaultdict(lambda: None)
41 ancestors = collections.defaultdict(lambda: None)
42 for rev in repo.revs("descendants(%ln) - ancestors(%ln)", good, good):
42 for rev in repo.revs("descendants(%ln) - ancestors(%ln)", good, good):
43 ancestors[rev] = []
43 ancestors[rev] = []
44 if ancestors[badrev] is None:
44 if ancestors[badrev] is None:
45 return badrev, None
45 return badrev, None
46 return badrev, ancestors
46 return badrev, ancestors
47
47
48 good = False
48 good = False
49 badrev, ancestors = buildancestors(state['bad'], state['good'])
49 badrev, ancestors = buildancestors(state['bad'], state['good'])
50 if not ancestors: # looking for bad to good transition?
50 if not ancestors: # looking for bad to good transition?
51 good = True
51 good = True
52 badrev, ancestors = buildancestors(state['good'], state['bad'])
52 badrev, ancestors = buildancestors(state['good'], state['bad'])
53 bad = changelog.node(badrev)
53 bad = changelog.node(badrev)
54 if not ancestors: # now we're confused
54 if not ancestors: # now we're confused
55 if (len(state['bad']) == 1 and len(state['good']) == 1 and
55 if (len(state['bad']) == 1 and len(state['good']) == 1 and
56 state['bad'] != state['good']):
56 state['bad'] != state['good']):
57 raise error.Abort(_("starting revisions are not directly related"))
57 raise error.Abort(_("starting revisions are not directly related"))
58 raise error.Abort(_("inconsistent state, %d:%s is good and bad")
58 raise error.Abort(_("inconsistent state, %d:%s is good and bad")
59 % (badrev, short(bad)))
59 % (badrev, short(bad)))
60
60
61 # build children dict
61 # build children dict
62 children = {}
62 children = {}
63 visit = collections.deque([badrev])
63 visit = collections.deque([badrev])
64 candidates = []
64 candidates = []
65 while visit:
65 while visit:
66 rev = visit.popleft()
66 rev = visit.popleft()
67 if ancestors[rev] == []:
67 if ancestors[rev] == []:
68 candidates.append(rev)
68 candidates.append(rev)
69 for prev in clparents(rev):
69 for prev in clparents(rev):
70 if prev != -1:
70 if prev != -1:
71 if prev in children:
71 if prev in children:
72 children[prev].append(rev)
72 children[prev].append(rev)
73 else:
73 else:
74 children[prev] = [rev]
74 children[prev] = [rev]
75 visit.append(prev)
75 visit.append(prev)
76
76
77 candidates.sort()
77 candidates.sort()
78 # have we narrowed it down to one entry?
78 # have we narrowed it down to one entry?
79 # or have all other possible candidates besides 'bad' have been skipped?
79 # or have all other possible candidates besides 'bad' have been skipped?
80 tot = len(candidates)
80 tot = len(candidates)
81 unskipped = [c for c in candidates if (c not in skip) and (c != badrev)]
81 unskipped = [c for c in candidates if (c not in skip) and (c != badrev)]
82 if tot == 1 or not unskipped:
82 if tot == 1 or not unskipped:
83 return ([changelog.node(c) for c in candidates], 0, good)
83 return ([changelog.node(c) for c in candidates], 0, good)
84 perfect = tot // 2
84 perfect = tot // 2
85
85
86 # find the best node to test
86 # find the best node to test
87 best_rev = None
87 best_rev = None
88 best_len = -1
88 best_len = -1
89 poison = set()
89 poison = set()
90 for rev in candidates:
90 for rev in candidates:
91 if rev in poison:
91 if rev in poison:
92 # poison children
92 # poison children
93 poison.update(children.get(rev, []))
93 poison.update(children.get(rev, []))
94 continue
94 continue
95
95
96 a = ancestors[rev] or [rev]
96 a = ancestors[rev] or [rev]
97 ancestors[rev] = None
97 ancestors[rev] = None
98
98
99 x = len(a) # number of ancestors
99 x = len(a) # number of ancestors
100 y = tot - x # number of non-ancestors
100 y = tot - x # number of non-ancestors
101 value = min(x, y) # how good is this test?
101 value = min(x, y) # how good is this test?
102 if value > best_len and rev not in skip:
102 if value > best_len and rev not in skip:
103 best_len = value
103 best_len = value
104 best_rev = rev
104 best_rev = rev
105 if value == perfect: # found a perfect candidate? quit early
105 if value == perfect: # found a perfect candidate? quit early
106 break
106 break
107
107
108 if y < perfect and rev not in skip: # all downhill from here?
108 if y < perfect and rev not in skip: # all downhill from here?
109 # poison children
109 # poison children
110 poison.update(children.get(rev, []))
110 poison.update(children.get(rev, []))
111 continue
111 continue
112
112
113 for c in children.get(rev, []):
113 for c in children.get(rev, []):
114 if ancestors[c]:
114 if ancestors[c]:
115 ancestors[c] = list(set(ancestors[c] + a))
115 ancestors[c] = list(set(ancestors[c] + a))
116 else:
116 else:
117 ancestors[c] = a + [c]
117 ancestors[c] = a + [c]
118
118
119 assert best_rev is not None
119 assert best_rev is not None
120 best_node = changelog.node(best_rev)
120 best_node = changelog.node(best_rev)
121
121
122 return ([best_node], tot, good)
122 return ([best_node], tot, good)
123
123
124 def extendrange(repo, state, nodes, good):
124 def extendrange(repo, state, nodes, good):
125 # bisect is incomplete when it ends on a merge node and
125 # bisect is incomplete when it ends on a merge node and
126 # one of the parent was not checked.
126 # one of the parent was not checked.
127 parents = repo[nodes[0]].parents()
127 parents = repo[nodes[0]].parents()
128 if len(parents) > 1:
128 if len(parents) > 1:
129 if good:
129 if good:
130 side = state['bad']
130 side = state['bad']
131 else:
131 else:
132 side = state['good']
132 side = state['good']
133 num = len(set(i.node() for i in parents) & set(side))
133 num = len(set(i.node() for i in parents) & set(side))
134 if num == 1:
134 if num == 1:
135 return parents[0].ancestor(parents[1])
135 return parents[0].ancestor(parents[1])
136 return None
136 return None
137
137
138 def load_state(repo):
138 def load_state(repo):
139 state = {'current': [], 'good': [], 'bad': [], 'skip': []}
139 state = {'current': [], 'good': [], 'bad': [], 'skip': []}
140 for l in repo.vfs.tryreadlines("bisect.state"):
140 for l in repo.vfs.tryreadlines("bisect.state"):
141 kind, node = l[:-1].split()
141 kind, node = l[:-1].split()
142 node = repo.lookup(node)
142 node = repo.lookup(node)
143 if kind not in state:
143 if kind not in state:
144 raise error.Abort(_("unknown bisect kind %s") % kind)
144 raise error.Abort(_("unknown bisect kind %s") % kind)
145 state[kind].append(node)
145 state[kind].append(node)
146 return state
146 return state
147
147
148
148
149 def save_state(repo, state):
149 def save_state(repo, state):
150 f = repo.vfs("bisect.state", "w", atomictemp=True)
150 f = repo.vfs("bisect.state", "w", atomictemp=True)
151 with repo.wlock():
151 with repo.wlock():
152 for kind in sorted(state):
152 for kind in sorted(state):
153 for node in state[kind]:
153 for node in state[kind]:
154 f.write("%s %s\n" % (kind, hex(node)))
154 f.write("%s %s\n" % (kind, hex(node)))
155 f.close()
155 f.close()
156
156
157 def resetstate(repo):
157 def resetstate(repo):
158 """remove any bisect state from the repository"""
158 """remove any bisect state from the repository"""
159 if repo.vfs.exists("bisect.state"):
159 if repo.vfs.exists("bisect.state"):
160 repo.vfs.unlink("bisect.state")
160 repo.vfs.unlink("bisect.state")
161
161
162 def checkstate(state):
162 def checkstate(state):
163 """check we have both 'good' and 'bad' to define a range
163 """check we have both 'good' and 'bad' to define a range
164
164
165 Raise Abort exception otherwise."""
165 Raise Abort exception otherwise."""
166 if state['good'] and state['bad']:
166 if state['good'] and state['bad']:
167 return True
167 return True
168 if not state['good']:
168 if not state['good']:
169 raise error.Abort(_('cannot bisect (no known good revisions)'))
169 raise error.Abort(_('cannot bisect (no known good revisions)'))
170 else:
170 else:
171 raise error.Abort(_('cannot bisect (no known bad revisions)'))
171 raise error.Abort(_('cannot bisect (no known bad revisions)'))
172
172
173 def get(repo, status):
173 def get(repo, status):
174 """
174 """
175 Return a list of revision(s) that match the given status:
175 Return a list of revision(s) that match the given status:
176
176
177 - ``good``, ``bad``, ``skip``: csets explicitly marked as good/bad/skip
177 - ``good``, ``bad``, ``skip``: csets explicitly marked as good/bad/skip
178 - ``goods``, ``bads`` : csets topologically good/bad
178 - ``goods``, ``bads`` : csets topologically good/bad
179 - ``range`` : csets taking part in the bisection
179 - ``range`` : csets taking part in the bisection
180 - ``pruned`` : csets that are goods, bads or skipped
180 - ``pruned`` : csets that are goods, bads or skipped
181 - ``untested`` : csets whose fate is yet unknown
181 - ``untested`` : csets whose fate is yet unknown
182 - ``ignored`` : csets ignored due to DAG topology
182 - ``ignored`` : csets ignored due to DAG topology
183 - ``current`` : the cset currently being bisected
183 - ``current`` : the cset currently being bisected
184 """
184 """
185 state = load_state(repo)
185 state = load_state(repo)
186 if status in ('good', 'bad', 'skip', 'current'):
186 if status in ('good', 'bad', 'skip', 'current'):
187 return map(repo.changelog.rev, state[status])
187 return map(repo.changelog.rev, state[status])
188 else:
188 else:
189 # In the following sets, we do *not* call 'bisect()' with more
189 # In the following sets, we do *not* call 'bisect()' with more
190 # than one level of recursion, because that can be very, very
190 # than one level of recursion, because that can be very, very
191 # time consuming. Instead, we always develop the expression as
191 # time consuming. Instead, we always develop the expression as
192 # much as possible.
192 # much as possible.
193
193
194 # 'range' is all csets that make the bisection:
194 # 'range' is all csets that make the bisection:
195 # - have a good ancestor and a bad descendant, or conversely
195 # - have a good ancestor and a bad descendant, or conversely
196 # that's because the bisection can go either way
196 # that's because the bisection can go either way
197 range = '( bisect(bad)::bisect(good) | bisect(good)::bisect(bad) )'
197 range = '( bisect(bad)::bisect(good) | bisect(good)::bisect(bad) )'
198
198
199 _t = repo.revs('bisect(good)::bisect(bad)')
199 _t = repo.revs('bisect(good)::bisect(bad)')
200 # The sets of topologically good or bad csets
200 # The sets of topologically good or bad csets
201 if len(_t) == 0:
201 if len(_t) == 0:
202 # Goods are topologically after bads
202 # Goods are topologically after bads
203 goods = 'bisect(good)::' # Pruned good csets
203 goods = 'bisect(good)::' # Pruned good csets
204 bads = '::bisect(bad)' # Pruned bad csets
204 bads = '::bisect(bad)' # Pruned bad csets
205 else:
205 else:
206 # Goods are topologically before bads
206 # Goods are topologically before bads
207 goods = '::bisect(good)' # Pruned good csets
207 goods = '::bisect(good)' # Pruned good csets
208 bads = 'bisect(bad)::' # Pruned bad csets
208 bads = 'bisect(bad)::' # Pruned bad csets
209
209
210 # 'pruned' is all csets whose fate is already known: good, bad, skip
210 # 'pruned' is all csets whose fate is already known: good, bad, skip
211 skips = 'bisect(skip)' # Pruned skipped csets
211 skips = 'bisect(skip)' # Pruned skipped csets
212 pruned = '( (%s) | (%s) | (%s) )' % (goods, bads, skips)
212 pruned = '( (%s) | (%s) | (%s) )' % (goods, bads, skips)
213
213
214 # 'untested' is all cset that are- in 'range', but not in 'pruned'
214 # 'untested' is all cset that are- in 'range', but not in 'pruned'
215 untested = '( (%s) - (%s) )' % (range, pruned)
215 untested = '( (%s) - (%s) )' % (range, pruned)
216
216
217 # 'ignored' is all csets that were not used during the bisection
217 # 'ignored' is all csets that were not used during the bisection
218 # due to DAG topology, but may however have had an impact.
218 # due to DAG topology, but may however have had an impact.
219 # E.g., a branch merged between bads and goods, but whose branch-
219 # E.g., a branch merged between bads and goods, but whose branch-
220 # point is out-side of the range.
220 # point is out-side of the range.
221 iba = '::bisect(bad) - ::bisect(good)' # Ignored bads' ancestors
221 iba = '::bisect(bad) - ::bisect(good)' # Ignored bads' ancestors
222 iga = '::bisect(good) - ::bisect(bad)' # Ignored goods' ancestors
222 iga = '::bisect(good) - ::bisect(bad)' # Ignored goods' ancestors
223 ignored = '( ( (%s) | (%s) ) - (%s) )' % (iba, iga, range)
223 ignored = '( ( (%s) | (%s) ) - (%s) )' % (iba, iga, range)
224
224
225 if status == 'range':
225 if status == 'range':
226 return repo.revs(range)
226 return repo.revs(range)
227 elif status == 'pruned':
227 elif status == 'pruned':
228 return repo.revs(pruned)
228 return repo.revs(pruned)
229 elif status == 'untested':
229 elif status == 'untested':
230 return repo.revs(untested)
230 return repo.revs(untested)
231 elif status == 'ignored':
231 elif status == 'ignored':
232 return repo.revs(ignored)
232 return repo.revs(ignored)
233 elif status == "goods":
233 elif status == "goods":
234 return repo.revs(goods)
234 return repo.revs(goods)
235 elif status == "bads":
235 elif status == "bads":
236 return repo.revs(bads)
236 return repo.revs(bads)
237 else:
237 else:
238 raise error.ParseError(_('invalid bisect state'))
238 raise error.ParseError(_('invalid bisect state'))
239
239
240 def label(repo, node):
240 def label(repo, node):
241 rev = repo.changelog.rev(node)
241 rev = repo.changelog.rev(node)
242
242
243 # Try explicit sets
243 # Try explicit sets
244 if rev in get(repo, 'good'):
244 if rev in get(repo, 'good'):
245 # i18n: bisect changeset status
245 # i18n: bisect changeset status
246 return _('good')
246 return _('good')
247 if rev in get(repo, 'bad'):
247 if rev in get(repo, 'bad'):
248 # i18n: bisect changeset status
248 # i18n: bisect changeset status
249 return _('bad')
249 return _('bad')
250 if rev in get(repo, 'skip'):
250 if rev in get(repo, 'skip'):
251 # i18n: bisect changeset status
251 # i18n: bisect changeset status
252 return _('skipped')
252 return _('skipped')
253 if rev in get(repo, 'untested') or rev in get(repo, 'current'):
253 if rev in get(repo, 'untested') or rev in get(repo, 'current'):
254 # i18n: bisect changeset status
254 # i18n: bisect changeset status
255 return _('untested')
255 return _('untested')
256 if rev in get(repo, 'ignored'):
256 if rev in get(repo, 'ignored'):
257 # i18n: bisect changeset status
257 # i18n: bisect changeset status
258 return _('ignored')
258 return _('ignored')
259
259
260 # Try implicit sets
260 # Try implicit sets
261 if rev in get(repo, 'goods'):
261 if rev in get(repo, 'goods'):
262 # i18n: bisect changeset status
262 # i18n: bisect changeset status
263 return _('good (implicit)')
263 return _('good (implicit)')
264 if rev in get(repo, 'bads'):
264 if rev in get(repo, 'bads'):
265 # i18n: bisect changeset status
265 # i18n: bisect changeset status
266 return _('bad (implicit)')
266 return _('bad (implicit)')
267
267
268 return None
268 return None
269
269
270 def shortlabel(label):
271 if label:
272 return label[0].upper()
273
274 return None
275
276 def printresult(ui, repo, state, displayer, nodes, good):
270 def printresult(ui, repo, state, displayer, nodes, good):
277 if len(nodes) == 1:
271 if len(nodes) == 1:
278 # narrowed it down to a single revision
272 # narrowed it down to a single revision
279 if good:
273 if good:
280 ui.write(_("The first good revision is:\n"))
274 ui.write(_("The first good revision is:\n"))
281 else:
275 else:
282 ui.write(_("The first bad revision is:\n"))
276 ui.write(_("The first bad revision is:\n"))
283 displayer.show(repo[nodes[0]])
277 displayer.show(repo[nodes[0]])
284 extendnode = extendrange(repo, state, nodes, good)
278 extendnode = extendrange(repo, state, nodes, good)
285 if extendnode is not None:
279 if extendnode is not None:
286 ui.write(_('Not all ancestors of this changeset have been'
280 ui.write(_('Not all ancestors of this changeset have been'
287 ' checked.\nUse bisect --extend to continue the '
281 ' checked.\nUse bisect --extend to continue the '
288 'bisection from\nthe common ancestor, %s.\n')
282 'bisection from\nthe common ancestor, %s.\n')
289 % extendnode)
283 % extendnode)
290 else:
284 else:
291 # multiple possible revisions
285 # multiple possible revisions
292 if good:
286 if good:
293 ui.write(_("Due to skipped revisions, the first "
287 ui.write(_("Due to skipped revisions, the first "
294 "good revision could be any of:\n"))
288 "good revision could be any of:\n"))
295 else:
289 else:
296 ui.write(_("Due to skipped revisions, the first "
290 ui.write(_("Due to skipped revisions, the first "
297 "bad revision could be any of:\n"))
291 "bad revision could be any of:\n"))
298 for n in nodes:
292 for n in nodes:
299 displayer.show(repo[n])
293 displayer.show(repo[n])
300 displayer.close()
294 displayer.close()
@@ -1,463 +1,464
1 # templatefilters.py - common template expansion filters
1 # templatefilters.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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import os
10 import os
11 import re
11 import re
12 import time
12 import time
13
13
14 from . import (
14 from . import (
15 encoding,
15 encoding,
16 error,
16 error,
17 hbisect,
18 node,
17 node,
19 pycompat,
18 pycompat,
20 registrar,
19 registrar,
21 templatekw,
20 templatekw,
22 url,
21 url,
23 util,
22 util,
24 )
23 )
25 from .utils import dateutil
24 from .utils import dateutil
26
25
27 urlerr = util.urlerr
26 urlerr = util.urlerr
28 urlreq = util.urlreq
27 urlreq = util.urlreq
29
28
30 if pycompat.ispy3:
29 if pycompat.ispy3:
31 long = int
30 long = int
32
31
33 # filters are callables like:
32 # filters are callables like:
34 # fn(obj)
33 # fn(obj)
35 # with:
34 # with:
36 # obj - object to be filtered (text, date, list and so on)
35 # obj - object to be filtered (text, date, list and so on)
37 filters = {}
36 filters = {}
38
37
39 templatefilter = registrar.templatefilter(filters)
38 templatefilter = registrar.templatefilter(filters)
40
39
41 @templatefilter('addbreaks')
40 @templatefilter('addbreaks')
42 def addbreaks(text):
41 def addbreaks(text):
43 """Any text. Add an XHTML "<br />" tag before the end of
42 """Any text. Add an XHTML "<br />" tag before the end of
44 every line except the last.
43 every line except the last.
45 """
44 """
46 return text.replace('\n', '<br/>\n')
45 return text.replace('\n', '<br/>\n')
47
46
48 agescales = [("year", 3600 * 24 * 365, 'Y'),
47 agescales = [("year", 3600 * 24 * 365, 'Y'),
49 ("month", 3600 * 24 * 30, 'M'),
48 ("month", 3600 * 24 * 30, 'M'),
50 ("week", 3600 * 24 * 7, 'W'),
49 ("week", 3600 * 24 * 7, 'W'),
51 ("day", 3600 * 24, 'd'),
50 ("day", 3600 * 24, 'd'),
52 ("hour", 3600, 'h'),
51 ("hour", 3600, 'h'),
53 ("minute", 60, 'm'),
52 ("minute", 60, 'm'),
54 ("second", 1, 's')]
53 ("second", 1, 's')]
55
54
56 @templatefilter('age')
55 @templatefilter('age')
57 def age(date, abbrev=False):
56 def age(date, abbrev=False):
58 """Date. Returns a human-readable date/time difference between the
57 """Date. Returns a human-readable date/time difference between the
59 given date/time and the current date/time.
58 given date/time and the current date/time.
60 """
59 """
61
60
62 def plural(t, c):
61 def plural(t, c):
63 if c == 1:
62 if c == 1:
64 return t
63 return t
65 return t + "s"
64 return t + "s"
66 def fmt(t, c, a):
65 def fmt(t, c, a):
67 if abbrev:
66 if abbrev:
68 return "%d%s" % (c, a)
67 return "%d%s" % (c, a)
69 return "%d %s" % (c, plural(t, c))
68 return "%d %s" % (c, plural(t, c))
70
69
71 now = time.time()
70 now = time.time()
72 then = date[0]
71 then = date[0]
73 future = False
72 future = False
74 if then > now:
73 if then > now:
75 future = True
74 future = True
76 delta = max(1, int(then - now))
75 delta = max(1, int(then - now))
77 if delta > agescales[0][1] * 30:
76 if delta > agescales[0][1] * 30:
78 return 'in the distant future'
77 return 'in the distant future'
79 else:
78 else:
80 delta = max(1, int(now - then))
79 delta = max(1, int(now - then))
81 if delta > agescales[0][1] * 2:
80 if delta > agescales[0][1] * 2:
82 return dateutil.shortdate(date)
81 return dateutil.shortdate(date)
83
82
84 for t, s, a in agescales:
83 for t, s, a in agescales:
85 n = delta // s
84 n = delta // s
86 if n >= 2 or s == 1:
85 if n >= 2 or s == 1:
87 if future:
86 if future:
88 return '%s from now' % fmt(t, n, a)
87 return '%s from now' % fmt(t, n, a)
89 return '%s ago' % fmt(t, n, a)
88 return '%s ago' % fmt(t, n, a)
90
89
91 @templatefilter('basename')
90 @templatefilter('basename')
92 def basename(path):
91 def basename(path):
93 """Any text. Treats the text as a path, and returns the last
92 """Any text. Treats the text as a path, and returns the last
94 component of the path after splitting by the path separator.
93 component of the path after splitting by the path separator.
95 For example, "foo/bar/baz" becomes "baz" and "foo/bar//" becomes "".
94 For example, "foo/bar/baz" becomes "baz" and "foo/bar//" becomes "".
96 """
95 """
97 return os.path.basename(path)
96 return os.path.basename(path)
98
97
99 @templatefilter('count')
98 @templatefilter('count')
100 def count(i):
99 def count(i):
101 """List or text. Returns the length as an integer."""
100 """List or text. Returns the length as an integer."""
102 return len(i)
101 return len(i)
103
102
104 @templatefilter('dirname')
103 @templatefilter('dirname')
105 def dirname(path):
104 def dirname(path):
106 """Any text. Treats the text as a path, and strips the last
105 """Any text. Treats the text as a path, and strips the last
107 component of the path after splitting by the path separator.
106 component of the path after splitting by the path separator.
108 """
107 """
109 return os.path.dirname(path)
108 return os.path.dirname(path)
110
109
111 @templatefilter('domain')
110 @templatefilter('domain')
112 def domain(author):
111 def domain(author):
113 """Any text. Finds the first string that looks like an email
112 """Any text. Finds the first string that looks like an email
114 address, and extracts just the domain component. Example: ``User
113 address, and extracts just the domain component. Example: ``User
115 <user@example.com>`` becomes ``example.com``.
114 <user@example.com>`` becomes ``example.com``.
116 """
115 """
117 f = author.find('@')
116 f = author.find('@')
118 if f == -1:
117 if f == -1:
119 return ''
118 return ''
120 author = author[f + 1:]
119 author = author[f + 1:]
121 f = author.find('>')
120 f = author.find('>')
122 if f >= 0:
121 if f >= 0:
123 author = author[:f]
122 author = author[:f]
124 return author
123 return author
125
124
126 @templatefilter('email')
125 @templatefilter('email')
127 def email(text):
126 def email(text):
128 """Any text. Extracts the first string that looks like an email
127 """Any text. Extracts the first string that looks like an email
129 address. Example: ``User <user@example.com>`` becomes
128 address. Example: ``User <user@example.com>`` becomes
130 ``user@example.com``.
129 ``user@example.com``.
131 """
130 """
132 return util.email(text)
131 return util.email(text)
133
132
134 @templatefilter('escape')
133 @templatefilter('escape')
135 def escape(text):
134 def escape(text):
136 """Any text. Replaces the special XML/XHTML characters "&", "<"
135 """Any text. Replaces the special XML/XHTML characters "&", "<"
137 and ">" with XML entities, and filters out NUL characters.
136 and ">" with XML entities, and filters out NUL characters.
138 """
137 """
139 return url.escape(text.replace('\0', ''), True)
138 return url.escape(text.replace('\0', ''), True)
140
139
141 para_re = None
140 para_re = None
142 space_re = None
141 space_re = None
143
142
144 def fill(text, width, initindent='', hangindent=''):
143 def fill(text, width, initindent='', hangindent=''):
145 '''fill many paragraphs with optional indentation.'''
144 '''fill many paragraphs with optional indentation.'''
146 global para_re, space_re
145 global para_re, space_re
147 if para_re is None:
146 if para_re is None:
148 para_re = re.compile('(\n\n|\n\\s*[-*]\\s*)', re.M)
147 para_re = re.compile('(\n\n|\n\\s*[-*]\\s*)', re.M)
149 space_re = re.compile(br' +')
148 space_re = re.compile(br' +')
150
149
151 def findparas():
150 def findparas():
152 start = 0
151 start = 0
153 while True:
152 while True:
154 m = para_re.search(text, start)
153 m = para_re.search(text, start)
155 if not m:
154 if not m:
156 uctext = encoding.unifromlocal(text[start:])
155 uctext = encoding.unifromlocal(text[start:])
157 w = len(uctext)
156 w = len(uctext)
158 while 0 < w and uctext[w - 1].isspace():
157 while 0 < w and uctext[w - 1].isspace():
159 w -= 1
158 w -= 1
160 yield (encoding.unitolocal(uctext[:w]),
159 yield (encoding.unitolocal(uctext[:w]),
161 encoding.unitolocal(uctext[w:]))
160 encoding.unitolocal(uctext[w:]))
162 break
161 break
163 yield text[start:m.start(0)], m.group(1)
162 yield text[start:m.start(0)], m.group(1)
164 start = m.end(1)
163 start = m.end(1)
165
164
166 return "".join([util.wrap(space_re.sub(' ', util.wrap(para, width)),
165 return "".join([util.wrap(space_re.sub(' ', util.wrap(para, width)),
167 width, initindent, hangindent) + rest
166 width, initindent, hangindent) + rest
168 for para, rest in findparas()])
167 for para, rest in findparas()])
169
168
170 @templatefilter('fill68')
169 @templatefilter('fill68')
171 def fill68(text):
170 def fill68(text):
172 """Any text. Wraps the text to fit in 68 columns."""
171 """Any text. Wraps the text to fit in 68 columns."""
173 return fill(text, 68)
172 return fill(text, 68)
174
173
175 @templatefilter('fill76')
174 @templatefilter('fill76')
176 def fill76(text):
175 def fill76(text):
177 """Any text. Wraps the text to fit in 76 columns."""
176 """Any text. Wraps the text to fit in 76 columns."""
178 return fill(text, 76)
177 return fill(text, 76)
179
178
180 @templatefilter('firstline')
179 @templatefilter('firstline')
181 def firstline(text):
180 def firstline(text):
182 """Any text. Returns the first line of text."""
181 """Any text. Returns the first line of text."""
183 try:
182 try:
184 return text.splitlines(True)[0].rstrip('\r\n')
183 return text.splitlines(True)[0].rstrip('\r\n')
185 except IndexError:
184 except IndexError:
186 return ''
185 return ''
187
186
188 @templatefilter('hex')
187 @templatefilter('hex')
189 def hexfilter(text):
188 def hexfilter(text):
190 """Any text. Convert a binary Mercurial node identifier into
189 """Any text. Convert a binary Mercurial node identifier into
191 its long hexadecimal representation.
190 its long hexadecimal representation.
192 """
191 """
193 return node.hex(text)
192 return node.hex(text)
194
193
195 @templatefilter('hgdate')
194 @templatefilter('hgdate')
196 def hgdate(text):
195 def hgdate(text):
197 """Date. Returns the date as a pair of numbers: "1157407993
196 """Date. Returns the date as a pair of numbers: "1157407993
198 25200" (Unix timestamp, timezone offset).
197 25200" (Unix timestamp, timezone offset).
199 """
198 """
200 return "%d %d" % text
199 return "%d %d" % text
201
200
202 @templatefilter('isodate')
201 @templatefilter('isodate')
203 def isodate(text):
202 def isodate(text):
204 """Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
203 """Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
205 +0200".
204 +0200".
206 """
205 """
207 return dateutil.datestr(text, '%Y-%m-%d %H:%M %1%2')
206 return dateutil.datestr(text, '%Y-%m-%d %H:%M %1%2')
208
207
209 @templatefilter('isodatesec')
208 @templatefilter('isodatesec')
210 def isodatesec(text):
209 def isodatesec(text):
211 """Date. Returns the date in ISO 8601 format, including
210 """Date. Returns the date in ISO 8601 format, including
212 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
211 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
213 filter.
212 filter.
214 """
213 """
215 return dateutil.datestr(text, '%Y-%m-%d %H:%M:%S %1%2')
214 return dateutil.datestr(text, '%Y-%m-%d %H:%M:%S %1%2')
216
215
217 def indent(text, prefix):
216 def indent(text, prefix):
218 '''indent each non-empty line of text after first with prefix.'''
217 '''indent each non-empty line of text after first with prefix.'''
219 lines = text.splitlines()
218 lines = text.splitlines()
220 num_lines = len(lines)
219 num_lines = len(lines)
221 endswithnewline = text[-1:] == '\n'
220 endswithnewline = text[-1:] == '\n'
222 def indenter():
221 def indenter():
223 for i in xrange(num_lines):
222 for i in xrange(num_lines):
224 l = lines[i]
223 l = lines[i]
225 if i and l.strip():
224 if i and l.strip():
226 yield prefix
225 yield prefix
227 yield l
226 yield l
228 if i < num_lines - 1 or endswithnewline:
227 if i < num_lines - 1 or endswithnewline:
229 yield '\n'
228 yield '\n'
230 return "".join(indenter())
229 return "".join(indenter())
231
230
232 @templatefilter('json')
231 @templatefilter('json')
233 def json(obj, paranoid=True):
232 def json(obj, paranoid=True):
234 if obj is None:
233 if obj is None:
235 return 'null'
234 return 'null'
236 elif obj is False:
235 elif obj is False:
237 return 'false'
236 return 'false'
238 elif obj is True:
237 elif obj is True:
239 return 'true'
238 return 'true'
240 elif isinstance(obj, (int, long, float)):
239 elif isinstance(obj, (int, long, float)):
241 return pycompat.bytestr(obj)
240 return pycompat.bytestr(obj)
242 elif isinstance(obj, bytes):
241 elif isinstance(obj, bytes):
243 return '"%s"' % encoding.jsonescape(obj, paranoid=paranoid)
242 return '"%s"' % encoding.jsonescape(obj, paranoid=paranoid)
244 elif isinstance(obj, str):
243 elif isinstance(obj, str):
245 # This branch is unreachable on Python 2, because bytes == str
244 # This branch is unreachable on Python 2, because bytes == str
246 # and we'll return in the next-earlier block in the elif
245 # and we'll return in the next-earlier block in the elif
247 # ladder. On Python 3, this helps us catch bugs before they
246 # ladder. On Python 3, this helps us catch bugs before they
248 # hurt someone.
247 # hurt someone.
249 raise error.ProgrammingError(
248 raise error.ProgrammingError(
250 'Mercurial only does output with bytes on Python 3: %r' % obj)
249 'Mercurial only does output with bytes on Python 3: %r' % obj)
251 elif util.safehasattr(obj, 'keys'):
250 elif util.safehasattr(obj, 'keys'):
252 out = ['"%s": %s' % (encoding.jsonescape(k, paranoid=paranoid),
251 out = ['"%s": %s' % (encoding.jsonescape(k, paranoid=paranoid),
253 json(v, paranoid))
252 json(v, paranoid))
254 for k, v in sorted(obj.iteritems())]
253 for k, v in sorted(obj.iteritems())]
255 return '{' + ', '.join(out) + '}'
254 return '{' + ', '.join(out) + '}'
256 elif util.safehasattr(obj, '__iter__'):
255 elif util.safehasattr(obj, '__iter__'):
257 out = [json(i, paranoid) for i in obj]
256 out = [json(i, paranoid) for i in obj]
258 return '[' + ', '.join(out) + ']'
257 return '[' + ', '.join(out) + ']'
259 else:
258 else:
260 raise TypeError('cannot encode type %s' % obj.__class__.__name__)
259 raise TypeError('cannot encode type %s' % obj.__class__.__name__)
261
260
262 @templatefilter('lower')
261 @templatefilter('lower')
263 def lower(text):
262 def lower(text):
264 """Any text. Converts the text to lowercase."""
263 """Any text. Converts the text to lowercase."""
265 return encoding.lower(text)
264 return encoding.lower(text)
266
265
267 @templatefilter('nonempty')
266 @templatefilter('nonempty')
268 def nonempty(text):
267 def nonempty(text):
269 """Any text. Returns '(none)' if the string is empty."""
268 """Any text. Returns '(none)' if the string is empty."""
270 return text or "(none)"
269 return text or "(none)"
271
270
272 @templatefilter('obfuscate')
271 @templatefilter('obfuscate')
273 def obfuscate(text):
272 def obfuscate(text):
274 """Any text. Returns the input text rendered as a sequence of
273 """Any text. Returns the input text rendered as a sequence of
275 XML entities.
274 XML entities.
276 """
275 """
277 text = unicode(text, pycompat.sysstr(encoding.encoding), r'replace')
276 text = unicode(text, pycompat.sysstr(encoding.encoding), r'replace')
278 return ''.join(['&#%d;' % ord(c) for c in text])
277 return ''.join(['&#%d;' % ord(c) for c in text])
279
278
280 @templatefilter('permissions')
279 @templatefilter('permissions')
281 def permissions(flags):
280 def permissions(flags):
282 if "l" in flags:
281 if "l" in flags:
283 return "lrwxrwxrwx"
282 return "lrwxrwxrwx"
284 if "x" in flags:
283 if "x" in flags:
285 return "-rwxr-xr-x"
284 return "-rwxr-xr-x"
286 return "-rw-r--r--"
285 return "-rw-r--r--"
287
286
288 @templatefilter('person')
287 @templatefilter('person')
289 def person(author):
288 def person(author):
290 """Any text. Returns the name before an email address,
289 """Any text. Returns the name before an email address,
291 interpreting it as per RFC 5322.
290 interpreting it as per RFC 5322.
292
291
293 >>> person(b'foo@bar')
292 >>> person(b'foo@bar')
294 'foo'
293 'foo'
295 >>> person(b'Foo Bar <foo@bar>')
294 >>> person(b'Foo Bar <foo@bar>')
296 'Foo Bar'
295 'Foo Bar'
297 >>> person(b'"Foo Bar" <foo@bar>')
296 >>> person(b'"Foo Bar" <foo@bar>')
298 'Foo Bar'
297 'Foo Bar'
299 >>> person(b'"Foo \"buz\" Bar" <foo@bar>')
298 >>> person(b'"Foo \"buz\" Bar" <foo@bar>')
300 'Foo "buz" Bar'
299 'Foo "buz" Bar'
301 >>> # The following are invalid, but do exist in real-life
300 >>> # The following are invalid, but do exist in real-life
302 ...
301 ...
303 >>> person(b'Foo "buz" Bar <foo@bar>')
302 >>> person(b'Foo "buz" Bar <foo@bar>')
304 'Foo "buz" Bar'
303 'Foo "buz" Bar'
305 >>> person(b'"Foo Bar <foo@bar>')
304 >>> person(b'"Foo Bar <foo@bar>')
306 'Foo Bar'
305 'Foo Bar'
307 """
306 """
308 if '@' not in author:
307 if '@' not in author:
309 return author
308 return author
310 f = author.find('<')
309 f = author.find('<')
311 if f != -1:
310 if f != -1:
312 return author[:f].strip(' "').replace('\\"', '"')
311 return author[:f].strip(' "').replace('\\"', '"')
313 f = author.find('@')
312 f = author.find('@')
314 return author[:f].replace('.', ' ')
313 return author[:f].replace('.', ' ')
315
314
316 @templatefilter('revescape')
315 @templatefilter('revescape')
317 def revescape(text):
316 def revescape(text):
318 """Any text. Escapes all "special" characters, except @.
317 """Any text. Escapes all "special" characters, except @.
319 Forward slashes are escaped twice to prevent web servers from prematurely
318 Forward slashes are escaped twice to prevent web servers from prematurely
320 unescaping them. For example, "@foo bar/baz" becomes "@foo%20bar%252Fbaz".
319 unescaping them. For example, "@foo bar/baz" becomes "@foo%20bar%252Fbaz".
321 """
320 """
322 return urlreq.quote(text, safe='/@').replace('/', '%252F')
321 return urlreq.quote(text, safe='/@').replace('/', '%252F')
323
322
324 @templatefilter('rfc3339date')
323 @templatefilter('rfc3339date')
325 def rfc3339date(text):
324 def rfc3339date(text):
326 """Date. Returns a date using the Internet date format
325 """Date. Returns a date using the Internet date format
327 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
326 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
328 """
327 """
329 return dateutil.datestr(text, "%Y-%m-%dT%H:%M:%S%1:%2")
328 return dateutil.datestr(text, "%Y-%m-%dT%H:%M:%S%1:%2")
330
329
331 @templatefilter('rfc822date')
330 @templatefilter('rfc822date')
332 def rfc822date(text):
331 def rfc822date(text):
333 """Date. Returns a date using the same format used in email
332 """Date. Returns a date using the same format used in email
334 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
333 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
335 """
334 """
336 return dateutil.datestr(text, "%a, %d %b %Y %H:%M:%S %1%2")
335 return dateutil.datestr(text, "%a, %d %b %Y %H:%M:%S %1%2")
337
336
338 @templatefilter('short')
337 @templatefilter('short')
339 def short(text):
338 def short(text):
340 """Changeset hash. Returns the short form of a changeset hash,
339 """Changeset hash. Returns the short form of a changeset hash,
341 i.e. a 12 hexadecimal digit string.
340 i.e. a 12 hexadecimal digit string.
342 """
341 """
343 return text[:12]
342 return text[:12]
344
343
345 @templatefilter('shortbisect')
344 @templatefilter('shortbisect')
346 def shortbisect(text):
345 def shortbisect(label):
347 """Any text. Treats `text` as a bisection status, and
346 """Any text. Treats `label` as a bisection status, and
348 returns a single-character representing the status (G: good, B: bad,
347 returns a single-character representing the status (G: good, B: bad,
349 S: skipped, U: untested, I: ignored). Returns single space if `text`
348 S: skipped, U: untested, I: ignored). Returns single space if `text`
350 is not a valid bisection status.
349 is not a valid bisection status.
351 """
350 """
352 return hbisect.shortlabel(text) or ' '
351 if label:
352 return label[0].upper()
353 return ' '
353
354
354 @templatefilter('shortdate')
355 @templatefilter('shortdate')
355 def shortdate(text):
356 def shortdate(text):
356 """Date. Returns a date like "2006-09-18"."""
357 """Date. Returns a date like "2006-09-18"."""
357 return dateutil.shortdate(text)
358 return dateutil.shortdate(text)
358
359
359 @templatefilter('slashpath')
360 @templatefilter('slashpath')
360 def slashpath(path):
361 def slashpath(path):
361 """Any text. Replaces the native path separator with slash."""
362 """Any text. Replaces the native path separator with slash."""
362 return util.pconvert(path)
363 return util.pconvert(path)
363
364
364 @templatefilter('splitlines')
365 @templatefilter('splitlines')
365 def splitlines(text):
366 def splitlines(text):
366 """Any text. Split text into a list of lines."""
367 """Any text. Split text into a list of lines."""
367 return templatekw.hybridlist(text.splitlines(), name='line')
368 return templatekw.hybridlist(text.splitlines(), name='line')
368
369
369 @templatefilter('stringescape')
370 @templatefilter('stringescape')
370 def stringescape(text):
371 def stringescape(text):
371 return util.escapestr(text)
372 return util.escapestr(text)
372
373
373 @templatefilter('stringify')
374 @templatefilter('stringify')
374 def stringify(thing):
375 def stringify(thing):
375 """Any type. Turns the value into text by converting values into
376 """Any type. Turns the value into text by converting values into
376 text and concatenating them.
377 text and concatenating them.
377 """
378 """
378 thing = templatekw.unwraphybrid(thing)
379 thing = templatekw.unwraphybrid(thing)
379 if util.safehasattr(thing, '__iter__') and not isinstance(thing, bytes):
380 if util.safehasattr(thing, '__iter__') and not isinstance(thing, bytes):
380 if isinstance(thing, str):
381 if isinstance(thing, str):
381 # This is only reachable on Python 3 (otherwise
382 # This is only reachable on Python 3 (otherwise
382 # isinstance(thing, bytes) would have been true), and is
383 # isinstance(thing, bytes) would have been true), and is
383 # here to prevent infinite recursion bugs on Python 3.
384 # here to prevent infinite recursion bugs on Python 3.
384 raise error.ProgrammingError(
385 raise error.ProgrammingError(
385 'stringify got unexpected unicode string: %r' % thing)
386 'stringify got unexpected unicode string: %r' % thing)
386 return "".join([stringify(t) for t in thing if t is not None])
387 return "".join([stringify(t) for t in thing if t is not None])
387 if thing is None:
388 if thing is None:
388 return ""
389 return ""
389 return pycompat.bytestr(thing)
390 return pycompat.bytestr(thing)
390
391
391 @templatefilter('stripdir')
392 @templatefilter('stripdir')
392 def stripdir(text):
393 def stripdir(text):
393 """Treat the text as path and strip a directory level, if
394 """Treat the text as path and strip a directory level, if
394 possible. For example, "foo" and "foo/bar" becomes "foo".
395 possible. For example, "foo" and "foo/bar" becomes "foo".
395 """
396 """
396 dir = os.path.dirname(text)
397 dir = os.path.dirname(text)
397 if dir == "":
398 if dir == "":
398 return os.path.basename(text)
399 return os.path.basename(text)
399 else:
400 else:
400 return dir
401 return dir
401
402
402 @templatefilter('tabindent')
403 @templatefilter('tabindent')
403 def tabindent(text):
404 def tabindent(text):
404 """Any text. Returns the text, with every non-empty line
405 """Any text. Returns the text, with every non-empty line
405 except the first starting with a tab character.
406 except the first starting with a tab character.
406 """
407 """
407 return indent(text, '\t')
408 return indent(text, '\t')
408
409
409 @templatefilter('upper')
410 @templatefilter('upper')
410 def upper(text):
411 def upper(text):
411 """Any text. Converts the text to uppercase."""
412 """Any text. Converts the text to uppercase."""
412 return encoding.upper(text)
413 return encoding.upper(text)
413
414
414 @templatefilter('urlescape')
415 @templatefilter('urlescape')
415 def urlescape(text):
416 def urlescape(text):
416 """Any text. Escapes all "special" characters. For example,
417 """Any text. Escapes all "special" characters. For example,
417 "foo bar" becomes "foo%20bar".
418 "foo bar" becomes "foo%20bar".
418 """
419 """
419 return urlreq.quote(text)
420 return urlreq.quote(text)
420
421
421 @templatefilter('user')
422 @templatefilter('user')
422 def userfilter(text):
423 def userfilter(text):
423 """Any text. Returns a short representation of a user name or email
424 """Any text. Returns a short representation of a user name or email
424 address."""
425 address."""
425 return util.shortuser(text)
426 return util.shortuser(text)
426
427
427 @templatefilter('emailuser')
428 @templatefilter('emailuser')
428 def emailuser(text):
429 def emailuser(text):
429 """Any text. Returns the user portion of an email address."""
430 """Any text. Returns the user portion of an email address."""
430 return util.emailuser(text)
431 return util.emailuser(text)
431
432
432 @templatefilter('utf8')
433 @templatefilter('utf8')
433 def utf8(text):
434 def utf8(text):
434 """Any text. Converts from the local character encoding to UTF-8."""
435 """Any text. Converts from the local character encoding to UTF-8."""
435 return encoding.fromlocal(text)
436 return encoding.fromlocal(text)
436
437
437 @templatefilter('xmlescape')
438 @templatefilter('xmlescape')
438 def xmlescape(text):
439 def xmlescape(text):
439 text = (text
440 text = (text
440 .replace('&', '&amp;')
441 .replace('&', '&amp;')
441 .replace('<', '&lt;')
442 .replace('<', '&lt;')
442 .replace('>', '&gt;')
443 .replace('>', '&gt;')
443 .replace('"', '&quot;')
444 .replace('"', '&quot;')
444 .replace("'", '&#39;')) # &apos; invalid in HTML
445 .replace("'", '&#39;')) # &apos; invalid in HTML
445 return re.sub('[\x00-\x08\x0B\x0C\x0E-\x1F]', ' ', text)
446 return re.sub('[\x00-\x08\x0B\x0C\x0E-\x1F]', ' ', text)
446
447
447 def websub(text, websubtable):
448 def websub(text, websubtable):
448 """:websub: Any text. Only applies to hgweb. Applies the regular
449 """:websub: Any text. Only applies to hgweb. Applies the regular
449 expression replacements defined in the websub section.
450 expression replacements defined in the websub section.
450 """
451 """
451 if websubtable:
452 if websubtable:
452 for regexp, format in websubtable:
453 for regexp, format in websubtable:
453 text = regexp.sub(format, text)
454 text = regexp.sub(format, text)
454 return text
455 return text
455
456
456 def loadfilter(ui, extname, registrarobj):
457 def loadfilter(ui, extname, registrarobj):
457 """Load template filter from specified registrarobj
458 """Load template filter from specified registrarobj
458 """
459 """
459 for name, func in registrarobj._table.iteritems():
460 for name, func in registrarobj._table.iteritems():
460 filters[name] = func
461 filters[name] = func
461
462
462 # tell hggettext to extract docstrings from these functions:
463 # tell hggettext to extract docstrings from these functions:
463 i18nfunctions = filters.values()
464 i18nfunctions = filters.values()
General Comments 0
You need to be logged in to leave comments. Login now