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