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