##// END OF EJS Templates
templater: factor out runtemplate method...
Matt Mackall -
r17632:523625e4 default
parent child Browse files
Show More
@@ -1,405 +1,409 b''
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 sys, os
9 import sys, os
10 import util, config, templatefilters, parser, error
10 import util, config, templatefilters, parser, error
11
11
12 # template parsing
12 # template parsing
13
13
14 elements = {
14 elements = {
15 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
15 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
16 ",": (2, None, ("list", 2)),
16 ",": (2, None, ("list", 2)),
17 "|": (5, None, ("|", 5)),
17 "|": (5, None, ("|", 5)),
18 "%": (6, None, ("%", 6)),
18 "%": (6, None, ("%", 6)),
19 ")": (0, None, None),
19 ")": (0, None, None),
20 "symbol": (0, ("symbol",), None),
20 "symbol": (0, ("symbol",), None),
21 "string": (0, ("string",), None),
21 "string": (0, ("string",), None),
22 "end": (0, None, None),
22 "end": (0, None, None),
23 }
23 }
24
24
25 def tokenizer(data):
25 def tokenizer(data):
26 program, start, end = data
26 program, start, end = data
27 pos = start
27 pos = start
28 while pos < end:
28 while pos < end:
29 c = program[pos]
29 c = program[pos]
30 if c.isspace(): # skip inter-token whitespace
30 if c.isspace(): # skip inter-token whitespace
31 pass
31 pass
32 elif c in "(,)%|": # handle simple operators
32 elif c in "(,)%|": # handle simple operators
33 yield (c, None, pos)
33 yield (c, None, pos)
34 elif (c in '"\'' or c == 'r' and
34 elif (c in '"\'' or c == 'r' and
35 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
35 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
36 if c == 'r':
36 if c == 'r':
37 pos += 1
37 pos += 1
38 c = program[pos]
38 c = program[pos]
39 decode = lambda x: x
39 decode = lambda x: x
40 else:
40 else:
41 decode = lambda x: x.decode('string-escape')
41 decode = lambda x: x.decode('string-escape')
42 pos += 1
42 pos += 1
43 s = pos
43 s = pos
44 while pos < end: # find closing quote
44 while pos < end: # find closing quote
45 d = program[pos]
45 d = program[pos]
46 if d == '\\': # skip over escaped characters
46 if d == '\\': # skip over escaped characters
47 pos += 2
47 pos += 2
48 continue
48 continue
49 if d == c:
49 if d == c:
50 yield ('string', decode(program[s:pos]), s)
50 yield ('string', decode(program[s:pos]), s)
51 break
51 break
52 pos += 1
52 pos += 1
53 else:
53 else:
54 raise error.ParseError(_("unterminated string"), s)
54 raise error.ParseError(_("unterminated string"), s)
55 elif c.isalnum() or c in '_':
55 elif c.isalnum() or c in '_':
56 s = pos
56 s = pos
57 pos += 1
57 pos += 1
58 while pos < end: # find end of symbol
58 while pos < end: # find end of symbol
59 d = program[pos]
59 d = program[pos]
60 if not (d.isalnum() or d == "_"):
60 if not (d.isalnum() or d == "_"):
61 break
61 break
62 pos += 1
62 pos += 1
63 sym = program[s:pos]
63 sym = program[s:pos]
64 yield ('symbol', sym, s)
64 yield ('symbol', sym, s)
65 pos -= 1
65 pos -= 1
66 elif c == '}':
66 elif c == '}':
67 pos += 1
67 pos += 1
68 break
68 break
69 else:
69 else:
70 raise error.ParseError(_("syntax error"), pos)
70 raise error.ParseError(_("syntax error"), pos)
71 pos += 1
71 pos += 1
72 yield ('end', None, pos)
72 yield ('end', None, pos)
73
73
74 def compiletemplate(tmpl, context):
74 def compiletemplate(tmpl, context):
75 parsed = []
75 parsed = []
76 pos, stop = 0, len(tmpl)
76 pos, stop = 0, len(tmpl)
77 p = parser.parser(tokenizer, elements)
77 p = parser.parser(tokenizer, elements)
78
78
79 while pos < stop:
79 while pos < stop:
80 n = tmpl.find('{', pos)
80 n = tmpl.find('{', pos)
81 if n < 0:
81 if n < 0:
82 parsed.append(("string", tmpl[pos:]))
82 parsed.append(("string", tmpl[pos:]))
83 break
83 break
84 if n > 0 and tmpl[n - 1] == '\\':
84 if n > 0 and tmpl[n - 1] == '\\':
85 # escaped
85 # escaped
86 parsed.append(("string", tmpl[pos:n - 1] + "{"))
86 parsed.append(("string", tmpl[pos:n - 1] + "{"))
87 pos = n + 1
87 pos = n + 1
88 continue
88 continue
89 if n > pos:
89 if n > pos:
90 parsed.append(("string", tmpl[pos:n]))
90 parsed.append(("string", tmpl[pos:n]))
91
91
92 pd = [tmpl, n + 1, stop]
92 pd = [tmpl, n + 1, stop]
93 parseres, pos = p.parse(pd)
93 parseres, pos = p.parse(pd)
94 parsed.append(parseres)
94 parsed.append(parseres)
95
95
96 return [compileexp(e, context) for e in parsed]
96 return [compileexp(e, context) for e in parsed]
97
97
98 def compileexp(exp, context):
98 def compileexp(exp, context):
99 t = exp[0]
99 t = exp[0]
100 if t in methods:
100 if t in methods:
101 return methods[t](exp, context)
101 return methods[t](exp, context)
102 raise error.ParseError(_("unknown method '%s'") % t)
102 raise error.ParseError(_("unknown method '%s'") % t)
103
103
104 # template evaluation
104 # template evaluation
105
105
106 def getsymbol(exp):
106 def getsymbol(exp):
107 if exp[0] == 'symbol':
107 if exp[0] == 'symbol':
108 return exp[1]
108 return exp[1]
109 raise error.ParseError(_("expected a symbol"))
109 raise error.ParseError(_("expected a symbol"))
110
110
111 def getlist(x):
111 def getlist(x):
112 if not x:
112 if not x:
113 return []
113 return []
114 if x[0] == 'list':
114 if x[0] == 'list':
115 return getlist(x[1]) + [x[2]]
115 return getlist(x[1]) + [x[2]]
116 return [x]
116 return [x]
117
117
118 def getfilter(exp, context):
118 def getfilter(exp, context):
119 f = getsymbol(exp)
119 f = getsymbol(exp)
120 if f not in context._filters:
120 if f not in context._filters:
121 raise error.ParseError(_("unknown function '%s'") % f)
121 raise error.ParseError(_("unknown function '%s'") % f)
122 return context._filters[f]
122 return context._filters[f]
123
123
124 def gettemplate(exp, context):
124 def gettemplate(exp, context):
125 if exp[0] == 'string':
125 if exp[0] == 'string':
126 return compiletemplate(exp[1], context)
126 return compiletemplate(exp[1], context)
127 if exp[0] == 'symbol':
127 if exp[0] == 'symbol':
128 return context._load(exp[1])
128 return context._load(exp[1])
129 raise error.ParseError(_("expected template specifier"))
129 raise error.ParseError(_("expected template specifier"))
130
130
131 def runstring(context, mapping, data):
131 def runstring(context, mapping, data):
132 return data
132 return data
133
133
134 def runsymbol(context, mapping, key):
134 def runsymbol(context, mapping, key):
135 v = mapping.get(key)
135 v = mapping.get(key)
136 if v is None:
136 if v is None:
137 v = context._defaults.get(key, '')
137 v = context._defaults.get(key, '')
138 if util.safehasattr(v, '__call__'):
138 if util.safehasattr(v, '__call__'):
139 return v(**mapping)
139 return v(**mapping)
140 return v
140 return v
141
141
142 def buildfilter(exp, context):
142 def buildfilter(exp, context):
143 func, data = compileexp(exp[1], context)
143 func, data = compileexp(exp[1], context)
144 filt = getfilter(exp[2], context)
144 filt = getfilter(exp[2], context)
145 return (runfilter, (func, data, filt))
145 return (runfilter, (func, data, filt))
146
146
147 def runfilter(context, mapping, data):
147 def runfilter(context, mapping, data):
148 func, data, filt = data
148 func, data, filt = data
149 try:
149 try:
150 return filt(func(context, mapping, data))
150 return filt(func(context, mapping, data))
151 except (ValueError, AttributeError, TypeError):
151 except (ValueError, AttributeError, TypeError):
152 if isinstance(data, tuple):
152 if isinstance(data, tuple):
153 dt = data[1]
153 dt = data[1]
154 else:
154 else:
155 dt = data
155 dt = data
156 raise util.Abort(_("template filter '%s' is not compatible with "
156 raise util.Abort(_("template filter '%s' is not compatible with "
157 "keyword '%s'") % (filt.func_name, dt))
157 "keyword '%s'") % (filt.func_name, dt))
158
158
159 def buildmap(exp, context):
159 def buildmap(exp, context):
160 func, data = compileexp(exp[1], context)
160 func, data = compileexp(exp[1], context)
161 ctmpl = gettemplate(exp[2], context)
161 ctmpl = gettemplate(exp[2], context)
162 return (runmap, (func, data, ctmpl))
162 return (runmap, (func, data, ctmpl))
163
163
164 def runtemplate(context, mapping, template):
165 for func, data in template:
166 yield func(context, mapping, data)
167
164 def runmap(context, mapping, data):
168 def runmap(context, mapping, data):
165 func, data, ctmpl = data
169 func, data, ctmpl = data
166 d = func(context, mapping, data)
170 d = func(context, mapping, data)
167 if util.safehasattr(d, '__call__'):
171 if util.safehasattr(d, '__call__'):
168 d = d()
172 d = d()
169
173
170 lm = mapping.copy()
174 lm = mapping.copy()
171
175
172 for i in d:
176 for i in d:
173 if isinstance(i, dict):
177 if isinstance(i, dict):
174 lm.update(i)
178 lm.update(i)
175 for f, d in ctmpl:
179 yield runtemplate(context, lm, ctmpl)
176 yield f(context, lm, d)
177 else:
180 else:
178 # v is not an iterable of dicts, this happen when 'key'
181 # v is not an iterable of dicts, this happen when 'key'
179 # has been fully expanded already and format is useless.
182 # has been fully expanded already and format is useless.
180 # If so, return the expanded value.
183 # If so, return the expanded value.
181 yield i
184 yield i
182
185
183 def buildfunc(exp, context):
186 def buildfunc(exp, context):
184 n = getsymbol(exp[1])
187 n = getsymbol(exp[1])
185 args = [compileexp(x, context) for x in getlist(exp[2])]
188 args = [compileexp(x, context) for x in getlist(exp[2])]
186 if n in funcs:
189 if n in funcs:
187 f = funcs[n]
190 f = funcs[n]
188 return (f, args)
191 return (f, args)
189 if n in context._filters:
192 if n in context._filters:
190 if len(args) != 1:
193 if len(args) != 1:
191 raise error.ParseError(_("filter %s expects one argument") % n)
194 raise error.ParseError(_("filter %s expects one argument") % n)
192 f = context._filters[n]
195 f = context._filters[n]
193 return (runfilter, (args[0][0], args[0][1], f))
196 return (runfilter, (args[0][0], args[0][1], f))
194
197
195 methods = {
198 methods = {
196 "string": lambda e, c: (runstring, e[1]),
199 "string": lambda e, c: (runstring, e[1]),
197 "symbol": lambda e, c: (runsymbol, e[1]),
200 "symbol": lambda e, c: (runsymbol, e[1]),
198 "group": lambda e, c: compileexp(e[1], c),
201 "group": lambda e, c: compileexp(e[1], c),
199 # ".": buildmember,
202 # ".": buildmember,
200 "|": buildfilter,
203 "|": buildfilter,
201 "%": buildmap,
204 "%": buildmap,
202 "func": buildfunc,
205 "func": buildfunc,
203 }
206 }
204
207
205 funcs = {
208 funcs = {
206 }
209 }
207
210
208 # template engine
211 # template engine
209
212
210 path = ['templates', '../templates']
213 path = ['templates', '../templates']
211 stringify = templatefilters.stringify
214 stringify = templatefilters.stringify
212
215
213 def _flatten(thing):
216 def _flatten(thing):
214 '''yield a single stream from a possibly nested set of iterators'''
217 '''yield a single stream from a possibly nested set of iterators'''
215 if isinstance(thing, str):
218 if isinstance(thing, str):
216 yield thing
219 yield thing
217 elif not util.safehasattr(thing, '__iter__'):
220 elif not util.safehasattr(thing, '__iter__'):
218 if thing is not None:
221 if thing is not None:
219 yield str(thing)
222 yield str(thing)
220 else:
223 else:
221 for i in thing:
224 for i in thing:
222 if isinstance(i, str):
225 if isinstance(i, str):
223 yield i
226 yield i
224 elif not util.safehasattr(i, '__iter__'):
227 elif not util.safehasattr(i, '__iter__'):
225 if i is not None:
228 if i is not None:
226 yield str(i)
229 yield str(i)
227 elif i is not None:
230 elif i is not None:
228 for j in _flatten(i):
231 for j in _flatten(i):
229 yield j
232 yield j
230
233
231 def parsestring(s, quoted=True):
234 def parsestring(s, quoted=True):
232 '''parse a string using simple c-like syntax.
235 '''parse a string using simple c-like syntax.
233 string must be in quotes if quoted is True.'''
236 string must be in quotes if quoted is True.'''
234 if quoted:
237 if quoted:
235 if len(s) < 2 or s[0] != s[-1]:
238 if len(s) < 2 or s[0] != s[-1]:
236 raise SyntaxError(_('unmatched quotes'))
239 raise SyntaxError(_('unmatched quotes'))
237 return s[1:-1].decode('string_escape')
240 return s[1:-1].decode('string_escape')
238
241
239 return s.decode('string_escape')
242 return s.decode('string_escape')
240
243
241 class engine(object):
244 class engine(object):
242 '''template expansion engine.
245 '''template expansion engine.
243
246
244 template expansion works like this. a map file contains key=value
247 template expansion works like this. a map file contains key=value
245 pairs. if value is quoted, it is treated as string. otherwise, it
248 pairs. if value is quoted, it is treated as string. otherwise, it
246 is treated as name of template file.
249 is treated as name of template file.
247
250
248 templater is asked to expand a key in map. it looks up key, and
251 templater is asked to expand a key in map. it looks up key, and
249 looks for strings like this: {foo}. it expands {foo} by looking up
252 looks for strings like this: {foo}. it expands {foo} by looking up
250 foo in map, and substituting it. expansion is recursive: it stops
253 foo in map, and substituting it. expansion is recursive: it stops
251 when there is no more {foo} to replace.
254 when there is no more {foo} to replace.
252
255
253 expansion also allows formatting and filtering.
256 expansion also allows formatting and filtering.
254
257
255 format uses key to expand each item in list. syntax is
258 format uses key to expand each item in list. syntax is
256 {key%format}.
259 {key%format}.
257
260
258 filter uses function to transform value. syntax is
261 filter uses function to transform value. syntax is
259 {key|filter1|filter2|...}.'''
262 {key|filter1|filter2|...}.'''
260
263
261 def __init__(self, loader, filters={}, defaults={}):
264 def __init__(self, loader, filters={}, defaults={}):
262 self._loader = loader
265 self._loader = loader
263 self._filters = filters
266 self._filters = filters
264 self._defaults = defaults
267 self._defaults = defaults
265 self._cache = {}
268 self._cache = {}
266
269
267 def _load(self, t):
270 def _load(self, t):
268 '''load, parse, and cache a template'''
271 '''load, parse, and cache a template'''
269 if t not in self._cache:
272 if t not in self._cache:
270 self._cache[t] = compiletemplate(self._loader(t), self)
273 self._cache[t] = compiletemplate(self._loader(t), self)
271 return self._cache[t]
274 return self._cache[t]
272
275
273 def process(self, t, mapping):
276 def process(self, t, mapping):
274 '''Perform expansion. t is name of map element to expand.
277 '''Perform expansion. t is name of map element to expand.
275 mapping contains added elements for use during expansion. Is a
278 mapping contains added elements for use during expansion. Is a
276 generator.'''
279 generator.'''
277 return _flatten(func(self, mapping, data) for func, data in
280 return _flatten(func(self, mapping, data) for func, data in
278 self._load(t))
281 self._load(t))
282 return _flatten(runtemplate(self, mapping, self._load(t)))
279
283
280 engines = {'default': engine}
284 engines = {'default': engine}
281
285
282 class templater(object):
286 class templater(object):
283
287
284 def __init__(self, mapfile, filters={}, defaults={}, cache={},
288 def __init__(self, mapfile, filters={}, defaults={}, cache={},
285 minchunk=1024, maxchunk=65536):
289 minchunk=1024, maxchunk=65536):
286 '''set up template engine.
290 '''set up template engine.
287 mapfile is name of file to read map definitions from.
291 mapfile is name of file to read map definitions from.
288 filters is dict of functions. each transforms a value into another.
292 filters is dict of functions. each transforms a value into another.
289 defaults is dict of default map definitions.'''
293 defaults is dict of default map definitions.'''
290 self.mapfile = mapfile or 'template'
294 self.mapfile = mapfile or 'template'
291 self.cache = cache.copy()
295 self.cache = cache.copy()
292 self.map = {}
296 self.map = {}
293 self.base = (mapfile and os.path.dirname(mapfile)) or ''
297 self.base = (mapfile and os.path.dirname(mapfile)) or ''
294 self.filters = templatefilters.filters.copy()
298 self.filters = templatefilters.filters.copy()
295 self.filters.update(filters)
299 self.filters.update(filters)
296 self.defaults = defaults
300 self.defaults = defaults
297 self.minchunk, self.maxchunk = minchunk, maxchunk
301 self.minchunk, self.maxchunk = minchunk, maxchunk
298 self.ecache = {}
302 self.ecache = {}
299
303
300 if not mapfile:
304 if not mapfile:
301 return
305 return
302 if not os.path.exists(mapfile):
306 if not os.path.exists(mapfile):
303 raise util.Abort(_('style not found: %s') % mapfile)
307 raise util.Abort(_('style not found: %s') % mapfile)
304
308
305 conf = config.config()
309 conf = config.config()
306 conf.read(mapfile)
310 conf.read(mapfile)
307
311
308 for key, val in conf[''].items():
312 for key, val in conf[''].items():
309 if not val:
313 if not val:
310 raise SyntaxError(_('%s: missing value') % conf.source('', key))
314 raise SyntaxError(_('%s: missing value') % conf.source('', key))
311 if val[0] in "'\"":
315 if val[0] in "'\"":
312 try:
316 try:
313 self.cache[key] = parsestring(val)
317 self.cache[key] = parsestring(val)
314 except SyntaxError, inst:
318 except SyntaxError, inst:
315 raise SyntaxError('%s: %s' %
319 raise SyntaxError('%s: %s' %
316 (conf.source('', key), inst.args[0]))
320 (conf.source('', key), inst.args[0]))
317 else:
321 else:
318 val = 'default', val
322 val = 'default', val
319 if ':' in val[1]:
323 if ':' in val[1]:
320 val = val[1].split(':', 1)
324 val = val[1].split(':', 1)
321 self.map[key] = val[0], os.path.join(self.base, val[1])
325 self.map[key] = val[0], os.path.join(self.base, val[1])
322
326
323 def __contains__(self, key):
327 def __contains__(self, key):
324 return key in self.cache or key in self.map
328 return key in self.cache or key in self.map
325
329
326 def load(self, t):
330 def load(self, t):
327 '''Get the template for the given template name. Use a local cache.'''
331 '''Get the template for the given template name. Use a local cache.'''
328 if t not in self.cache:
332 if t not in self.cache:
329 try:
333 try:
330 self.cache[t] = util.readfile(self.map[t][1])
334 self.cache[t] = util.readfile(self.map[t][1])
331 except KeyError, inst:
335 except KeyError, inst:
332 raise util.Abort(_('"%s" not in template map') % inst.args[0])
336 raise util.Abort(_('"%s" not in template map') % inst.args[0])
333 except IOError, inst:
337 except IOError, inst:
334 raise IOError(inst.args[0], _('template file %s: %s') %
338 raise IOError(inst.args[0], _('template file %s: %s') %
335 (self.map[t][1], inst.args[1]))
339 (self.map[t][1], inst.args[1]))
336 return self.cache[t]
340 return self.cache[t]
337
341
338 def __call__(self, t, **mapping):
342 def __call__(self, t, **mapping):
339 ttype = t in self.map and self.map[t][0] or 'default'
343 ttype = t in self.map and self.map[t][0] or 'default'
340 if ttype not in self.ecache:
344 if ttype not in self.ecache:
341 self.ecache[ttype] = engines[ttype](self.load,
345 self.ecache[ttype] = engines[ttype](self.load,
342 self.filters, self.defaults)
346 self.filters, self.defaults)
343 proc = self.ecache[ttype]
347 proc = self.ecache[ttype]
344
348
345 stream = proc.process(t, mapping)
349 stream = proc.process(t, mapping)
346 if self.minchunk:
350 if self.minchunk:
347 stream = util.increasingchunks(stream, min=self.minchunk,
351 stream = util.increasingchunks(stream, min=self.minchunk,
348 max=self.maxchunk)
352 max=self.maxchunk)
349 return stream
353 return stream
350
354
351 def templatepath(name=None):
355 def templatepath(name=None):
352 '''return location of template file or directory (if no name).
356 '''return location of template file or directory (if no name).
353 returns None if not found.'''
357 returns None if not found.'''
354 normpaths = []
358 normpaths = []
355
359
356 # executable version (py2exe) doesn't support __file__
360 # executable version (py2exe) doesn't support __file__
357 if util.mainfrozen():
361 if util.mainfrozen():
358 module = sys.executable
362 module = sys.executable
359 else:
363 else:
360 module = __file__
364 module = __file__
361 for f in path:
365 for f in path:
362 if f.startswith('/'):
366 if f.startswith('/'):
363 p = f
367 p = f
364 else:
368 else:
365 fl = f.split('/')
369 fl = f.split('/')
366 p = os.path.join(os.path.dirname(module), *fl)
370 p = os.path.join(os.path.dirname(module), *fl)
367 if name:
371 if name:
368 p = os.path.join(p, name)
372 p = os.path.join(p, name)
369 if name and os.path.exists(p):
373 if name and os.path.exists(p):
370 return os.path.normpath(p)
374 return os.path.normpath(p)
371 elif os.path.isdir(p):
375 elif os.path.isdir(p):
372 normpaths.append(os.path.normpath(p))
376 normpaths.append(os.path.normpath(p))
373
377
374 return normpaths
378 return normpaths
375
379
376 def stylemap(styles, paths=None):
380 def stylemap(styles, paths=None):
377 """Return path to mapfile for a given style.
381 """Return path to mapfile for a given style.
378
382
379 Searches mapfile in the following locations:
383 Searches mapfile in the following locations:
380 1. templatepath/style/map
384 1. templatepath/style/map
381 2. templatepath/map-style
385 2. templatepath/map-style
382 3. templatepath/map
386 3. templatepath/map
383 """
387 """
384
388
385 if paths is None:
389 if paths is None:
386 paths = templatepath()
390 paths = templatepath()
387 elif isinstance(paths, str):
391 elif isinstance(paths, str):
388 paths = [paths]
392 paths = [paths]
389
393
390 if isinstance(styles, str):
394 if isinstance(styles, str):
391 styles = [styles]
395 styles = [styles]
392
396
393 for style in styles:
397 for style in styles:
394 if not style:
398 if not style:
395 continue
399 continue
396 locations = [os.path.join(style, 'map'), 'map-' + style]
400 locations = [os.path.join(style, 'map'), 'map-' + style]
397 locations.append('map')
401 locations.append('map')
398
402
399 for path in paths:
403 for path in paths:
400 for location in locations:
404 for location in locations:
401 mapfile = os.path.join(path, location)
405 mapfile = os.path.join(path, location)
402 if os.path.isfile(mapfile):
406 if os.path.isfile(mapfile):
403 return style, mapfile
407 return style, mapfile
404
408
405 raise RuntimeError("No hgweb templates found in %r" % paths)
409 raise RuntimeError("No hgweb templates found in %r" % paths)
General Comments 0
You need to be logged in to leave comments. Login now