##// END OF EJS Templates
merge with stable
Matt Mackall -
r24303:15afda34 merge default
parent child Browse files
Show More
@@ -1,763 +1,767
1 # templater.py - template expansion for output
1 # templater.py - template expansion for output
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 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 i18n import _
8 from i18n import _
9 import os, re
9 import os, re
10 import util, config, templatefilters, templatekw, parser, error
10 import util, config, templatefilters, templatekw, parser, error
11 import revset as revsetmod
11 import revset as revsetmod
12 import types
12 import types
13 import minirst
13 import minirst
14
14
15 # template parsing
15 # template parsing
16
16
17 elements = {
17 elements = {
18 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
18 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
19 ",": (2, None, ("list", 2)),
19 ",": (2, None, ("list", 2)),
20 "|": (5, None, ("|", 5)),
20 "|": (5, None, ("|", 5)),
21 "%": (6, None, ("%", 6)),
21 "%": (6, None, ("%", 6)),
22 ")": (0, None, None),
22 ")": (0, None, None),
23 "symbol": (0, ("symbol",), None),
23 "symbol": (0, ("symbol",), None),
24 "string": (0, ("string",), None),
24 "string": (0, ("string",), None),
25 "rawstring": (0, ("rawstring",), None),
25 "rawstring": (0, ("rawstring",), None),
26 "end": (0, None, None),
26 "end": (0, None, None),
27 }
27 }
28
28
29 def tokenizer(data):
29 def tokenizer(data):
30 program, start, end = data
30 program, start, end = data
31 pos = start
31 pos = start
32 while pos < end:
32 while pos < end:
33 c = program[pos]
33 c = program[pos]
34 if c.isspace(): # skip inter-token whitespace
34 if c.isspace(): # skip inter-token whitespace
35 pass
35 pass
36 elif c in "(,)%|": # handle simple operators
36 elif c in "(,)%|": # handle simple operators
37 yield (c, None, pos)
37 yield (c, None, pos)
38 elif (c in '"\'' or c == 'r' and
38 elif (c in '"\'' or c == 'r' and
39 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
39 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
40 if c == 'r':
40 if c == 'r':
41 pos += 1
41 pos += 1
42 c = program[pos]
42 c = program[pos]
43 decode = False
43 decode = False
44 else:
44 else:
45 decode = True
45 decode = True
46 pos += 1
46 pos += 1
47 s = pos
47 s = pos
48 while pos < end: # find closing quote
48 while pos < end: # find closing quote
49 d = program[pos]
49 d = program[pos]
50 if decode and d == '\\': # skip over escaped characters
50 if decode and d == '\\': # skip over escaped characters
51 pos += 2
51 pos += 2
52 continue
52 continue
53 if d == c:
53 if d == c:
54 if not decode:
54 if not decode:
55 yield ('rawstring', program[s:pos], s)
55 yield ('rawstring', program[s:pos], s)
56 break
56 break
57 yield ('string', program[s:pos], s)
57 yield ('string', program[s:pos], s)
58 break
58 break
59 pos += 1
59 pos += 1
60 else:
60 else:
61 raise error.ParseError(_("unterminated string"), s)
61 raise error.ParseError(_("unterminated string"), s)
62 elif c.isalnum() or c in '_':
62 elif c.isalnum() or c in '_':
63 s = pos
63 s = pos
64 pos += 1
64 pos += 1
65 while pos < end: # find end of symbol
65 while pos < end: # find end of symbol
66 d = program[pos]
66 d = program[pos]
67 if not (d.isalnum() or d == "_"):
67 if not (d.isalnum() or d == "_"):
68 break
68 break
69 pos += 1
69 pos += 1
70 sym = program[s:pos]
70 sym = program[s:pos]
71 yield ('symbol', sym, s)
71 yield ('symbol', sym, s)
72 pos -= 1
72 pos -= 1
73 elif c == '}':
73 elif c == '}':
74 pos += 1
74 pos += 1
75 break
75 break
76 else:
76 else:
77 raise error.ParseError(_("syntax error"), pos)
77 raise error.ParseError(_("syntax error"), pos)
78 pos += 1
78 pos += 1
79 yield ('end', None, pos)
79 yield ('end', None, pos)
80
80
81 def compiletemplate(tmpl, context, strtoken="string"):
81 def compiletemplate(tmpl, context, strtoken="string"):
82 parsed = []
82 parsed = []
83 pos, stop = 0, len(tmpl)
83 pos, stop = 0, len(tmpl)
84 p = parser.parser(tokenizer, elements)
84 p = parser.parser(tokenizer, elements)
85 while pos < stop:
85 while pos < stop:
86 n = tmpl.find('{', pos)
86 n = tmpl.find('{', pos)
87 if n < 0:
87 if n < 0:
88 parsed.append((strtoken, tmpl[pos:]))
88 parsed.append((strtoken, tmpl[pos:]))
89 break
89 break
90 if n > 0 and tmpl[n - 1] == '\\':
90 if n > 0 and tmpl[n - 1] == '\\':
91 # escaped
91 # escaped
92 parsed.append((strtoken, (tmpl[pos:n - 1] + "{")))
92 parsed.append((strtoken, (tmpl[pos:n - 1] + "{")))
93 pos = n + 1
93 pos = n + 1
94 continue
94 continue
95 if n > pos:
95 if n > pos:
96 parsed.append((strtoken, tmpl[pos:n]))
96 parsed.append((strtoken, tmpl[pos:n]))
97
97
98 pd = [tmpl, n + 1, stop]
98 pd = [tmpl, n + 1, stop]
99 parseres, pos = p.parse(pd)
99 parseres, pos = p.parse(pd)
100 parsed.append(parseres)
100 parsed.append(parseres)
101
101
102 return [compileexp(e, context) for e in parsed]
102 return [compileexp(e, context) for e in parsed]
103
103
104 def compileexp(exp, context):
104 def compileexp(exp, context):
105 t = exp[0]
105 t = exp[0]
106 if t in methods:
106 if t in methods:
107 return methods[t](exp, context)
107 return methods[t](exp, context)
108 raise error.ParseError(_("unknown method '%s'") % t)
108 raise error.ParseError(_("unknown method '%s'") % t)
109
109
110 # template evaluation
110 # template evaluation
111
111
112 def getsymbol(exp):
112 def getsymbol(exp):
113 if exp[0] == 'symbol':
113 if exp[0] == 'symbol':
114 return exp[1]
114 return exp[1]
115 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
115 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
116
116
117 def getlist(x):
117 def getlist(x):
118 if not x:
118 if not x:
119 return []
119 return []
120 if x[0] == 'list':
120 if x[0] == 'list':
121 return getlist(x[1]) + [x[2]]
121 return getlist(x[1]) + [x[2]]
122 return [x]
122 return [x]
123
123
124 def getfilter(exp, context):
124 def getfilter(exp, context):
125 f = getsymbol(exp)
125 f = getsymbol(exp)
126 if f not in context._filters:
126 if f not in context._filters:
127 raise error.ParseError(_("unknown function '%s'") % f)
127 raise error.ParseError(_("unknown function '%s'") % f)
128 return context._filters[f]
128 return context._filters[f]
129
129
130 def gettemplate(exp, context):
130 def gettemplate(exp, context):
131 if exp[0] == 'string' or exp[0] == 'rawstring':
131 if exp[0] == 'string' or exp[0] == 'rawstring':
132 return compiletemplate(exp[1], context, strtoken=exp[0])
132 return compiletemplate(exp[1], context, strtoken=exp[0])
133 if exp[0] == 'symbol':
133 if exp[0] == 'symbol':
134 return context._load(exp[1])
134 return context._load(exp[1])
135 raise error.ParseError(_("expected template specifier"))
135 raise error.ParseError(_("expected template specifier"))
136
136
137 def runstring(context, mapping, data):
137 def runstring(context, mapping, data):
138 return data.decode("string-escape")
138 return data.decode("string-escape")
139
139
140 def runrawstring(context, mapping, data):
140 def runrawstring(context, mapping, data):
141 return data
141 return data
142
142
143 def runsymbol(context, mapping, key):
143 def runsymbol(context, mapping, key):
144 v = mapping.get(key)
144 v = mapping.get(key)
145 if v is None:
145 if v is None:
146 v = context._defaults.get(key)
146 v = context._defaults.get(key)
147 if v is None:
147 if v is None:
148 try:
148 try:
149 v = context.process(key, mapping)
149 v = context.process(key, mapping)
150 except TemplateNotFound:
150 except TemplateNotFound:
151 v = ''
151 v = ''
152 if callable(v):
152 if callable(v):
153 return v(**mapping)
153 return v(**mapping)
154 if isinstance(v, types.GeneratorType):
154 if isinstance(v, types.GeneratorType):
155 v = list(v)
155 v = list(v)
156 return v
156 return v
157
157
158 def buildfilter(exp, context):
158 def buildfilter(exp, context):
159 func, data = compileexp(exp[1], context)
159 func, data = compileexp(exp[1], context)
160 filt = getfilter(exp[2], context)
160 filt = getfilter(exp[2], context)
161 return (runfilter, (func, data, filt))
161 return (runfilter, (func, data, filt))
162
162
163 def runfilter(context, mapping, data):
163 def runfilter(context, mapping, data):
164 func, data, filt = data
164 func, data, filt = data
165 # func() may return string, generator of strings or arbitrary object such
165 # func() may return string, generator of strings or arbitrary object such
166 # as date tuple, but filter does not want generator.
166 # as date tuple, but filter does not want generator.
167 thing = func(context, mapping, data)
167 thing = func(context, mapping, data)
168 if isinstance(thing, types.GeneratorType):
168 if isinstance(thing, types.GeneratorType):
169 thing = stringify(thing)
169 thing = stringify(thing)
170 try:
170 try:
171 return filt(thing)
171 return filt(thing)
172 except (ValueError, AttributeError, TypeError):
172 except (ValueError, AttributeError, TypeError):
173 if isinstance(data, tuple):
173 if isinstance(data, tuple):
174 dt = data[1]
174 dt = data[1]
175 else:
175 else:
176 dt = data
176 dt = data
177 raise util.Abort(_("template filter '%s' is not compatible with "
177 raise util.Abort(_("template filter '%s' is not compatible with "
178 "keyword '%s'") % (filt.func_name, dt))
178 "keyword '%s'") % (filt.func_name, dt))
179
179
180 def buildmap(exp, context):
180 def buildmap(exp, context):
181 func, data = compileexp(exp[1], context)
181 func, data = compileexp(exp[1], context)
182 ctmpl = gettemplate(exp[2], context)
182 ctmpl = gettemplate(exp[2], context)
183 return (runmap, (func, data, ctmpl))
183 return (runmap, (func, data, ctmpl))
184
184
185 def runtemplate(context, mapping, template):
185 def runtemplate(context, mapping, template):
186 for func, data in template:
186 for func, data in template:
187 yield func(context, mapping, data)
187 yield func(context, mapping, data)
188
188
189 def runmap(context, mapping, data):
189 def runmap(context, mapping, data):
190 func, data, ctmpl = data
190 func, data, ctmpl = data
191 d = func(context, mapping, data)
191 d = func(context, mapping, data)
192 if callable(d):
192 if callable(d):
193 d = d()
193 d = d()
194
194
195 lm = mapping.copy()
195 lm = mapping.copy()
196
196
197 for i in d:
197 for i in d:
198 if isinstance(i, dict):
198 if isinstance(i, dict):
199 lm.update(i)
199 lm.update(i)
200 lm['originalnode'] = mapping.get('node')
200 lm['originalnode'] = mapping.get('node')
201 yield runtemplate(context, lm, ctmpl)
201 yield runtemplate(context, lm, ctmpl)
202 else:
202 else:
203 # v is not an iterable of dicts, this happen when 'key'
203 # v is not an iterable of dicts, this happen when 'key'
204 # has been fully expanded already and format is useless.
204 # has been fully expanded already and format is useless.
205 # If so, return the expanded value.
205 # If so, return the expanded value.
206 yield i
206 yield i
207
207
208 def buildfunc(exp, context):
208 def buildfunc(exp, context):
209 n = getsymbol(exp[1])
209 n = getsymbol(exp[1])
210 args = [compileexp(x, context) for x in getlist(exp[2])]
210 args = [compileexp(x, context) for x in getlist(exp[2])]
211 if n in funcs:
211 if n in funcs:
212 f = funcs[n]
212 f = funcs[n]
213 return (f, args)
213 return (f, args)
214 if n in context._filters:
214 if n in context._filters:
215 if len(args) != 1:
215 if len(args) != 1:
216 raise error.ParseError(_("filter %s expects one argument") % n)
216 raise error.ParseError(_("filter %s expects one argument") % n)
217 f = context._filters[n]
217 f = context._filters[n]
218 return (runfilter, (args[0][0], args[0][1], f))
218 return (runfilter, (args[0][0], args[0][1], f))
219 raise error.ParseError(_("unknown function '%s'") % n)
219 raise error.ParseError(_("unknown function '%s'") % n)
220
220
221 def date(context, mapping, args):
221 def date(context, mapping, args):
222 if not (1 <= len(args) <= 2):
222 if not (1 <= len(args) <= 2):
223 # i18n: "date" is a keyword
223 # i18n: "date" is a keyword
224 raise error.ParseError(_("date expects one or two arguments"))
224 raise error.ParseError(_("date expects one or two arguments"))
225
225
226 date = args[0][0](context, mapping, args[0][1])
226 date = args[0][0](context, mapping, args[0][1])
227 if len(args) == 2:
227 if len(args) == 2:
228 fmt = stringify(args[1][0](context, mapping, args[1][1]))
228 fmt = stringify(args[1][0](context, mapping, args[1][1]))
229 return util.datestr(date, fmt)
229 return util.datestr(date, fmt)
230 return util.datestr(date)
230 return util.datestr(date)
231
231
232 def diff(context, mapping, args):
232 def diff(context, mapping, args):
233 if len(args) > 2:
233 if len(args) > 2:
234 # i18n: "diff" is a keyword
234 # i18n: "diff" is a keyword
235 raise error.ParseError(_("diff expects one, two or no arguments"))
235 raise error.ParseError(_("diff expects one, two or no arguments"))
236
236
237 def getpatterns(i):
237 def getpatterns(i):
238 if i < len(args):
238 if i < len(args):
239 s = args[i][1].strip()
239 s = args[i][1].strip()
240 if s:
240 if s:
241 return [s]
241 return [s]
242 return []
242 return []
243
243
244 ctx = mapping['ctx']
244 ctx = mapping['ctx']
245 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
245 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
246
246
247 return ''.join(chunks)
247 return ''.join(chunks)
248
248
249 def fill(context, mapping, args):
249 def fill(context, mapping, args):
250 if not (1 <= len(args) <= 4):
250 if not (1 <= len(args) <= 4):
251 # i18n: "fill" is a keyword
251 # i18n: "fill" is a keyword
252 raise error.ParseError(_("fill expects one to four arguments"))
252 raise error.ParseError(_("fill expects one to four arguments"))
253
253
254 text = stringify(args[0][0](context, mapping, args[0][1]))
254 text = stringify(args[0][0](context, mapping, args[0][1]))
255 width = 76
255 width = 76
256 initindent = ''
256 initindent = ''
257 hangindent = ''
257 hangindent = ''
258 if 2 <= len(args) <= 4:
258 if 2 <= len(args) <= 4:
259 try:
259 try:
260 width = int(stringify(args[1][0](context, mapping, args[1][1])))
260 width = int(stringify(args[1][0](context, mapping, args[1][1])))
261 except ValueError:
261 except ValueError:
262 # i18n: "fill" is a keyword
262 # i18n: "fill" is a keyword
263 raise error.ParseError(_("fill expects an integer width"))
263 raise error.ParseError(_("fill expects an integer width"))
264 try:
264 try:
265 initindent = stringify(_evalifliteral(args[2], context, mapping))
265 initindent = stringify(_evalifliteral(args[2], context, mapping))
266 hangindent = stringify(_evalifliteral(args[3], context, mapping))
266 hangindent = stringify(_evalifliteral(args[3], context, mapping))
267 except IndexError:
267 except IndexError:
268 pass
268 pass
269
269
270 return templatefilters.fill(text, width, initindent, hangindent)
270 return templatefilters.fill(text, width, initindent, hangindent)
271
271
272 def pad(context, mapping, args):
272 def pad(context, mapping, args):
273 """usage: pad(text, width, fillchar=' ', right=False)
273 """usage: pad(text, width, fillchar=' ', right=False)
274 """
274 """
275 if not (2 <= len(args) <= 4):
275 if not (2 <= len(args) <= 4):
276 # i18n: "pad" is a keyword
276 # i18n: "pad" is a keyword
277 raise error.ParseError(_("pad() expects two to four arguments"))
277 raise error.ParseError(_("pad() expects two to four arguments"))
278
278
279 width = int(args[1][1])
279 width = int(args[1][1])
280
280
281 text = stringify(args[0][0](context, mapping, args[0][1]))
281 text = stringify(args[0][0](context, mapping, args[0][1]))
282 if args[0][0] == runstring:
282 if args[0][0] == runstring:
283 text = stringify(runtemplate(context, mapping,
283 text = stringify(runtemplate(context, mapping,
284 compiletemplate(text, context)))
284 compiletemplate(text, context)))
285
285
286 right = False
286 right = False
287 fillchar = ' '
287 fillchar = ' '
288 if len(args) > 2:
288 if len(args) > 2:
289 fillchar = stringify(args[2][0](context, mapping, args[2][1]))
289 fillchar = stringify(args[2][0](context, mapping, args[2][1]))
290 if len(args) > 3:
290 if len(args) > 3:
291 right = util.parsebool(args[3][1])
291 right = util.parsebool(args[3][1])
292
292
293 if right:
293 if right:
294 return text.rjust(width, fillchar)
294 return text.rjust(width, fillchar)
295 else:
295 else:
296 return text.ljust(width, fillchar)
296 return text.ljust(width, fillchar)
297
297
298 def get(context, mapping, args):
298 def get(context, mapping, args):
299 if len(args) != 2:
299 if len(args) != 2:
300 # i18n: "get" is a keyword
300 # i18n: "get" is a keyword
301 raise error.ParseError(_("get() expects two arguments"))
301 raise error.ParseError(_("get() expects two arguments"))
302
302
303 dictarg = args[0][0](context, mapping, args[0][1])
303 dictarg = args[0][0](context, mapping, args[0][1])
304 if not util.safehasattr(dictarg, 'get'):
304 if not util.safehasattr(dictarg, 'get'):
305 # i18n: "get" is a keyword
305 # i18n: "get" is a keyword
306 raise error.ParseError(_("get() expects a dict as first argument"))
306 raise error.ParseError(_("get() expects a dict as first argument"))
307
307
308 key = args[1][0](context, mapping, args[1][1])
308 key = args[1][0](context, mapping, args[1][1])
309 yield dictarg.get(key)
309 yield dictarg.get(key)
310
310
311 def _evalifliteral(arg, context, mapping):
311 def _evalifliteral(arg, context, mapping):
312 t = stringify(arg[0](context, mapping, arg[1]))
312 t = stringify(arg[0](context, mapping, arg[1]))
313 if arg[0] == runstring or arg[0] == runrawstring:
313 if arg[0] == runstring or arg[0] == runrawstring:
314 yield runtemplate(context, mapping,
314 yield runtemplate(context, mapping,
315 compiletemplate(t, context, strtoken='rawstring'))
315 compiletemplate(t, context, strtoken='rawstring'))
316 else:
316 else:
317 yield t
317 yield t
318
318
319 def if_(context, mapping, args):
319 def if_(context, mapping, args):
320 if not (2 <= len(args) <= 3):
320 if not (2 <= len(args) <= 3):
321 # i18n: "if" is a keyword
321 # i18n: "if" is a keyword
322 raise error.ParseError(_("if expects two or three arguments"))
322 raise error.ParseError(_("if expects two or three arguments"))
323
323
324 test = stringify(args[0][0](context, mapping, args[0][1]))
324 test = stringify(args[0][0](context, mapping, args[0][1]))
325 if test:
325 if test:
326 yield _evalifliteral(args[1], context, mapping)
326 yield _evalifliteral(args[1], context, mapping)
327 elif len(args) == 3:
327 elif len(args) == 3:
328 yield _evalifliteral(args[2], context, mapping)
328 yield _evalifliteral(args[2], context, mapping)
329
329
330 def ifcontains(context, mapping, args):
330 def ifcontains(context, mapping, args):
331 if not (3 <= len(args) <= 4):
331 if not (3 <= len(args) <= 4):
332 # i18n: "ifcontains" is a keyword
332 # i18n: "ifcontains" is a keyword
333 raise error.ParseError(_("ifcontains expects three or four arguments"))
333 raise error.ParseError(_("ifcontains expects three or four arguments"))
334
334
335 item = stringify(args[0][0](context, mapping, args[0][1]))
335 item = stringify(args[0][0](context, mapping, args[0][1]))
336 items = args[1][0](context, mapping, args[1][1])
336 items = args[1][0](context, mapping, args[1][1])
337
337
338 if item in items:
338 if item in items:
339 yield _evalifliteral(args[2], context, mapping)
339 yield _evalifliteral(args[2], context, mapping)
340 elif len(args) == 4:
340 elif len(args) == 4:
341 yield _evalifliteral(args[3], context, mapping)
341 yield _evalifliteral(args[3], context, mapping)
342
342
343 def ifeq(context, mapping, args):
343 def ifeq(context, mapping, args):
344 if not (3 <= len(args) <= 4):
344 if not (3 <= len(args) <= 4):
345 # i18n: "ifeq" is a keyword
345 # i18n: "ifeq" is a keyword
346 raise error.ParseError(_("ifeq expects three or four arguments"))
346 raise error.ParseError(_("ifeq expects three or four arguments"))
347
347
348 test = stringify(args[0][0](context, mapping, args[0][1]))
348 test = stringify(args[0][0](context, mapping, args[0][1]))
349 match = stringify(args[1][0](context, mapping, args[1][1]))
349 match = stringify(args[1][0](context, mapping, args[1][1]))
350 if test == match:
350 if test == match:
351 yield _evalifliteral(args[2], context, mapping)
351 yield _evalifliteral(args[2], context, mapping)
352 elif len(args) == 4:
352 elif len(args) == 4:
353 yield _evalifliteral(args[3], context, mapping)
353 yield _evalifliteral(args[3], context, mapping)
354
354
355 def join(context, mapping, args):
355 def join(context, mapping, args):
356 if not (1 <= len(args) <= 2):
356 if not (1 <= len(args) <= 2):
357 # i18n: "join" is a keyword
357 # i18n: "join" is a keyword
358 raise error.ParseError(_("join expects one or two arguments"))
358 raise error.ParseError(_("join expects one or two arguments"))
359
359
360 joinset = args[0][0](context, mapping, args[0][1])
360 joinset = args[0][0](context, mapping, args[0][1])
361 if callable(joinset):
361 if callable(joinset):
362 jf = joinset.joinfmt
362 jf = joinset.joinfmt
363 joinset = [jf(x) for x in joinset()]
363 joinset = [jf(x) for x in joinset()]
364
364
365 joiner = " "
365 joiner = " "
366 if len(args) > 1:
366 if len(args) > 1:
367 joiner = stringify(args[1][0](context, mapping, args[1][1]))
367 joiner = stringify(args[1][0](context, mapping, args[1][1]))
368
368
369 first = True
369 first = True
370 for x in joinset:
370 for x in joinset:
371 if first:
371 if first:
372 first = False
372 first = False
373 else:
373 else:
374 yield joiner
374 yield joiner
375 yield x
375 yield x
376
376
377 def label(context, mapping, args):
377 def label(context, mapping, args):
378 if len(args) != 2:
378 if len(args) != 2:
379 # i18n: "label" is a keyword
379 # i18n: "label" is a keyword
380 raise error.ParseError(_("label expects two arguments"))
380 raise error.ParseError(_("label expects two arguments"))
381
381
382 # ignore args[0] (the label string) since this is supposed to be a a no-op
382 # ignore args[0] (the label string) since this is supposed to be a a no-op
383 yield _evalifliteral(args[1], context, mapping)
383 yield _evalifliteral(args[1], context, mapping)
384
384
385 def revset(context, mapping, args):
385 def revset(context, mapping, args):
386 """usage: revset(query[, formatargs...])
386 """usage: revset(query[, formatargs...])
387 """
387 """
388 if not len(args) > 0:
388 if not len(args) > 0:
389 # i18n: "revset" is a keyword
389 # i18n: "revset" is a keyword
390 raise error.ParseError(_("revset expects one or more arguments"))
390 raise error.ParseError(_("revset expects one or more arguments"))
391
391
392 raw = args[0][1]
392 raw = args[0][1]
393 ctx = mapping['ctx']
393 ctx = mapping['ctx']
394 repo = ctx._repo
394 repo = ctx._repo
395
395
396 def query(expr):
396 def query(expr):
397 m = revsetmod.match(repo.ui, expr)
397 m = revsetmod.match(repo.ui, expr)
398 return m(repo)
398 return m(repo)
399
399
400 if len(args) > 1:
400 if len(args) > 1:
401 formatargs = list([a[0](context, mapping, a[1]) for a in args[1:]])
401 formatargs = list([a[0](context, mapping, a[1]) for a in args[1:]])
402 revs = query(revsetmod.formatspec(raw, *formatargs))
402 revs = query(revsetmod.formatspec(raw, *formatargs))
403 revs = list([str(r) for r in revs])
403 revs = list([str(r) for r in revs])
404 else:
404 else:
405 revsetcache = mapping['cache'].setdefault("revsetcache", {})
405 revsetcache = mapping['cache'].setdefault("revsetcache", {})
406 if raw in revsetcache:
406 if raw in revsetcache:
407 revs = revsetcache[raw]
407 revs = revsetcache[raw]
408 else:
408 else:
409 revs = query(raw)
409 revs = query(raw)
410 revs = list([str(r) for r in revs])
410 revs = list([str(r) for r in revs])
411 revsetcache[raw] = revs
411 revsetcache[raw] = revs
412
412
413 return templatekw.showlist("revision", revs, **mapping)
413 return templatekw.showlist("revision", revs, **mapping)
414
414
415 def rstdoc(context, mapping, args):
415 def rstdoc(context, mapping, args):
416 if len(args) != 2:
416 if len(args) != 2:
417 # i18n: "rstdoc" is a keyword
417 # i18n: "rstdoc" is a keyword
418 raise error.ParseError(_("rstdoc expects two arguments"))
418 raise error.ParseError(_("rstdoc expects two arguments"))
419
419
420 text = stringify(args[0][0](context, mapping, args[0][1]))
420 text = stringify(args[0][0](context, mapping, args[0][1]))
421 style = stringify(args[1][0](context, mapping, args[1][1]))
421 style = stringify(args[1][0](context, mapping, args[1][1]))
422
422
423 return minirst.format(text, style=style, keep=['verbose'])
423 return minirst.format(text, style=style, keep=['verbose'])
424
424
425 def shortest(context, mapping, args):
425 def shortest(context, mapping, args):
426 """usage: shortest(node, minlength=4)
426 """usage: shortest(node, minlength=4)
427 """
427 """
428 if not (1 <= len(args) <= 2):
428 if not (1 <= len(args) <= 2):
429 # i18n: "shortest" is a keyword
429 # i18n: "shortest" is a keyword
430 raise error.ParseError(_("shortest() expects one or two arguments"))
430 raise error.ParseError(_("shortest() expects one or two arguments"))
431
431
432 node = stringify(args[0][0](context, mapping, args[0][1]))
432 node = stringify(args[0][0](context, mapping, args[0][1]))
433
433
434 minlength = 4
434 minlength = 4
435 if len(args) > 1:
435 if len(args) > 1:
436 minlength = int(args[1][1])
436 minlength = int(args[1][1])
437
437
438 cl = mapping['ctx']._repo.changelog
438 cl = mapping['ctx']._repo.changelog
439 def isvalid(test):
439 def isvalid(test):
440 try:
440 try:
441 try:
441 try:
442 cl.index.partialmatch(test)
442 cl.index.partialmatch(test)
443 except AttributeError:
443 except AttributeError:
444 # Pure mercurial doesn't support partialmatch on the index.
444 # Pure mercurial doesn't support partialmatch on the index.
445 # Fallback to the slow way.
445 # Fallback to the slow way.
446 if cl._partialmatch(test) is None:
446 if cl._partialmatch(test) is None:
447 return False
447 return False
448
448
449 try:
449 try:
450 i = int(test)
450 i = int(test)
451 # if we are a pure int, then starting with zero will not be
451 # if we are a pure int, then starting with zero will not be
452 # confused as a rev; or, obviously, if the int is larger than
452 # confused as a rev; or, obviously, if the int is larger than
453 # the value of the tip rev
453 # the value of the tip rev
454 if test[0] == '0' or i > len(cl):
454 if test[0] == '0' or i > len(cl):
455 return True
455 return True
456 return False
456 return False
457 except ValueError:
457 except ValueError:
458 return True
458 return True
459 except error.RevlogError:
459 except error.RevlogError:
460 return False
460 return False
461
461
462 shortest = node
462 shortest = node
463 startlength = max(6, minlength)
463 startlength = max(6, minlength)
464 length = startlength
464 length = startlength
465 while True:
465 while True:
466 test = node[:length]
466 test = node[:length]
467 if isvalid(test):
467 if isvalid(test):
468 shortest = test
468 shortest = test
469 if length == minlength or length > startlength:
469 if length == minlength or length > startlength:
470 return shortest
470 return shortest
471 length -= 1
471 length -= 1
472 else:
472 else:
473 length += 1
473 length += 1
474 if len(shortest) <= length:
474 if len(shortest) <= length:
475 return shortest
475 return shortest
476
476
477 def strip(context, mapping, args):
477 def strip(context, mapping, args):
478 if not (1 <= len(args) <= 2):
478 if not (1 <= len(args) <= 2):
479 # i18n: "strip" is a keyword
479 # i18n: "strip" is a keyword
480 raise error.ParseError(_("strip expects one or two arguments"))
480 raise error.ParseError(_("strip expects one or two arguments"))
481
481
482 text = stringify(args[0][0](context, mapping, args[0][1]))
482 text = stringify(args[0][0](context, mapping, args[0][1]))
483 if len(args) == 2:
483 if len(args) == 2:
484 chars = stringify(args[1][0](context, mapping, args[1][1]))
484 chars = stringify(args[1][0](context, mapping, args[1][1]))
485 return text.strip(chars)
485 return text.strip(chars)
486 return text.strip()
486 return text.strip()
487
487
488 def sub(context, mapping, args):
488 def sub(context, mapping, args):
489 if len(args) != 3:
489 if len(args) != 3:
490 # i18n: "sub" is a keyword
490 # i18n: "sub" is a keyword
491 raise error.ParseError(_("sub expects three arguments"))
491 raise error.ParseError(_("sub expects three arguments"))
492
492
493 pat = stringify(args[0][0](context, mapping, args[0][1]))
493 pat = stringify(args[0][0](context, mapping, args[0][1]))
494 rpl = stringify(args[1][0](context, mapping, args[1][1]))
494 rpl = stringify(args[1][0](context, mapping, args[1][1]))
495 src = stringify(_evalifliteral(args[2], context, mapping))
495 src = stringify(_evalifliteral(args[2], context, mapping))
496 yield re.sub(pat, rpl, src)
496 yield re.sub(pat, rpl, src)
497
497
498 def startswith(context, mapping, args):
498 def startswith(context, mapping, args):
499 if len(args) != 2:
499 if len(args) != 2:
500 # i18n: "startswith" is a keyword
500 # i18n: "startswith" is a keyword
501 raise error.ParseError(_("startswith expects two arguments"))
501 raise error.ParseError(_("startswith expects two arguments"))
502
502
503 patn = stringify(args[0][0](context, mapping, args[0][1]))
503 patn = stringify(args[0][0](context, mapping, args[0][1]))
504 text = stringify(args[1][0](context, mapping, args[1][1]))
504 text = stringify(args[1][0](context, mapping, args[1][1]))
505 if text.startswith(patn):
505 if text.startswith(patn):
506 return text
506 return text
507 return ''
507 return ''
508
508
509
509
510 def word(context, mapping, args):
510 def word(context, mapping, args):
511 """return nth word from a string"""
511 """return nth word from a string"""
512 if not (2 <= len(args) <= 3):
512 if not (2 <= len(args) <= 3):
513 # i18n: "word" is a keyword
513 # i18n: "word" is a keyword
514 raise error.ParseError(_("word expects two or three arguments, got %d")
514 raise error.ParseError(_("word expects two or three arguments, got %d")
515 % len(args))
515 % len(args))
516
516
517 num = int(stringify(args[0][0](context, mapping, args[0][1])))
517 num = int(stringify(args[0][0](context, mapping, args[0][1])))
518 text = stringify(args[1][0](context, mapping, args[1][1]))
518 text = stringify(args[1][0](context, mapping, args[1][1]))
519 if len(args) == 3:
519 if len(args) == 3:
520 splitter = stringify(args[2][0](context, mapping, args[2][1]))
520 splitter = stringify(args[2][0](context, mapping, args[2][1]))
521 else:
521 else:
522 splitter = None
522 splitter = None
523
523
524 tokens = text.split(splitter)
524 tokens = text.split(splitter)
525 if num >= len(tokens):
525 if num >= len(tokens):
526 return ''
526 return ''
527 else:
527 else:
528 return tokens[num]
528 return tokens[num]
529
529
530 methods = {
530 methods = {
531 "string": lambda e, c: (runstring, e[1]),
531 "string": lambda e, c: (runstring, e[1]),
532 "rawstring": lambda e, c: (runrawstring, e[1]),
532 "rawstring": lambda e, c: (runrawstring, e[1]),
533 "symbol": lambda e, c: (runsymbol, e[1]),
533 "symbol": lambda e, c: (runsymbol, e[1]),
534 "group": lambda e, c: compileexp(e[1], c),
534 "group": lambda e, c: compileexp(e[1], c),
535 # ".": buildmember,
535 # ".": buildmember,
536 "|": buildfilter,
536 "|": buildfilter,
537 "%": buildmap,
537 "%": buildmap,
538 "func": buildfunc,
538 "func": buildfunc,
539 }
539 }
540
540
541 funcs = {
541 funcs = {
542 "date": date,
542 "date": date,
543 "diff": diff,
543 "diff": diff,
544 "fill": fill,
544 "fill": fill,
545 "get": get,
545 "get": get,
546 "if": if_,
546 "if": if_,
547 "ifcontains": ifcontains,
547 "ifcontains": ifcontains,
548 "ifeq": ifeq,
548 "ifeq": ifeq,
549 "join": join,
549 "join": join,
550 "label": label,
550 "label": label,
551 "pad": pad,
551 "pad": pad,
552 "revset": revset,
552 "revset": revset,
553 "rstdoc": rstdoc,
553 "rstdoc": rstdoc,
554 "shortest": shortest,
554 "shortest": shortest,
555 "startswith": startswith,
555 "startswith": startswith,
556 "strip": strip,
556 "strip": strip,
557 "sub": sub,
557 "sub": sub,
558 "word": word,
558 "word": word,
559 }
559 }
560
560
561 # template engine
561 # template engine
562
562
563 stringify = templatefilters.stringify
563 stringify = templatefilters.stringify
564
564
565 def _flatten(thing):
565 def _flatten(thing):
566 '''yield a single stream from a possibly nested set of iterators'''
566 '''yield a single stream from a possibly nested set of iterators'''
567 if isinstance(thing, str):
567 if isinstance(thing, str):
568 yield thing
568 yield thing
569 elif not util.safehasattr(thing, '__iter__'):
569 elif not util.safehasattr(thing, '__iter__'):
570 if thing is not None:
570 if thing is not None:
571 yield str(thing)
571 yield str(thing)
572 else:
572 else:
573 for i in thing:
573 for i in thing:
574 if isinstance(i, str):
574 if isinstance(i, str):
575 yield i
575 yield i
576 elif not util.safehasattr(i, '__iter__'):
576 elif not util.safehasattr(i, '__iter__'):
577 if i is not None:
577 if i is not None:
578 yield str(i)
578 yield str(i)
579 elif i is not None:
579 elif i is not None:
580 for j in _flatten(i):
580 for j in _flatten(i):
581 yield j
581 yield j
582
582
583 def parsestring(s, quoted=True):
583 def parsestring(s, quoted=True):
584 '''parse a string using simple c-like syntax.
584 '''parse a string using simple c-like syntax.
585 string must be in quotes if quoted is True.'''
585 string must be in quotes if quoted is True.'''
586 if quoted:
586 if quoted:
587 if len(s) < 2 or s[0] != s[-1]:
587 if len(s) < 2 or s[0] != s[-1]:
588 raise SyntaxError(_('unmatched quotes'))
588 raise SyntaxError(_('unmatched quotes'))
589 return s[1:-1].decode('string_escape')
589 return s[1:-1].decode('string_escape')
590
590
591 return s.decode('string_escape')
591 return s.decode('string_escape')
592
592
593 class engine(object):
593 class engine(object):
594 '''template expansion engine.
594 '''template expansion engine.
595
595
596 template expansion works like this. a map file contains key=value
596 template expansion works like this. a map file contains key=value
597 pairs. if value is quoted, it is treated as string. otherwise, it
597 pairs. if value is quoted, it is treated as string. otherwise, it
598 is treated as name of template file.
598 is treated as name of template file.
599
599
600 templater is asked to expand a key in map. it looks up key, and
600 templater is asked to expand a key in map. it looks up key, and
601 looks for strings like this: {foo}. it expands {foo} by looking up
601 looks for strings like this: {foo}. it expands {foo} by looking up
602 foo in map, and substituting it. expansion is recursive: it stops
602 foo in map, and substituting it. expansion is recursive: it stops
603 when there is no more {foo} to replace.
603 when there is no more {foo} to replace.
604
604
605 expansion also allows formatting and filtering.
605 expansion also allows formatting and filtering.
606
606
607 format uses key to expand each item in list. syntax is
607 format uses key to expand each item in list. syntax is
608 {key%format}.
608 {key%format}.
609
609
610 filter uses function to transform value. syntax is
610 filter uses function to transform value. syntax is
611 {key|filter1|filter2|...}.'''
611 {key|filter1|filter2|...}.'''
612
612
613 def __init__(self, loader, filters={}, defaults={}):
613 def __init__(self, loader, filters={}, defaults={}):
614 self._loader = loader
614 self._loader = loader
615 self._filters = filters
615 self._filters = filters
616 self._defaults = defaults
616 self._defaults = defaults
617 self._cache = {}
617 self._cache = {}
618
618
619 def _load(self, t):
619 def _load(self, t):
620 '''load, parse, and cache a template'''
620 '''load, parse, and cache a template'''
621 if t not in self._cache:
621 if t not in self._cache:
622 self._cache[t] = compiletemplate(self._loader(t), self)
622 self._cache[t] = compiletemplate(self._loader(t), self)
623 return self._cache[t]
623 return self._cache[t]
624
624
625 def process(self, t, mapping):
625 def process(self, t, mapping):
626 '''Perform expansion. t is name of map element to expand.
626 '''Perform expansion. t is name of map element to expand.
627 mapping contains added elements for use during expansion. Is a
627 mapping contains added elements for use during expansion. Is a
628 generator.'''
628 generator.'''
629 return _flatten(runtemplate(self, mapping, self._load(t)))
629 return _flatten(runtemplate(self, mapping, self._load(t)))
630
630
631 engines = {'default': engine}
631 engines = {'default': engine}
632
632
633 def stylelist():
633 def stylelist():
634 paths = templatepaths()
634 paths = templatepaths()
635 if not paths:
635 if not paths:
636 return _('no templates found, try `hg debuginstall` for more info')
636 return _('no templates found, try `hg debuginstall` for more info')
637 dirlist = os.listdir(paths[0])
637 dirlist = os.listdir(paths[0])
638 stylelist = []
638 stylelist = []
639 for file in dirlist:
639 for file in dirlist:
640 split = file.split(".")
640 split = file.split(".")
641 if split[0] == "map-cmdline":
641 if split[0] == "map-cmdline":
642 stylelist.append(split[1])
642 stylelist.append(split[1])
643 return ", ".join(sorted(stylelist))
643 return ", ".join(sorted(stylelist))
644
644
645 class TemplateNotFound(util.Abort):
645 class TemplateNotFound(util.Abort):
646 pass
646 pass
647
647
648 class templater(object):
648 class templater(object):
649
649
650 def __init__(self, mapfile, filters={}, defaults={}, cache={},
650 def __init__(self, mapfile, filters={}, defaults={}, cache={},
651 minchunk=1024, maxchunk=65536):
651 minchunk=1024, maxchunk=65536):
652 '''set up template engine.
652 '''set up template engine.
653 mapfile is name of file to read map definitions from.
653 mapfile is name of file to read map definitions from.
654 filters is dict of functions. each transforms a value into another.
654 filters is dict of functions. each transforms a value into another.
655 defaults is dict of default map definitions.'''
655 defaults is dict of default map definitions.'''
656 self.mapfile = mapfile or 'template'
656 self.mapfile = mapfile or 'template'
657 self.cache = cache.copy()
657 self.cache = cache.copy()
658 self.map = {}
658 self.map = {}
659 self.base = (mapfile and os.path.dirname(mapfile)) or ''
659 self.base = (mapfile and os.path.dirname(mapfile)) or ''
660 self.filters = templatefilters.filters.copy()
660 self.filters = templatefilters.filters.copy()
661 self.filters.update(filters)
661 self.filters.update(filters)
662 self.defaults = defaults
662 self.defaults = defaults
663 self.minchunk, self.maxchunk = minchunk, maxchunk
663 self.minchunk, self.maxchunk = minchunk, maxchunk
664 self.ecache = {}
664 self.ecache = {}
665
665
666 if not mapfile:
666 if not mapfile:
667 return
667 return
668 if not os.path.exists(mapfile):
668 if not os.path.exists(mapfile):
669 raise util.Abort(_("style '%s' not found") % mapfile,
669 raise util.Abort(_("style '%s' not found") % mapfile,
670 hint=_("available styles: %s") % stylelist())
670 hint=_("available styles: %s") % stylelist())
671
671
672 conf = config.config()
672 conf = config.config()
673 conf.read(mapfile)
673 conf.read(mapfile)
674
674
675 for key, val in conf[''].items():
675 for key, val in conf[''].items():
676 if not val:
676 if not val:
677 raise SyntaxError(_('%s: missing value') % conf.source('', key))
677 raise SyntaxError(_('%s: missing value') % conf.source('', key))
678 if val[0] in "'\"":
678 if val[0] in "'\"":
679 try:
679 try:
680 self.cache[key] = parsestring(val)
680 self.cache[key] = parsestring(val)
681 except SyntaxError, inst:
681 except SyntaxError, inst:
682 raise SyntaxError('%s: %s' %
682 raise SyntaxError('%s: %s' %
683 (conf.source('', key), inst.args[0]))
683 (conf.source('', key), inst.args[0]))
684 else:
684 else:
685 val = 'default', val
685 val = 'default', val
686 if ':' in val[1]:
686 if ':' in val[1]:
687 val = val[1].split(':', 1)
687 val = val[1].split(':', 1)
688 self.map[key] = val[0], os.path.join(self.base, val[1])
688 self.map[key] = val[0], os.path.join(self.base, val[1])
689
689
690 def __contains__(self, key):
690 def __contains__(self, key):
691 return key in self.cache or key in self.map
691 return key in self.cache or key in self.map
692
692
693 def load(self, t):
693 def load(self, t):
694 '''Get the template for the given template name. Use a local cache.'''
694 '''Get the template for the given template name. Use a local cache.'''
695 if t not in self.cache:
695 if t not in self.cache:
696 try:
696 try:
697 self.cache[t] = util.readfile(self.map[t][1])
697 self.cache[t] = util.readfile(self.map[t][1])
698 except KeyError, inst:
698 except KeyError, inst:
699 raise TemplateNotFound(_('"%s" not in template map') %
699 raise TemplateNotFound(_('"%s" not in template map') %
700 inst.args[0])
700 inst.args[0])
701 except IOError, inst:
701 except IOError, inst:
702 raise IOError(inst.args[0], _('template file %s: %s') %
702 raise IOError(inst.args[0], _('template file %s: %s') %
703 (self.map[t][1], inst.args[1]))
703 (self.map[t][1], inst.args[1]))
704 return self.cache[t]
704 return self.cache[t]
705
705
706 def __call__(self, t, **mapping):
706 def __call__(self, t, **mapping):
707 ttype = t in self.map and self.map[t][0] or 'default'
707 ttype = t in self.map and self.map[t][0] or 'default'
708 if ttype not in self.ecache:
708 if ttype not in self.ecache:
709 self.ecache[ttype] = engines[ttype](self.load,
709 self.ecache[ttype] = engines[ttype](self.load,
710 self.filters, self.defaults)
710 self.filters, self.defaults)
711 proc = self.ecache[ttype]
711 proc = self.ecache[ttype]
712
712
713 stream = proc.process(t, mapping)
713 stream = proc.process(t, mapping)
714 if self.minchunk:
714 if self.minchunk:
715 stream = util.increasingchunks(stream, min=self.minchunk,
715 stream = util.increasingchunks(stream, min=self.minchunk,
716 max=self.maxchunk)
716 max=self.maxchunk)
717 return stream
717 return stream
718
718
719 def templatepaths():
719 def templatepaths():
720 '''return locations used for template files.'''
720 '''return locations used for template files.'''
721 pathsrel = ['templates']
721 pathsrel = ['templates']
722 paths = [os.path.normpath(os.path.join(util.datapath, f))
722 paths = [os.path.normpath(os.path.join(util.datapath, f))
723 for f in pathsrel]
723 for f in pathsrel]
724 return [p for p in paths if os.path.isdir(p)]
724 return [p for p in paths if os.path.isdir(p)]
725
725
726 def templatepath(name):
726 def templatepath(name):
727 '''return location of template file. returns None if not found.'''
727 '''return location of template file. returns None if not found.'''
728 for p in templatepaths():
728 for p in templatepaths():
729 f = os.path.join(p, name)
729 f = os.path.join(p, name)
730 if os.path.exists(f):
730 if os.path.exists(f):
731 return f
731 return f
732 return None
732 return None
733
733
734 def stylemap(styles, paths=None):
734 def stylemap(styles, paths=None):
735 """Return path to mapfile for a given style.
735 """Return path to mapfile for a given style.
736
736
737 Searches mapfile in the following locations:
737 Searches mapfile in the following locations:
738 1. templatepath/style/map
738 1. templatepath/style/map
739 2. templatepath/map-style
739 2. templatepath/map-style
740 3. templatepath/map
740 3. templatepath/map
741 """
741 """
742
742
743 if paths is None:
743 if paths is None:
744 paths = templatepaths()
744 paths = templatepaths()
745 elif isinstance(paths, str):
745 elif isinstance(paths, str):
746 paths = [paths]
746 paths = [paths]
747
747
748 if isinstance(styles, str):
748 if isinstance(styles, str):
749 styles = [styles]
749 styles = [styles]
750
750
751 for style in styles:
751 for style in styles:
752 if not style:
752 # only plain name is allowed to honor template paths
753 if (not style
754 or style in (os.curdir, os.pardir)
755 or os.sep in style
756 or os.altsep and os.altsep in style):
753 continue
757 continue
754 locations = [os.path.join(style, 'map'), 'map-' + style]
758 locations = [os.path.join(style, 'map'), 'map-' + style]
755 locations.append('map')
759 locations.append('map')
756
760
757 for path in paths:
761 for path in paths:
758 for location in locations:
762 for location in locations:
759 mapfile = os.path.join(path, location)
763 mapfile = os.path.join(path, location)
760 if os.path.isfile(mapfile):
764 if os.path.isfile(mapfile):
761 return style, mapfile
765 return style, mapfile
762
766
763 raise RuntimeError("No hgweb templates found in %r" % paths)
767 raise RuntimeError("No hgweb templates found in %r" % paths)
@@ -1,617 +1,656
1 #require serve
1 #require serve
2
2
3 Some tests for hgweb. Tests static files, plain files and different 404's.
3 Some tests for hgweb. Tests static files, plain files and different 404's.
4
4
5 $ hg init test
5 $ hg init test
6 $ cd test
6 $ cd test
7 $ mkdir da
7 $ mkdir da
8 $ echo foo > da/foo
8 $ echo foo > da/foo
9 $ echo foo > foo
9 $ echo foo > foo
10 $ hg ci -Ambase
10 $ hg ci -Ambase
11 adding da/foo
11 adding da/foo
12 adding foo
12 adding foo
13 $ hg serve -n test -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
13 $ hg serve -n test -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
14 $ cat hg.pid >> $DAEMON_PIDS
14 $ cat hg.pid >> $DAEMON_PIDS
15
15
16 manifest
16 manifest
17
17
18 $ ("$TESTDIR/get-with-headers.py" localhost:$HGPORT 'file/tip/?style=raw')
18 $ ("$TESTDIR/get-with-headers.py" localhost:$HGPORT 'file/tip/?style=raw')
19 200 Script output follows
19 200 Script output follows
20
20
21
21
22 drwxr-xr-x da
22 drwxr-xr-x da
23 -rw-r--r-- 4 foo
23 -rw-r--r-- 4 foo
24
24
25
25
26 $ ("$TESTDIR/get-with-headers.py" localhost:$HGPORT 'file/tip/da?style=raw')
26 $ ("$TESTDIR/get-with-headers.py" localhost:$HGPORT 'file/tip/da?style=raw')
27 200 Script output follows
27 200 Script output follows
28
28
29
29
30 -rw-r--r-- 4 foo
30 -rw-r--r-- 4 foo
31
31
32
32
33
33
34 plain file
34 plain file
35
35
36 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'file/tip/foo?style=raw'
36 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'file/tip/foo?style=raw'
37 200 Script output follows
37 200 Script output follows
38
38
39 foo
39 foo
40
40
41 should give a 404 - static file that does not exist
41 should give a 404 - static file that does not exist
42
42
43 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'static/bogus'
43 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'static/bogus'
44 404 Not Found
44 404 Not Found
45
45
46 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
46 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
47 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
47 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
48 <head>
48 <head>
49 <link rel="icon" href="/static/hgicon.png" type="image/png" />
49 <link rel="icon" href="/static/hgicon.png" type="image/png" />
50 <meta name="robots" content="index, nofollow" />
50 <meta name="robots" content="index, nofollow" />
51 <link rel="stylesheet" href="/static/style-paper.css" type="text/css" />
51 <link rel="stylesheet" href="/static/style-paper.css" type="text/css" />
52 <script type="text/javascript" src="/static/mercurial.js"></script>
52 <script type="text/javascript" src="/static/mercurial.js"></script>
53
53
54 <title>test: error</title>
54 <title>test: error</title>
55 </head>
55 </head>
56 <body>
56 <body>
57
57
58 <div class="container">
58 <div class="container">
59 <div class="menu">
59 <div class="menu">
60 <div class="logo">
60 <div class="logo">
61 <a href="http://mercurial.selenic.com/">
61 <a href="http://mercurial.selenic.com/">
62 <img src="/static/hglogo.png" width=75 height=90 border=0 alt="mercurial" /></a>
62 <img src="/static/hglogo.png" width=75 height=90 border=0 alt="mercurial" /></a>
63 </div>
63 </div>
64 <ul>
64 <ul>
65 <li><a href="/shortlog">log</a></li>
65 <li><a href="/shortlog">log</a></li>
66 <li><a href="/graph">graph</a></li>
66 <li><a href="/graph">graph</a></li>
67 <li><a href="/tags">tags</a></li>
67 <li><a href="/tags">tags</a></li>
68 <li><a href="/bookmarks">bookmarks</a></li>
68 <li><a href="/bookmarks">bookmarks</a></li>
69 <li><a href="/branches">branches</a></li>
69 <li><a href="/branches">branches</a></li>
70 </ul>
70 </ul>
71 <ul>
71 <ul>
72 <li><a href="/help">help</a></li>
72 <li><a href="/help">help</a></li>
73 </ul>
73 </ul>
74 </div>
74 </div>
75
75
76 <div class="main">
76 <div class="main">
77
77
78 <h2 class="breadcrumb"><a href="/">Mercurial</a> </h2>
78 <h2 class="breadcrumb"><a href="/">Mercurial</a> </h2>
79 <h3>error</h3>
79 <h3>error</h3>
80
80
81 <form class="search" action="/log">
81 <form class="search" action="/log">
82
82
83 <p><input name="rev" id="search1" type="text" size="30"></p>
83 <p><input name="rev" id="search1" type="text" size="30"></p>
84 <div id="hint">Find changesets by keywords (author, files, the commit message), revision
84 <div id="hint">Find changesets by keywords (author, files, the commit message), revision
85 number or hash, or <a href="/help/revsets">revset expression</a>.</div>
85 number or hash, or <a href="/help/revsets">revset expression</a>.</div>
86 </form>
86 </form>
87
87
88 <div class="description">
88 <div class="description">
89 <p>
89 <p>
90 An error occurred while processing your request:
90 An error occurred while processing your request:
91 </p>
91 </p>
92 <p>
92 <p>
93 Not Found
93 Not Found
94 </p>
94 </p>
95 </div>
95 </div>
96 </div>
96 </div>
97 </div>
97 </div>
98
98
99 <script type="text/javascript">process_dates()</script>
99 <script type="text/javascript">process_dates()</script>
100
100
101
101
102 </body>
102 </body>
103 </html>
103 </html>
104
104
105 [1]
105 [1]
106
106
107 should give a 404 - bad revision
107 should give a 404 - bad revision
108
108
109 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'file/spam/foo?style=raw'
109 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'file/spam/foo?style=raw'
110 404 Not Found
110 404 Not Found
111
111
112
112
113 error: revision not found: spam
113 error: revision not found: spam
114 [1]
114 [1]
115
115
116 should give a 400 - bad command
116 should give a 400 - bad command
117
117
118 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'file/tip/foo?cmd=spam&style=raw'
118 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'file/tip/foo?cmd=spam&style=raw'
119 400* (glob)
119 400* (glob)
120
120
121
121
122 error: no such method: spam
122 error: no such method: spam
123 [1]
123 [1]
124
124
125 $ "$TESTDIR/get-with-headers.py" --headeronly localhost:$HGPORT '?cmd=spam'
125 $ "$TESTDIR/get-with-headers.py" --headeronly localhost:$HGPORT '?cmd=spam'
126 400 no such method: spam
126 400 no such method: spam
127 [1]
127 [1]
128
128
129 should give a 400 - bad command as a part of url path (issue4071)
129 should give a 400 - bad command as a part of url path (issue4071)
130
130
131 $ "$TESTDIR/get-with-headers.py" --headeronly localhost:$HGPORT 'spam'
131 $ "$TESTDIR/get-with-headers.py" --headeronly localhost:$HGPORT 'spam'
132 400 no such method: spam
132 400 no such method: spam
133 [1]
133 [1]
134
134
135 $ "$TESTDIR/get-with-headers.py" --headeronly localhost:$HGPORT 'raw-spam'
135 $ "$TESTDIR/get-with-headers.py" --headeronly localhost:$HGPORT 'raw-spam'
136 400 no such method: spam
136 400 no such method: spam
137 [1]
137 [1]
138
138
139 $ "$TESTDIR/get-with-headers.py" --headeronly localhost:$HGPORT 'spam/tip/foo'
139 $ "$TESTDIR/get-with-headers.py" --headeronly localhost:$HGPORT 'spam/tip/foo'
140 400 no such method: spam
140 400 no such method: spam
141 [1]
141 [1]
142
142
143 should give a 404 - file does not exist
143 should give a 404 - file does not exist
144
144
145 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'file/tip/bork?style=raw'
145 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'file/tip/bork?style=raw'
146 404 Not Found
146 404 Not Found
147
147
148
148
149 error: bork@2ef0ac749a14: not found in manifest
149 error: bork@2ef0ac749a14: not found in manifest
150 [1]
150 [1]
151 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'file/tip/bork'
151 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'file/tip/bork'
152 404 Not Found
152 404 Not Found
153
153
154 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
154 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
155 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
155 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
156 <head>
156 <head>
157 <link rel="icon" href="/static/hgicon.png" type="image/png" />
157 <link rel="icon" href="/static/hgicon.png" type="image/png" />
158 <meta name="robots" content="index, nofollow" />
158 <meta name="robots" content="index, nofollow" />
159 <link rel="stylesheet" href="/static/style-paper.css" type="text/css" />
159 <link rel="stylesheet" href="/static/style-paper.css" type="text/css" />
160 <script type="text/javascript" src="/static/mercurial.js"></script>
160 <script type="text/javascript" src="/static/mercurial.js"></script>
161
161
162 <title>test: error</title>
162 <title>test: error</title>
163 </head>
163 </head>
164 <body>
164 <body>
165
165
166 <div class="container">
166 <div class="container">
167 <div class="menu">
167 <div class="menu">
168 <div class="logo">
168 <div class="logo">
169 <a href="http://mercurial.selenic.com/">
169 <a href="http://mercurial.selenic.com/">
170 <img src="/static/hglogo.png" width=75 height=90 border=0 alt="mercurial" /></a>
170 <img src="/static/hglogo.png" width=75 height=90 border=0 alt="mercurial" /></a>
171 </div>
171 </div>
172 <ul>
172 <ul>
173 <li><a href="/shortlog">log</a></li>
173 <li><a href="/shortlog">log</a></li>
174 <li><a href="/graph">graph</a></li>
174 <li><a href="/graph">graph</a></li>
175 <li><a href="/tags">tags</a></li>
175 <li><a href="/tags">tags</a></li>
176 <li><a href="/bookmarks">bookmarks</a></li>
176 <li><a href="/bookmarks">bookmarks</a></li>
177 <li><a href="/branches">branches</a></li>
177 <li><a href="/branches">branches</a></li>
178 </ul>
178 </ul>
179 <ul>
179 <ul>
180 <li><a href="/help">help</a></li>
180 <li><a href="/help">help</a></li>
181 </ul>
181 </ul>
182 </div>
182 </div>
183
183
184 <div class="main">
184 <div class="main">
185
185
186 <h2 class="breadcrumb"><a href="/">Mercurial</a> </h2>
186 <h2 class="breadcrumb"><a href="/">Mercurial</a> </h2>
187 <h3>error</h3>
187 <h3>error</h3>
188
188
189 <form class="search" action="/log">
189 <form class="search" action="/log">
190
190
191 <p><input name="rev" id="search1" type="text" size="30"></p>
191 <p><input name="rev" id="search1" type="text" size="30"></p>
192 <div id="hint">Find changesets by keywords (author, files, the commit message), revision
192 <div id="hint">Find changesets by keywords (author, files, the commit message), revision
193 number or hash, or <a href="/help/revsets">revset expression</a>.</div>
193 number or hash, or <a href="/help/revsets">revset expression</a>.</div>
194 </form>
194 </form>
195
195
196 <div class="description">
196 <div class="description">
197 <p>
197 <p>
198 An error occurred while processing your request:
198 An error occurred while processing your request:
199 </p>
199 </p>
200 <p>
200 <p>
201 bork@2ef0ac749a14: not found in manifest
201 bork@2ef0ac749a14: not found in manifest
202 </p>
202 </p>
203 </div>
203 </div>
204 </div>
204 </div>
205 </div>
205 </div>
206
206
207 <script type="text/javascript">process_dates()</script>
207 <script type="text/javascript">process_dates()</script>
208
208
209
209
210 </body>
210 </body>
211 </html>
211 </html>
212
212
213 [1]
213 [1]
214 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'diff/tip/bork?style=raw'
214 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'diff/tip/bork?style=raw'
215 404 Not Found
215 404 Not Found
216
216
217
217
218 error: bork@2ef0ac749a14: not found in manifest
218 error: bork@2ef0ac749a14: not found in manifest
219 [1]
219 [1]
220
220
221 try bad style
221 try bad style
222
222
223 $ ("$TESTDIR/get-with-headers.py" localhost:$HGPORT 'file/tip/?style=foobar')
223 $ ("$TESTDIR/get-with-headers.py" localhost:$HGPORT 'file/tip/?style=foobar')
224 200 Script output follows
224 200 Script output follows
225
225
226 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
226 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
227 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
227 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
228 <head>
228 <head>
229 <link rel="icon" href="/static/hgicon.png" type="image/png" />
229 <link rel="icon" href="/static/hgicon.png" type="image/png" />
230 <meta name="robots" content="index, nofollow" />
230 <meta name="robots" content="index, nofollow" />
231 <link rel="stylesheet" href="/static/style-paper.css" type="text/css" />
231 <link rel="stylesheet" href="/static/style-paper.css" type="text/css" />
232 <script type="text/javascript" src="/static/mercurial.js"></script>
232 <script type="text/javascript" src="/static/mercurial.js"></script>
233
233
234 <title>test: 2ef0ac749a14 /</title>
234 <title>test: 2ef0ac749a14 /</title>
235 </head>
235 </head>
236 <body>
236 <body>
237
237
238 <div class="container">
238 <div class="container">
239 <div class="menu">
239 <div class="menu">
240 <div class="logo">
240 <div class="logo">
241 <a href="http://mercurial.selenic.com/">
241 <a href="http://mercurial.selenic.com/">
242 <img src="/static/hglogo.png" alt="mercurial" /></a>
242 <img src="/static/hglogo.png" alt="mercurial" /></a>
243 </div>
243 </div>
244 <ul>
244 <ul>
245 <li><a href="/shortlog/2ef0ac749a14">log</a></li>
245 <li><a href="/shortlog/2ef0ac749a14">log</a></li>
246 <li><a href="/graph/2ef0ac749a14">graph</a></li>
246 <li><a href="/graph/2ef0ac749a14">graph</a></li>
247 <li><a href="/tags">tags</a></li>
247 <li><a href="/tags">tags</a></li>
248 <li><a href="/bookmarks">bookmarks</a></li>
248 <li><a href="/bookmarks">bookmarks</a></li>
249 <li><a href="/branches">branches</a></li>
249 <li><a href="/branches">branches</a></li>
250 </ul>
250 </ul>
251 <ul>
251 <ul>
252 <li><a href="/rev/2ef0ac749a14">changeset</a></li>
252 <li><a href="/rev/2ef0ac749a14">changeset</a></li>
253 <li class="active">browse</li>
253 <li class="active">browse</li>
254 </ul>
254 </ul>
255 <ul>
255 <ul>
256
256
257 </ul>
257 </ul>
258 <ul>
258 <ul>
259 <li><a href="/help">help</a></li>
259 <li><a href="/help">help</a></li>
260 </ul>
260 </ul>
261 </div>
261 </div>
262
262
263 <div class="main">
263 <div class="main">
264 <h2 class="breadcrumb"><a href="/">Mercurial</a> </h2>
264 <h2 class="breadcrumb"><a href="/">Mercurial</a> </h2>
265 <h3>directory / @ 0:2ef0ac749a14 <span class="tag">tip</span> </h3>
265 <h3>directory / @ 0:2ef0ac749a14 <span class="tag">tip</span> </h3>
266
266
267 <form class="search" action="/log">
267 <form class="search" action="/log">
268
268
269 <p><input name="rev" id="search1" type="text" size="30" /></p>
269 <p><input name="rev" id="search1" type="text" size="30" /></p>
270 <div id="hint">Find changesets by keywords (author, files, the commit message), revision
270 <div id="hint">Find changesets by keywords (author, files, the commit message), revision
271 number or hash, or <a href="/help/revsets">revset expression</a>.</div>
271 number or hash, or <a href="/help/revsets">revset expression</a>.</div>
272 </form>
272 </form>
273
273
274 <table class="bigtable">
274 <table class="bigtable">
275 <thead>
275 <thead>
276 <tr>
276 <tr>
277 <th class="name">name</th>
277 <th class="name">name</th>
278 <th class="size">size</th>
278 <th class="size">size</th>
279 <th class="permissions">permissions</th>
279 <th class="permissions">permissions</th>
280 </tr>
280 </tr>
281 </thead>
281 </thead>
282 <tbody class="stripes2">
282 <tbody class="stripes2">
283 <tr class="fileline">
283 <tr class="fileline">
284 <td class="name"><a href="/file/2ef0ac749a14/">[up]</a></td>
284 <td class="name"><a href="/file/2ef0ac749a14/">[up]</a></td>
285 <td class="size"></td>
285 <td class="size"></td>
286 <td class="permissions">drwxr-xr-x</td>
286 <td class="permissions">drwxr-xr-x</td>
287 </tr>
287 </tr>
288
288
289 <tr class="fileline">
289 <tr class="fileline">
290 <td class="name">
290 <td class="name">
291 <a href="/file/2ef0ac749a14/da">
291 <a href="/file/2ef0ac749a14/da">
292 <img src="/static/coal-folder.png" alt="dir."/> da/
292 <img src="/static/coal-folder.png" alt="dir."/> da/
293 </a>
293 </a>
294 <a href="/file/2ef0ac749a14/da/">
294 <a href="/file/2ef0ac749a14/da/">
295
295
296 </a>
296 </a>
297 </td>
297 </td>
298 <td class="size"></td>
298 <td class="size"></td>
299 <td class="permissions">drwxr-xr-x</td>
299 <td class="permissions">drwxr-xr-x</td>
300 </tr>
300 </tr>
301
301
302 <tr class="fileline">
302 <tr class="fileline">
303 <td class="filename">
303 <td class="filename">
304 <a href="/file/2ef0ac749a14/foo">
304 <a href="/file/2ef0ac749a14/foo">
305 <img src="/static/coal-file.png" alt="file"/> foo
305 <img src="/static/coal-file.png" alt="file"/> foo
306 </a>
306 </a>
307 </td>
307 </td>
308 <td class="size">4</td>
308 <td class="size">4</td>
309 <td class="permissions">-rw-r--r--</td>
309 <td class="permissions">-rw-r--r--</td>
310 </tr>
310 </tr>
311 </tbody>
311 </tbody>
312 </table>
312 </table>
313 </div>
313 </div>
314 </div>
314 </div>
315 <script type="text/javascript">process_dates()</script>
315 <script type="text/javascript">process_dates()</script>
316
316
317
317
318 </body>
318 </body>
319 </html>
319 </html>
320
320
321
321
322 stop and restart
322 stop and restart
323
323
324 $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS
324 $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS
325 $ hg serve -p $HGPORT -d --pid-file=hg.pid -A access.log
325 $ hg serve -p $HGPORT -d --pid-file=hg.pid -A access.log
326 $ cat hg.pid >> $DAEMON_PIDS
326 $ cat hg.pid >> $DAEMON_PIDS
327
327
328 Test the access/error files are opened in append mode
328 Test the access/error files are opened in append mode
329
329
330 $ $PYTHON -c "print len(file('access.log').readlines()), 'log lines written'"
330 $ $PYTHON -c "print len(file('access.log').readlines()), 'log lines written'"
331 14 log lines written
331 14 log lines written
332
332
333 static file
333 static file
334
334
335 $ "$TESTDIR/get-with-headers.py" --twice localhost:$HGPORT 'static/style-gitweb.css' - date etag server
335 $ "$TESTDIR/get-with-headers.py" --twice localhost:$HGPORT 'static/style-gitweb.css' - date etag server
336 200 Script output follows
336 200 Script output follows
337 content-length: 5372
337 content-length: 5372
338 content-type: text/css
338 content-type: text/css
339
339
340 body { font-family: sans-serif; font-size: 12px; border:solid #d9d8d1; border-width:1px; margin:10px; }
340 body { font-family: sans-serif; font-size: 12px; border:solid #d9d8d1; border-width:1px; margin:10px; }
341 a { color:#0000cc; }
341 a { color:#0000cc; }
342 a:hover, a:visited, a:active { color:#880000; }
342 a:hover, a:visited, a:active { color:#880000; }
343 div.page_header { height:25px; padding:8px; font-size:18px; font-weight:bold; background-color:#d9d8d1; }
343 div.page_header { height:25px; padding:8px; font-size:18px; font-weight:bold; background-color:#d9d8d1; }
344 div.page_header a:visited { color:#0000cc; }
344 div.page_header a:visited { color:#0000cc; }
345 div.page_header a:hover { color:#880000; }
345 div.page_header a:hover { color:#880000; }
346 div.page_nav { padding:8px; }
346 div.page_nav { padding:8px; }
347 div.page_nav a:visited { color:#0000cc; }
347 div.page_nav a:visited { color:#0000cc; }
348 div.page_path { padding:8px; border:solid #d9d8d1; border-width:0px 0px 1px}
348 div.page_path { padding:8px; border:solid #d9d8d1; border-width:0px 0px 1px}
349 div.page_footer { padding:4px 8px; background-color: #d9d8d1; }
349 div.page_footer { padding:4px 8px; background-color: #d9d8d1; }
350 div.page_footer_text { float:left; color:#555555; font-style:italic; }
350 div.page_footer_text { float:left; color:#555555; font-style:italic; }
351 div.page_body { padding:8px; }
351 div.page_body { padding:8px; }
352 div.title, a.title {
352 div.title, a.title {
353 display:block; padding:6px 8px;
353 display:block; padding:6px 8px;
354 font-weight:bold; background-color:#edece6; text-decoration:none; color:#000000;
354 font-weight:bold; background-color:#edece6; text-decoration:none; color:#000000;
355 }
355 }
356 a.title:hover { background-color: #d9d8d1; }
356 a.title:hover { background-color: #d9d8d1; }
357 div.title_text { padding:6px 0px; border: solid #d9d8d1; border-width:0px 0px 1px; }
357 div.title_text { padding:6px 0px; border: solid #d9d8d1; border-width:0px 0px 1px; }
358 div.log_body { padding:8px 8px 8px 150px; }
358 div.log_body { padding:8px 8px 8px 150px; }
359 .age { white-space:nowrap; }
359 .age { white-space:nowrap; }
360 span.age { position:relative; float:left; width:142px; font-style:italic; }
360 span.age { position:relative; float:left; width:142px; font-style:italic; }
361 div.log_link {
361 div.log_link {
362 padding:0px 8px;
362 padding:0px 8px;
363 font-size:10px; font-family:sans-serif; font-style:normal;
363 font-size:10px; font-family:sans-serif; font-style:normal;
364 position:relative; float:left; width:136px;
364 position:relative; float:left; width:136px;
365 }
365 }
366 div.list_head { padding:6px 8px 4px; border:solid #d9d8d1; border-width:1px 0px 0px; font-style:italic; }
366 div.list_head { padding:6px 8px 4px; border:solid #d9d8d1; border-width:1px 0px 0px; font-style:italic; }
367 a.list { text-decoration:none; color:#000000; }
367 a.list { text-decoration:none; color:#000000; }
368 a.list:hover { text-decoration:underline; color:#880000; }
368 a.list:hover { text-decoration:underline; color:#880000; }
369 table { padding:8px 4px; }
369 table { padding:8px 4px; }
370 th { padding:2px 5px; font-size:12px; text-align:left; }
370 th { padding:2px 5px; font-size:12px; text-align:left; }
371 tr.light:hover, .parity0:hover { background-color:#edece6; }
371 tr.light:hover, .parity0:hover { background-color:#edece6; }
372 tr.dark, .parity1 { background-color:#f6f6f0; }
372 tr.dark, .parity1 { background-color:#f6f6f0; }
373 tr.dark:hover, .parity1:hover { background-color:#edece6; }
373 tr.dark:hover, .parity1:hover { background-color:#edece6; }
374 td { padding:2px 5px; font-size:12px; vertical-align:top; }
374 td { padding:2px 5px; font-size:12px; vertical-align:top; }
375 td.closed { background-color: #99f; }
375 td.closed { background-color: #99f; }
376 td.link { padding:2px 5px; font-family:sans-serif; font-size:10px; }
376 td.link { padding:2px 5px; font-family:sans-serif; font-size:10px; }
377 td.indexlinks { white-space: nowrap; }
377 td.indexlinks { white-space: nowrap; }
378 td.indexlinks a {
378 td.indexlinks a {
379 padding: 2px 5px; line-height: 10px;
379 padding: 2px 5px; line-height: 10px;
380 border: 1px solid;
380 border: 1px solid;
381 color: #ffffff; background-color: #7777bb;
381 color: #ffffff; background-color: #7777bb;
382 border-color: #aaaadd #333366 #333366 #aaaadd;
382 border-color: #aaaadd #333366 #333366 #aaaadd;
383 font-weight: bold; text-align: center; text-decoration: none;
383 font-weight: bold; text-align: center; text-decoration: none;
384 font-size: 10px;
384 font-size: 10px;
385 }
385 }
386 td.indexlinks a:hover { background-color: #6666aa; }
386 td.indexlinks a:hover { background-color: #6666aa; }
387 div.pre { font-family:monospace; font-size:12px; white-space:pre; }
387 div.pre { font-family:monospace; font-size:12px; white-space:pre; }
388 div.diff_info { font-family:monospace; color:#000099; background-color:#edece6; font-style:italic; }
388 div.diff_info { font-family:monospace; color:#000099; background-color:#edece6; font-style:italic; }
389 div.index_include { border:solid #d9d8d1; border-width:0px 0px 1px; padding:12px 8px; }
389 div.index_include { border:solid #d9d8d1; border-width:0px 0px 1px; padding:12px 8px; }
390 div.search { margin:4px 8px; position:absolute; top:56px; right:12px }
390 div.search { margin:4px 8px; position:absolute; top:56px; right:12px }
391 .linenr { color:#999999; text-decoration:none }
391 .linenr { color:#999999; text-decoration:none }
392 div.rss_logo { float: right; white-space: nowrap; }
392 div.rss_logo { float: right; white-space: nowrap; }
393 div.rss_logo a {
393 div.rss_logo a {
394 padding:3px 6px; line-height:10px;
394 padding:3px 6px; line-height:10px;
395 border:1px solid; border-color:#fcc7a5 #7d3302 #3e1a01 #ff954e;
395 border:1px solid; border-color:#fcc7a5 #7d3302 #3e1a01 #ff954e;
396 color:#ffffff; background-color:#ff6600;
396 color:#ffffff; background-color:#ff6600;
397 font-weight:bold; font-family:sans-serif; font-size:10px;
397 font-weight:bold; font-family:sans-serif; font-size:10px;
398 text-align:center; text-decoration:none;
398 text-align:center; text-decoration:none;
399 }
399 }
400 div.rss_logo a:hover { background-color:#ee5500; }
400 div.rss_logo a:hover { background-color:#ee5500; }
401 pre { margin: 0; }
401 pre { margin: 0; }
402 span.logtags span {
402 span.logtags span {
403 padding: 0px 4px;
403 padding: 0px 4px;
404 font-size: 10px;
404 font-size: 10px;
405 font-weight: normal;
405 font-weight: normal;
406 border: 1px solid;
406 border: 1px solid;
407 background-color: #ffaaff;
407 background-color: #ffaaff;
408 border-color: #ffccff #ff00ee #ff00ee #ffccff;
408 border-color: #ffccff #ff00ee #ff00ee #ffccff;
409 }
409 }
410 span.logtags span.tagtag {
410 span.logtags span.tagtag {
411 background-color: #ffffaa;
411 background-color: #ffffaa;
412 border-color: #ffffcc #ffee00 #ffee00 #ffffcc;
412 border-color: #ffffcc #ffee00 #ffee00 #ffffcc;
413 }
413 }
414 span.logtags span.branchtag {
414 span.logtags span.branchtag {
415 background-color: #aaffaa;
415 background-color: #aaffaa;
416 border-color: #ccffcc #00cc33 #00cc33 #ccffcc;
416 border-color: #ccffcc #00cc33 #00cc33 #ccffcc;
417 }
417 }
418 span.logtags span.inbranchtag {
418 span.logtags span.inbranchtag {
419 background-color: #d5dde6;
419 background-color: #d5dde6;
420 border-color: #e3ecf4 #9398f4 #9398f4 #e3ecf4;
420 border-color: #e3ecf4 #9398f4 #9398f4 #e3ecf4;
421 }
421 }
422 span.logtags span.bookmarktag {
422 span.logtags span.bookmarktag {
423 background-color: #afdffa;
423 background-color: #afdffa;
424 border-color: #ccecff #46ace6 #46ace6 #ccecff;
424 border-color: #ccecff #46ace6 #46ace6 #ccecff;
425 }
425 }
426 span.difflineplus { color:#008800; }
426 span.difflineplus { color:#008800; }
427 span.difflineminus { color:#cc0000; }
427 span.difflineminus { color:#cc0000; }
428 span.difflineat { color:#990099; }
428 span.difflineat { color:#990099; }
429
429
430 /* Graph */
430 /* Graph */
431 div#wrapper {
431 div#wrapper {
432 position: relative;
432 position: relative;
433 margin: 0;
433 margin: 0;
434 padding: 0;
434 padding: 0;
435 margin-top: 3px;
435 margin-top: 3px;
436 }
436 }
437
437
438 canvas {
438 canvas {
439 position: absolute;
439 position: absolute;
440 z-index: 5;
440 z-index: 5;
441 top: -0.9em;
441 top: -0.9em;
442 margin: 0;
442 margin: 0;
443 }
443 }
444
444
445 ul#nodebgs {
445 ul#nodebgs {
446 list-style: none inside none;
446 list-style: none inside none;
447 padding: 0;
447 padding: 0;
448 margin: 0;
448 margin: 0;
449 top: -0.7em;
449 top: -0.7em;
450 }
450 }
451
451
452 ul#graphnodes li, ul#nodebgs li {
452 ul#graphnodes li, ul#nodebgs li {
453 height: 39px;
453 height: 39px;
454 }
454 }
455
455
456 ul#graphnodes {
456 ul#graphnodes {
457 position: absolute;
457 position: absolute;
458 z-index: 10;
458 z-index: 10;
459 top: -0.8em;
459 top: -0.8em;
460 list-style: none inside none;
460 list-style: none inside none;
461 padding: 0;
461 padding: 0;
462 }
462 }
463
463
464 ul#graphnodes li .info {
464 ul#graphnodes li .info {
465 display: block;
465 display: block;
466 font-size: 100%;
466 font-size: 100%;
467 position: relative;
467 position: relative;
468 top: -3px;
468 top: -3px;
469 font-style: italic;
469 font-style: italic;
470 }
470 }
471
471
472 /* Comparison */
472 /* Comparison */
473 .legend {
473 .legend {
474 padding: 1.5% 0 1.5% 0;
474 padding: 1.5% 0 1.5% 0;
475 }
475 }
476
476
477 .legendinfo {
477 .legendinfo {
478 border: 1px solid #d9d8d1;
478 border: 1px solid #d9d8d1;
479 font-size: 80%;
479 font-size: 80%;
480 text-align: center;
480 text-align: center;
481 padding: 0.5%;
481 padding: 0.5%;
482 }
482 }
483
483
484 .equal {
484 .equal {
485 background-color: #ffffff;
485 background-color: #ffffff;
486 }
486 }
487
487
488 .delete {
488 .delete {
489 background-color: #faa;
489 background-color: #faa;
490 color: #333;
490 color: #333;
491 }
491 }
492
492
493 .insert {
493 .insert {
494 background-color: #ffa;
494 background-color: #ffa;
495 }
495 }
496
496
497 .replace {
497 .replace {
498 background-color: #e8e8e8;
498 background-color: #e8e8e8;
499 }
499 }
500
500
501 .comparison {
501 .comparison {
502 overflow-x: auto;
502 overflow-x: auto;
503 }
503 }
504
504
505 .header th {
505 .header th {
506 text-align: center;
506 text-align: center;
507 }
507 }
508
508
509 .block {
509 .block {
510 border-top: 1px solid #d9d8d1;
510 border-top: 1px solid #d9d8d1;
511 }
511 }
512
512
513 .scroll-loading {
513 .scroll-loading {
514 -webkit-animation: change_color 1s linear 0s infinite alternate;
514 -webkit-animation: change_color 1s linear 0s infinite alternate;
515 -moz-animation: change_color 1s linear 0s infinite alternate;
515 -moz-animation: change_color 1s linear 0s infinite alternate;
516 -o-animation: change_color 1s linear 0s infinite alternate;
516 -o-animation: change_color 1s linear 0s infinite alternate;
517 animation: change_color 1s linear 0s infinite alternate;
517 animation: change_color 1s linear 0s infinite alternate;
518 }
518 }
519
519
520 @-webkit-keyframes change_color {
520 @-webkit-keyframes change_color {
521 from { background-color: #A0CEFF; } to { }
521 from { background-color: #A0CEFF; } to { }
522 }
522 }
523 @-moz-keyframes change_color {
523 @-moz-keyframes change_color {
524 from { background-color: #A0CEFF; } to { }
524 from { background-color: #A0CEFF; } to { }
525 }
525 }
526 @-o-keyframes change_color {
526 @-o-keyframes change_color {
527 from { background-color: #A0CEFF; } to { }
527 from { background-color: #A0CEFF; } to { }
528 }
528 }
529 @keyframes change_color {
529 @keyframes change_color {
530 from { background-color: #A0CEFF; } to { }
530 from { background-color: #A0CEFF; } to { }
531 }
531 }
532
532
533 .scroll-loading-error {
533 .scroll-loading-error {
534 background-color: #FFCCCC !important;
534 background-color: #FFCCCC !important;
535 }
535 }
536 304 Not Modified
536 304 Not Modified
537
537
538
538
539 phase changes are refreshed (issue4061)
539 phase changes are refreshed (issue4061)
540
540
541 $ echo bar >> foo
541 $ echo bar >> foo
542 $ hg ci -msecret --secret
542 $ hg ci -msecret --secret
543 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'log?style=raw'
543 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'log?style=raw'
544 200 Script output follows
544 200 Script output follows
545
545
546
546
547 # HG changelog
547 # HG changelog
548 # Node ID 2ef0ac749a14e4f57a5a822464a0902c6f7f448f
548 # Node ID 2ef0ac749a14e4f57a5a822464a0902c6f7f448f
549
549
550 changeset: 2ef0ac749a14e4f57a5a822464a0902c6f7f448f
550 changeset: 2ef0ac749a14e4f57a5a822464a0902c6f7f448f
551 revision: 0
551 revision: 0
552 user: test
552 user: test
553 date: Thu, 01 Jan 1970 00:00:00 +0000
553 date: Thu, 01 Jan 1970 00:00:00 +0000
554 summary: base
554 summary: base
555 branch: default
555 branch: default
556 tag: tip
556 tag: tip
557
557
558
558
559 $ hg phase --draft tip
559 $ hg phase --draft tip
560 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'log?style=raw'
560 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'log?style=raw'
561 200 Script output follows
561 200 Script output follows
562
562
563
563
564 # HG changelog
564 # HG changelog
565 # Node ID a084749e708a9c4c0a5b652a2a446322ce290e04
565 # Node ID a084749e708a9c4c0a5b652a2a446322ce290e04
566
566
567 changeset: a084749e708a9c4c0a5b652a2a446322ce290e04
567 changeset: a084749e708a9c4c0a5b652a2a446322ce290e04
568 revision: 1
568 revision: 1
569 user: test
569 user: test
570 date: Thu, 01 Jan 1970 00:00:00 +0000
570 date: Thu, 01 Jan 1970 00:00:00 +0000
571 summary: secret
571 summary: secret
572 branch: default
572 branch: default
573 tag: tip
573 tag: tip
574
574
575 changeset: 2ef0ac749a14e4f57a5a822464a0902c6f7f448f
575 changeset: 2ef0ac749a14e4f57a5a822464a0902c6f7f448f
576 revision: 0
576 revision: 0
577 user: test
577 user: test
578 date: Thu, 01 Jan 1970 00:00:00 +0000
578 date: Thu, 01 Jan 1970 00:00:00 +0000
579 summary: base
579 summary: base
580
580
581
581
582
582
583 no style can be loaded from directories other than the specified paths
584
585 $ mkdir -p x/templates/fallback
586 $ cat <<EOF > x/templates/fallback/map
587 > default = 'shortlog'
588 > shortlog = 'fall back to default\n'
589 > mimetype = 'text/plain'
590 > EOF
591 $ cat <<EOF > x/map
592 > default = 'shortlog'
593 > shortlog = 'access to outside of templates directory\n'
594 > mimetype = 'text/plain'
595 > EOF
596
597 $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS
598 $ hg serve -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log \
599 > --config web.style=fallback --config web.templates=x/templates
600 $ cat hg.pid >> $DAEMON_PIDS
601
602 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT "?style=`pwd`/x"
603 200 Script output follows
604
605 fall back to default
606
607 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT '?style=..'
608 200 Script output follows
609
610 fall back to default
611
612 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT '?style=./..'
613 200 Script output follows
614
615 fall back to default
616
617 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT '?style=.../.../'
618 200 Script output follows
619
620 fall back to default
621
583 errors
622 errors
584
623
585 $ cat errors.log
624 $ cat errors.log
586
625
587 Uncaught exceptions result in a logged error and canned HTTP response
626 Uncaught exceptions result in a logged error and canned HTTP response
588
627
589 $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS
628 $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS
590 $ hg --config extensions.hgweberror=$TESTDIR/hgweberror.py serve -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
629 $ hg --config extensions.hgweberror=$TESTDIR/hgweberror.py serve -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
591 $ cat hg.pid >> $DAEMON_PIDS
630 $ cat hg.pid >> $DAEMON_PIDS
592
631
593 $ $TESTDIR/get-with-headers.py localhost:$HGPORT 'raiseerror' transfer-encoding content-type
632 $ $TESTDIR/get-with-headers.py localhost:$HGPORT 'raiseerror' transfer-encoding content-type
594 500 Internal Server Error
633 500 Internal Server Error
595 transfer-encoding: chunked
634 transfer-encoding: chunked
596
635
597 Internal Server Error (no-eol)
636 Internal Server Error (no-eol)
598 [1]
637 [1]
599
638
600 $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS
639 $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS
601 $ head -1 errors.log
640 $ head -1 errors.log
602 .* Exception happened during processing request '/raiseerror': (re)
641 .* Exception happened during processing request '/raiseerror': (re)
603
642
604 Uncaught exception after partial content sent
643 Uncaught exception after partial content sent
605
644
606 $ hg --config extensions.hgweberror=$TESTDIR/hgweberror.py serve -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
645 $ hg --config extensions.hgweberror=$TESTDIR/hgweberror.py serve -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
607 $ cat hg.pid >> $DAEMON_PIDS
646 $ cat hg.pid >> $DAEMON_PIDS
608 $ $TESTDIR/get-with-headers.py localhost:$HGPORT 'raiseerror?partialresponse=1' transfer-encoding content-type
647 $ $TESTDIR/get-with-headers.py localhost:$HGPORT 'raiseerror?partialresponse=1' transfer-encoding content-type
609 200 Script output follows
648 200 Script output follows
610 transfer-encoding: chunked
649 transfer-encoding: chunked
611 content-type: text/plain
650 content-type: text/plain
612
651
613 partial content
652 partial content
614 Internal Server Error (no-eol)
653 Internal Server Error (no-eol)
615
654
616 $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS
655 $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS
617 $ cd ..
656 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now