##// END OF EJS Templates
Allow hgweb to search for templates in more than one path....
Brendan Cully -
r7107:125c8fed default
parent child Browse files
Show More
@@ -1,121 +1,124 b''
1 # hgweb/common.py - Utility functions needed by hgweb_mod and hgwebdir_mod
1 # hgweb/common.py - Utility functions needed by hgweb_mod and hgwebdir_mod
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms
6 # This software may be used and distributed according to the terms
7 # of the GNU General Public License, incorporated herein by reference.
7 # of the GNU General Public License, incorporated herein by reference.
8
8
9 import errno, mimetypes, os
9 import errno, mimetypes, os
10
10
11 HTTP_OK = 200
11 HTTP_OK = 200
12 HTTP_BAD_REQUEST = 400
12 HTTP_BAD_REQUEST = 400
13 HTTP_UNAUTHORIZED = 401
13 HTTP_UNAUTHORIZED = 401
14 HTTP_FORBIDDEN = 403
14 HTTP_FORBIDDEN = 403
15 HTTP_NOT_FOUND = 404
15 HTTP_NOT_FOUND = 404
16 HTTP_METHOD_NOT_ALLOWED = 405
16 HTTP_METHOD_NOT_ALLOWED = 405
17 HTTP_SERVER_ERROR = 500
17 HTTP_SERVER_ERROR = 500
18
18
19 class ErrorResponse(Exception):
19 class ErrorResponse(Exception):
20 def __init__(self, code, message=None):
20 def __init__(self, code, message=None):
21 Exception.__init__(self)
21 Exception.__init__(self)
22 self.code = code
22 self.code = code
23 if message is not None:
23 if message is not None:
24 self.message = message
24 self.message = message
25 else:
25 else:
26 self.message = _statusmessage(code)
26 self.message = _statusmessage(code)
27
27
28 def _statusmessage(code):
28 def _statusmessage(code):
29 from BaseHTTPServer import BaseHTTPRequestHandler
29 from BaseHTTPServer import BaseHTTPRequestHandler
30 responses = BaseHTTPRequestHandler.responses
30 responses = BaseHTTPRequestHandler.responses
31 return responses.get(code, ('Error', 'Unknown error'))[0]
31 return responses.get(code, ('Error', 'Unknown error'))[0]
32
32
33 def statusmessage(code):
33 def statusmessage(code):
34 return '%d %s' % (code, _statusmessage(code))
34 return '%d %s' % (code, _statusmessage(code))
35
35
36 def get_mtime(repo_path):
36 def get_mtime(repo_path):
37 store_path = os.path.join(repo_path, ".hg")
37 store_path = os.path.join(repo_path, ".hg")
38 if not os.path.isdir(os.path.join(store_path, "data")):
38 if not os.path.isdir(os.path.join(store_path, "data")):
39 store_path = os.path.join(store_path, "store")
39 store_path = os.path.join(store_path, "store")
40 cl_path = os.path.join(store_path, "00changelog.i")
40 cl_path = os.path.join(store_path, "00changelog.i")
41 if os.path.exists(cl_path):
41 if os.path.exists(cl_path):
42 return os.stat(cl_path).st_mtime
42 return os.stat(cl_path).st_mtime
43 else:
43 else:
44 return os.stat(store_path).st_mtime
44 return os.stat(store_path).st_mtime
45
45
46 def staticfile(directory, fname, req):
46 def staticfile(directory, fname, req):
47 """return a file inside directory with guessed Content-Type header
47 """return a file inside directory with guessed Content-Type header
48
48
49 fname always uses '/' as directory separator and isn't allowed to
49 fname always uses '/' as directory separator and isn't allowed to
50 contain unusual path components.
50 contain unusual path components.
51 Content-Type is guessed using the mimetypes module.
51 Content-Type is guessed using the mimetypes module.
52 Return an empty string if fname is illegal or file not found.
52 Return an empty string if fname is illegal or file not found.
53
53
54 """
54 """
55 parts = fname.split('/')
55 parts = fname.split('/')
56 path = directory
56 path = directory
57 for part in parts:
57 for part in parts:
58 if (part in ('', os.curdir, os.pardir) or
58 if (part in ('', os.curdir, os.pardir) or
59 os.sep in part or os.altsep is not None and os.altsep in part):
59 os.sep in part or os.altsep is not None and os.altsep in part):
60 return ""
60 return ""
61 path = os.path.join(path, part)
61 path = os.path.join(path, part)
62 try:
62 try:
63 os.stat(path)
63 os.stat(path)
64 ct = mimetypes.guess_type(path)[0] or "text/plain"
64 ct = mimetypes.guess_type(path)[0] or "text/plain"
65 req.respond(HTTP_OK, ct, length = os.path.getsize(path))
65 req.respond(HTTP_OK, ct, length = os.path.getsize(path))
66 return file(path, 'rb').read()
66 return file(path, 'rb').read()
67 except TypeError:
67 except TypeError:
68 raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal file name')
68 raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal file name')
69 except OSError, err:
69 except OSError, err:
70 if err.errno == errno.ENOENT:
70 if err.errno == errno.ENOENT:
71 raise ErrorResponse(HTTP_NOT_FOUND)
71 raise ErrorResponse(HTTP_NOT_FOUND)
72 else:
72 else:
73 raise ErrorResponse(HTTP_SERVER_ERROR, err.strerror)
73 raise ErrorResponse(HTTP_SERVER_ERROR, err.strerror)
74
74
75 def style_map(templatepath, style):
75 def style_map(templatepath, style):
76 """Return path to mapfile for a given style.
76 """Return path to mapfile for a given style.
77
77
78 Searches mapfile in the following locations:
78 Searches mapfile in the following locations:
79 1. templatepath/style/map
79 1. templatepath/style/map
80 2. templatepath/map-style
80 2. templatepath/map-style
81 3. templatepath/map
81 3. templatepath/map
82 """
82 """
83 locations = style and [os.path.join(style, "map"), "map-"+style] or []
83 locations = style and [os.path.join(style, "map"), "map-"+style] or []
84 locations.append("map")
84 locations.append("map")
85 for location in locations:
85 if isinstance(templatepath, str):
86 mapfile = os.path.join(templatepath, location)
86 templatepath = [templatepath]
87 if os.path.isfile(mapfile):
87 for path in templatepath:
88 return mapfile
88 for location in locations:
89 mapfile = os.path.join(path, location)
90 if os.path.isfile(mapfile):
91 return mapfile
89 raise RuntimeError("No hgweb templates found in %r" % templatepath)
92 raise RuntimeError("No hgweb templates found in %r" % templatepath)
90
93
91 def paritygen(stripecount, offset=0):
94 def paritygen(stripecount, offset=0):
92 """count parity of horizontal stripes for easier reading"""
95 """count parity of horizontal stripes for easier reading"""
93 if stripecount and offset:
96 if stripecount and offset:
94 # account for offset, e.g. due to building the list in reverse
97 # account for offset, e.g. due to building the list in reverse
95 count = (stripecount + offset) % stripecount
98 count = (stripecount + offset) % stripecount
96 parity = (stripecount + offset) / stripecount & 1
99 parity = (stripecount + offset) / stripecount & 1
97 else:
100 else:
98 count = 0
101 count = 0
99 parity = 0
102 parity = 0
100 while True:
103 while True:
101 yield parity
104 yield parity
102 count += 1
105 count += 1
103 if stripecount and count >= stripecount:
106 if stripecount and count >= stripecount:
104 parity = 1 - parity
107 parity = 1 - parity
105 count = 0
108 count = 0
106
109
107 def countgen(start=0, step=1):
110 def countgen(start=0, step=1):
108 """count forever -- useful for line numbers"""
111 """count forever -- useful for line numbers"""
109 while True:
112 while True:
110 yield start
113 yield start
111 start += step
114 start += step
112
115
113 def get_contact(config):
116 def get_contact(config):
114 """Return repo contact information or empty string.
117 """Return repo contact information or empty string.
115
118
116 web.contact is the primary source, but if that is not set, try
119 web.contact is the primary source, but if that is not set, try
117 ui.username or $EMAIL as a fallback to display something useful.
120 ui.username or $EMAIL as a fallback to display something useful.
118 """
121 """
119 return (config("web", "contact") or
122 return (config("web", "contact") or
120 config("ui", "username") or
123 config("ui", "username") or
121 os.environ.get("EMAIL") or "")
124 os.environ.get("EMAIL") or "")
@@ -1,284 +1,284 b''
1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms
6 # This software may be used and distributed according to the terms
7 # of the GNU General Public License, incorporated herein by reference.
7 # of the GNU General Public License, incorporated herein by reference.
8
8
9 import os
9 import os
10 from mercurial.i18n import gettext as _
10 from mercurial.i18n import gettext as _
11 from mercurial.repo import RepoError
11 from mercurial.repo import RepoError
12 from mercurial import ui, hg, util, templater, templatefilters
12 from mercurial import ui, hg, util, templater, templatefilters
13 from common import ErrorResponse, get_mtime, staticfile, style_map, paritygen,\
13 from common import ErrorResponse, get_mtime, staticfile, style_map, paritygen,\
14 get_contact, HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
14 get_contact, HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
15 from hgweb_mod import hgweb
15 from hgweb_mod import hgweb
16 from request import wsgirequest
16 from request import wsgirequest
17
17
18 # This is a stopgap
18 # This is a stopgap
19 class hgwebdir(object):
19 class hgwebdir(object):
20 def __init__(self, config, parentui=None):
20 def __init__(self, config, parentui=None):
21 def cleannames(items):
21 def cleannames(items):
22 return [(util.pconvert(name).strip('/'), path)
22 return [(util.pconvert(name).strip('/'), path)
23 for name, path in items]
23 for name, path in items]
24
24
25 self.parentui = parentui or ui.ui(report_untrusted=False,
25 self.parentui = parentui or ui.ui(report_untrusted=False,
26 interactive = False)
26 interactive = False)
27 self.motd = None
27 self.motd = None
28 self.style = None
28 self.style = None
29 self.stripecount = None
29 self.stripecount = None
30 self.repos_sorted = ('name', False)
30 self.repos_sorted = ('name', False)
31 self._baseurl = None
31 self._baseurl = None
32 if isinstance(config, (list, tuple)):
32 if isinstance(config, (list, tuple)):
33 self.repos = cleannames(config)
33 self.repos = cleannames(config)
34 self.repos_sorted = ('', False)
34 self.repos_sorted = ('', False)
35 elif isinstance(config, dict):
35 elif isinstance(config, dict):
36 self.repos = util.sort(cleannames(config.items()))
36 self.repos = util.sort(cleannames(config.items()))
37 else:
37 else:
38 if isinstance(config, util.configparser):
38 if isinstance(config, util.configparser):
39 cp = config
39 cp = config
40 else:
40 else:
41 cp = util.configparser()
41 cp = util.configparser()
42 cp.read(config)
42 cp.read(config)
43 self.repos = []
43 self.repos = []
44 if cp.has_section('web'):
44 if cp.has_section('web'):
45 if cp.has_option('web', 'motd'):
45 if cp.has_option('web', 'motd'):
46 self.motd = cp.get('web', 'motd')
46 self.motd = cp.get('web', 'motd')
47 if cp.has_option('web', 'style'):
47 if cp.has_option('web', 'style'):
48 self.style = cp.get('web', 'style')
48 self.style = cp.get('web', 'style')
49 if cp.has_option('web', 'stripes'):
49 if cp.has_option('web', 'stripes'):
50 self.stripecount = int(cp.get('web', 'stripes'))
50 self.stripecount = int(cp.get('web', 'stripes'))
51 if cp.has_option('web', 'baseurl'):
51 if cp.has_option('web', 'baseurl'):
52 self._baseurl = cp.get('web', 'baseurl')
52 self._baseurl = cp.get('web', 'baseurl')
53 if cp.has_section('paths'):
53 if cp.has_section('paths'):
54 self.repos.extend(cleannames(cp.items('paths')))
54 self.repos.extend(cleannames(cp.items('paths')))
55 if cp.has_section('collections'):
55 if cp.has_section('collections'):
56 for prefix, root in cp.items('collections'):
56 for prefix, root in cp.items('collections'):
57 for path in util.walkrepos(root, followsym=True):
57 for path in util.walkrepos(root, followsym=True):
58 repo = os.path.normpath(path)
58 repo = os.path.normpath(path)
59 name = repo
59 name = repo
60 if name.startswith(prefix):
60 if name.startswith(prefix):
61 name = name[len(prefix):]
61 name = name[len(prefix):]
62 self.repos.append((name.lstrip(os.sep), repo))
62 self.repos.append((name.lstrip(os.sep), repo))
63 self.repos.sort()
63 self.repos.sort()
64
64
65 def run(self):
65 def run(self):
66 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
66 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
67 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
67 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
68 import mercurial.hgweb.wsgicgi as wsgicgi
68 import mercurial.hgweb.wsgicgi as wsgicgi
69 wsgicgi.launch(self)
69 wsgicgi.launch(self)
70
70
71 def __call__(self, env, respond):
71 def __call__(self, env, respond):
72 req = wsgirequest(env, respond)
72 req = wsgirequest(env, respond)
73 return self.run_wsgi(req)
73 return self.run_wsgi(req)
74
74
75 def run_wsgi(self, req):
75 def run_wsgi(self, req):
76
76
77 try:
77 try:
78 try:
78 try:
79
79
80 virtual = req.env.get("PATH_INFO", "").strip('/')
80 virtual = req.env.get("PATH_INFO", "").strip('/')
81 tmpl = self.templater(req)
81 tmpl = self.templater(req)
82 ctype = tmpl('mimetype', encoding=util._encoding)
82 ctype = tmpl('mimetype', encoding=util._encoding)
83 ctype = templater.stringify(ctype)
83 ctype = templater.stringify(ctype)
84
84
85 # a static file
85 # a static file
86 if virtual.startswith('static/') or 'static' in req.form:
86 if virtual.startswith('static/') or 'static' in req.form:
87 static = os.path.join(templater.templatepath(), 'static')
88 if virtual.startswith('static/'):
87 if virtual.startswith('static/'):
89 fname = virtual[7:]
88 fname = virtual[7:]
90 else:
89 else:
91 fname = req.form['static'][0]
90 fname = req.form['static'][0]
91 static = templater.templatepath('static')
92 return staticfile(static, fname, req)
92 return staticfile(static, fname, req)
93
93
94 # top-level index
94 # top-level index
95 elif not virtual:
95 elif not virtual:
96 req.respond(HTTP_OK, ctype)
96 req.respond(HTTP_OK, ctype)
97 return ''.join(self.makeindex(req, tmpl)),
97 return ''.join(self.makeindex(req, tmpl)),
98
98
99 # nested indexes and hgwebs
99 # nested indexes and hgwebs
100
100
101 repos = dict(self.repos)
101 repos = dict(self.repos)
102 while virtual:
102 while virtual:
103 real = repos.get(virtual)
103 real = repos.get(virtual)
104 if real:
104 if real:
105 req.env['REPO_NAME'] = virtual
105 req.env['REPO_NAME'] = virtual
106 try:
106 try:
107 repo = hg.repository(self.parentui, real)
107 repo = hg.repository(self.parentui, real)
108 return hgweb(repo).run_wsgi(req)
108 return hgweb(repo).run_wsgi(req)
109 except IOError, inst:
109 except IOError, inst:
110 msg = inst.strerror
110 msg = inst.strerror
111 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
111 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
112 except RepoError, inst:
112 except RepoError, inst:
113 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
113 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
114
114
115 # browse subdirectories
115 # browse subdirectories
116 subdir = virtual + '/'
116 subdir = virtual + '/'
117 if [r for r in repos if r.startswith(subdir)]:
117 if [r for r in repos if r.startswith(subdir)]:
118 req.respond(HTTP_OK, ctype)
118 req.respond(HTTP_OK, ctype)
119 return ''.join(self.makeindex(req, tmpl, subdir)),
119 return ''.join(self.makeindex(req, tmpl, subdir)),
120
120
121 up = virtual.rfind('/')
121 up = virtual.rfind('/')
122 if up < 0:
122 if up < 0:
123 break
123 break
124 virtual = virtual[:up]
124 virtual = virtual[:up]
125
125
126 # prefixes not found
126 # prefixes not found
127 req.respond(HTTP_NOT_FOUND, ctype)
127 req.respond(HTTP_NOT_FOUND, ctype)
128 return ''.join(tmpl("notfound", repo=virtual)),
128 return ''.join(tmpl("notfound", repo=virtual)),
129
129
130 except ErrorResponse, err:
130 except ErrorResponse, err:
131 req.respond(err.code, ctype)
131 req.respond(err.code, ctype)
132 return ''.join(tmpl('error', error=err.message or '')),
132 return ''.join(tmpl('error', error=err.message or '')),
133 finally:
133 finally:
134 tmpl = None
134 tmpl = None
135
135
136 def makeindex(self, req, tmpl, subdir=""):
136 def makeindex(self, req, tmpl, subdir=""):
137
137
138 def archivelist(ui, nodeid, url):
138 def archivelist(ui, nodeid, url):
139 allowed = ui.configlist("web", "allow_archive", untrusted=True)
139 allowed = ui.configlist("web", "allow_archive", untrusted=True)
140 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
140 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
141 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
141 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
142 untrusted=True):
142 untrusted=True):
143 yield {"type" : i[0], "extension": i[1],
143 yield {"type" : i[0], "extension": i[1],
144 "node": nodeid, "url": url}
144 "node": nodeid, "url": url}
145
145
146 def entries(sortcolumn="", descending=False, subdir="", **map):
146 def entries(sortcolumn="", descending=False, subdir="", **map):
147 def sessionvars(**map):
147 def sessionvars(**map):
148 fields = []
148 fields = []
149 if 'style' in req.form:
149 if 'style' in req.form:
150 style = req.form['style'][0]
150 style = req.form['style'][0]
151 if style != get('web', 'style', ''):
151 if style != get('web', 'style', ''):
152 fields.append(('style', style))
152 fields.append(('style', style))
153
153
154 separator = url[-1] == '?' and ';' or '?'
154 separator = url[-1] == '?' and ';' or '?'
155 for name, value in fields:
155 for name, value in fields:
156 yield dict(name=name, value=value, separator=separator)
156 yield dict(name=name, value=value, separator=separator)
157 separator = ';'
157 separator = ';'
158
158
159 rows = []
159 rows = []
160 parity = paritygen(self.stripecount)
160 parity = paritygen(self.stripecount)
161 for name, path in self.repos:
161 for name, path in self.repos:
162 if not name.startswith(subdir):
162 if not name.startswith(subdir):
163 continue
163 continue
164 name = name[len(subdir):]
164 name = name[len(subdir):]
165
165
166 u = ui.ui(parentui=self.parentui)
166 u = ui.ui(parentui=self.parentui)
167 try:
167 try:
168 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
168 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
169 except Exception, e:
169 except Exception, e:
170 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
170 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
171 continue
171 continue
172 def get(section, name, default=None):
172 def get(section, name, default=None):
173 return u.config(section, name, default, untrusted=True)
173 return u.config(section, name, default, untrusted=True)
174
174
175 if u.configbool("web", "hidden", untrusted=True):
175 if u.configbool("web", "hidden", untrusted=True):
176 continue
176 continue
177
177
178 parts = [name]
178 parts = [name]
179 if 'PATH_INFO' in req.env:
179 if 'PATH_INFO' in req.env:
180 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
180 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
181 if req.env['SCRIPT_NAME']:
181 if req.env['SCRIPT_NAME']:
182 parts.insert(0, req.env['SCRIPT_NAME'])
182 parts.insert(0, req.env['SCRIPT_NAME'])
183 url = ('/'.join(parts).replace("//", "/")) + '/'
183 url = ('/'.join(parts).replace("//", "/")) + '/'
184
184
185 # update time with local timezone
185 # update time with local timezone
186 try:
186 try:
187 d = (get_mtime(path), util.makedate()[1])
187 d = (get_mtime(path), util.makedate()[1])
188 except OSError:
188 except OSError:
189 continue
189 continue
190
190
191 contact = get_contact(get)
191 contact = get_contact(get)
192 description = get("web", "description", "")
192 description = get("web", "description", "")
193 name = get("web", "name", name)
193 name = get("web", "name", name)
194 row = dict(contact=contact or "unknown",
194 row = dict(contact=contact or "unknown",
195 contact_sort=contact.upper() or "unknown",
195 contact_sort=contact.upper() or "unknown",
196 name=name,
196 name=name,
197 name_sort=name,
197 name_sort=name,
198 url=url,
198 url=url,
199 description=description or "unknown",
199 description=description or "unknown",
200 description_sort=description.upper() or "unknown",
200 description_sort=description.upper() or "unknown",
201 lastchange=d,
201 lastchange=d,
202 lastchange_sort=d[1]-d[0],
202 lastchange_sort=d[1]-d[0],
203 sessionvars=sessionvars,
203 sessionvars=sessionvars,
204 archives=archivelist(u, "tip", url))
204 archives=archivelist(u, "tip", url))
205 if (not sortcolumn
205 if (not sortcolumn
206 or (sortcolumn, descending) == self.repos_sorted):
206 or (sortcolumn, descending) == self.repos_sorted):
207 # fast path for unsorted output
207 # fast path for unsorted output
208 row['parity'] = parity.next()
208 row['parity'] = parity.next()
209 yield row
209 yield row
210 else:
210 else:
211 rows.append((row["%s_sort" % sortcolumn], row))
211 rows.append((row["%s_sort" % sortcolumn], row))
212 if rows:
212 if rows:
213 rows.sort()
213 rows.sort()
214 if descending:
214 if descending:
215 rows.reverse()
215 rows.reverse()
216 for key, row in rows:
216 for key, row in rows:
217 row['parity'] = parity.next()
217 row['parity'] = parity.next()
218 yield row
218 yield row
219
219
220 sortable = ["name", "description", "contact", "lastchange"]
220 sortable = ["name", "description", "contact", "lastchange"]
221 sortcolumn, descending = self.repos_sorted
221 sortcolumn, descending = self.repos_sorted
222 if 'sort' in req.form:
222 if 'sort' in req.form:
223 sortcolumn = req.form['sort'][0]
223 sortcolumn = req.form['sort'][0]
224 descending = sortcolumn.startswith('-')
224 descending = sortcolumn.startswith('-')
225 if descending:
225 if descending:
226 sortcolumn = sortcolumn[1:]
226 sortcolumn = sortcolumn[1:]
227 if sortcolumn not in sortable:
227 if sortcolumn not in sortable:
228 sortcolumn = ""
228 sortcolumn = ""
229
229
230 sort = [("sort_%s" % column,
230 sort = [("sort_%s" % column,
231 "%s%s" % ((not descending and column == sortcolumn)
231 "%s%s" % ((not descending and column == sortcolumn)
232 and "-" or "", column))
232 and "-" or "", column))
233 for column in sortable]
233 for column in sortable]
234
234
235 if self._baseurl is not None:
235 if self._baseurl is not None:
236 req.env['SCRIPT_NAME'] = self._baseurl
236 req.env['SCRIPT_NAME'] = self._baseurl
237
237
238 return tmpl("index", entries=entries, subdir=subdir,
238 return tmpl("index", entries=entries, subdir=subdir,
239 sortcolumn=sortcolumn, descending=descending,
239 sortcolumn=sortcolumn, descending=descending,
240 **dict(sort))
240 **dict(sort))
241
241
242 def templater(self, req):
242 def templater(self, req):
243
243
244 def header(**map):
244 def header(**map):
245 yield tmpl('header', encoding=util._encoding, **map)
245 yield tmpl('header', encoding=util._encoding, **map)
246
246
247 def footer(**map):
247 def footer(**map):
248 yield tmpl("footer", **map)
248 yield tmpl("footer", **map)
249
249
250 def motd(**map):
250 def motd(**map):
251 if self.motd is not None:
251 if self.motd is not None:
252 yield self.motd
252 yield self.motd
253 else:
253 else:
254 yield config('web', 'motd', '')
254 yield config('web', 'motd', '')
255
255
256 def config(section, name, default=None, untrusted=True):
256 def config(section, name, default=None, untrusted=True):
257 return self.parentui.config(section, name, default, untrusted)
257 return self.parentui.config(section, name, default, untrusted)
258
258
259 if self._baseurl is not None:
259 if self._baseurl is not None:
260 req.env['SCRIPT_NAME'] = self._baseurl
260 req.env['SCRIPT_NAME'] = self._baseurl
261
261
262 url = req.env.get('SCRIPT_NAME', '')
262 url = req.env.get('SCRIPT_NAME', '')
263 if not url.endswith('/'):
263 if not url.endswith('/'):
264 url += '/'
264 url += '/'
265
265
266 staticurl = config('web', 'staticurl') or url + 'static/'
266 staticurl = config('web', 'staticurl') or url + 'static/'
267 if not staticurl.endswith('/'):
267 if not staticurl.endswith('/'):
268 staticurl += '/'
268 staticurl += '/'
269
269
270 style = self.style
270 style = self.style
271 if style is None:
271 if style is None:
272 style = config('web', 'style', '')
272 style = config('web', 'style', '')
273 if 'style' in req.form:
273 if 'style' in req.form:
274 style = req.form['style'][0]
274 style = req.form['style'][0]
275 if self.stripecount is None:
275 if self.stripecount is None:
276 self.stripecount = int(config('web', 'stripes', 1))
276 self.stripecount = int(config('web', 'stripes', 1))
277 mapfile = style_map(templater.templatepath(), style)
277 mapfile = style_map(templater.templatepath(), style)
278 tmpl = templater.templater(mapfile, templatefilters.filters,
278 tmpl = templater.templater(mapfile, templatefilters.filters,
279 defaults={"header": header,
279 defaults={"header": header,
280 "footer": footer,
280 "footer": footer,
281 "motd": motd,
281 "motd": motd,
282 "url": url,
282 "url": url,
283 "staticurl": staticurl})
283 "staticurl": staticurl})
284 return tmpl
284 return tmpl
@@ -1,608 +1,614 b''
1 #
1 #
2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
6 # of the GNU General Public License, incorporated herein by reference.
7
7
8 import os, mimetypes, re, cgi
8 import os, mimetypes, re, cgi
9 import webutil
9 import webutil
10 from mercurial import revlog, archival, templatefilters
10 from mercurial import revlog, archival, templatefilters
11 from mercurial.node import short, hex, nullid
11 from mercurial.node import short, hex, nullid
12 from mercurial.util import binary, datestr
12 from mercurial.util import binary, datestr
13 from mercurial.repo import RepoError
13 from mercurial.repo import RepoError
14 from common import paritygen, staticfile, get_contact, ErrorResponse
14 from common import paritygen, staticfile, get_contact, ErrorResponse
15 from common import HTTP_OK, HTTP_FORBIDDEN, HTTP_NOT_FOUND
15 from common import HTTP_OK, HTTP_FORBIDDEN, HTTP_NOT_FOUND
16 from mercurial import graphmod, util
16 from mercurial import graphmod, util
17
17
18 # __all__ is populated with the allowed commands. Be sure to add to it if
18 # __all__ is populated with the allowed commands. Be sure to add to it if
19 # you're adding a new command, or the new command won't work.
19 # you're adding a new command, or the new command won't work.
20
20
21 __all__ = [
21 __all__ = [
22 'log', 'rawfile', 'file', 'changelog', 'shortlog', 'changeset', 'rev',
22 'log', 'rawfile', 'file', 'changelog', 'shortlog', 'changeset', 'rev',
23 'manifest', 'tags', 'summary', 'filediff', 'diff', 'annotate', 'filelog',
23 'manifest', 'tags', 'summary', 'filediff', 'diff', 'annotate', 'filelog',
24 'archive', 'static', 'graph',
24 'archive', 'static', 'graph',
25 ]
25 ]
26
26
27 def log(web, req, tmpl):
27 def log(web, req, tmpl):
28 if 'file' in req.form and req.form['file'][0]:
28 if 'file' in req.form and req.form['file'][0]:
29 return filelog(web, req, tmpl)
29 return filelog(web, req, tmpl)
30 else:
30 else:
31 return changelog(web, req, tmpl)
31 return changelog(web, req, tmpl)
32
32
33 def rawfile(web, req, tmpl):
33 def rawfile(web, req, tmpl):
34 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
34 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
35 if not path:
35 if not path:
36 content = manifest(web, req, tmpl)
36 content = manifest(web, req, tmpl)
37 req.respond(HTTP_OK, web.ctype)
37 req.respond(HTTP_OK, web.ctype)
38 return content
38 return content
39
39
40 try:
40 try:
41 fctx = webutil.filectx(web.repo, req)
41 fctx = webutil.filectx(web.repo, req)
42 except revlog.LookupError, inst:
42 except revlog.LookupError, inst:
43 try:
43 try:
44 content = manifest(web, req, tmpl)
44 content = manifest(web, req, tmpl)
45 req.respond(HTTP_OK, web.ctype)
45 req.respond(HTTP_OK, web.ctype)
46 return content
46 return content
47 except ErrorResponse:
47 except ErrorResponse:
48 raise inst
48 raise inst
49
49
50 path = fctx.path()
50 path = fctx.path()
51 text = fctx.data()
51 text = fctx.data()
52 mt = mimetypes.guess_type(path)[0]
52 mt = mimetypes.guess_type(path)[0]
53 if mt is None:
53 if mt is None:
54 mt = binary(text) and 'application/octet-stream' or 'text/plain'
54 mt = binary(text) and 'application/octet-stream' or 'text/plain'
55
55
56 req.respond(HTTP_OK, mt, path, len(text))
56 req.respond(HTTP_OK, mt, path, len(text))
57 return [text]
57 return [text]
58
58
59 def _filerevision(web, tmpl, fctx):
59 def _filerevision(web, tmpl, fctx):
60 f = fctx.path()
60 f = fctx.path()
61 text = fctx.data()
61 text = fctx.data()
62 fl = fctx.filelog()
62 fl = fctx.filelog()
63 n = fctx.filenode()
63 n = fctx.filenode()
64 parity = paritygen(web.stripecount)
64 parity = paritygen(web.stripecount)
65
65
66 if binary(text):
66 if binary(text):
67 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
67 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
68 text = '(binary:%s)' % mt
68 text = '(binary:%s)' % mt
69
69
70 def lines():
70 def lines():
71 for lineno, t in enumerate(text.splitlines(1)):
71 for lineno, t in enumerate(text.splitlines(1)):
72 yield {"line": t,
72 yield {"line": t,
73 "lineid": "l%d" % (lineno + 1),
73 "lineid": "l%d" % (lineno + 1),
74 "linenumber": "% 6d" % (lineno + 1),
74 "linenumber": "% 6d" % (lineno + 1),
75 "parity": parity.next()}
75 "parity": parity.next()}
76
76
77 return tmpl("filerevision",
77 return tmpl("filerevision",
78 file=f,
78 file=f,
79 path=webutil.up(f),
79 path=webutil.up(f),
80 text=lines(),
80 text=lines(),
81 rev=fctx.rev(),
81 rev=fctx.rev(),
82 node=hex(fctx.node()),
82 node=hex(fctx.node()),
83 author=fctx.user(),
83 author=fctx.user(),
84 date=fctx.date(),
84 date=fctx.date(),
85 desc=fctx.description(),
85 desc=fctx.description(),
86 branch=webutil.nodebranchnodefault(fctx),
86 branch=webutil.nodebranchnodefault(fctx),
87 parent=webutil.siblings(fctx.parents()),
87 parent=webutil.siblings(fctx.parents()),
88 child=webutil.siblings(fctx.children()),
88 child=webutil.siblings(fctx.children()),
89 rename=webutil.renamelink(fctx),
89 rename=webutil.renamelink(fctx),
90 permissions=fctx.manifest().flags(f))
90 permissions=fctx.manifest().flags(f))
91
91
92 def file(web, req, tmpl):
92 def file(web, req, tmpl):
93 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
93 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
94 if not path:
94 if not path:
95 return manifest(web, req, tmpl)
95 return manifest(web, req, tmpl)
96 try:
96 try:
97 return _filerevision(web, tmpl, webutil.filectx(web.repo, req))
97 return _filerevision(web, tmpl, webutil.filectx(web.repo, req))
98 except revlog.LookupError, inst:
98 except revlog.LookupError, inst:
99 try:
99 try:
100 return manifest(web, req, tmpl)
100 return manifest(web, req, tmpl)
101 except ErrorResponse:
101 except ErrorResponse:
102 raise inst
102 raise inst
103
103
104 def _search(web, tmpl, query):
104 def _search(web, tmpl, query):
105
105
106 def changelist(**map):
106 def changelist(**map):
107 cl = web.repo.changelog
107 cl = web.repo.changelog
108 count = 0
108 count = 0
109 qw = query.lower().split()
109 qw = query.lower().split()
110
110
111 def revgen():
111 def revgen():
112 for i in xrange(len(cl) - 1, 0, -100):
112 for i in xrange(len(cl) - 1, 0, -100):
113 l = []
113 l = []
114 for j in xrange(max(0, i - 100), i + 1):
114 for j in xrange(max(0, i - 100), i + 1):
115 ctx = web.repo[j]
115 ctx = web.repo[j]
116 l.append(ctx)
116 l.append(ctx)
117 l.reverse()
117 l.reverse()
118 for e in l:
118 for e in l:
119 yield e
119 yield e
120
120
121 for ctx in revgen():
121 for ctx in revgen():
122 miss = 0
122 miss = 0
123 for q in qw:
123 for q in qw:
124 if not (q in ctx.user().lower() or
124 if not (q in ctx.user().lower() or
125 q in ctx.description().lower() or
125 q in ctx.description().lower() or
126 q in " ".join(ctx.files()).lower()):
126 q in " ".join(ctx.files()).lower()):
127 miss = 1
127 miss = 1
128 break
128 break
129 if miss:
129 if miss:
130 continue
130 continue
131
131
132 count += 1
132 count += 1
133 n = ctx.node()
133 n = ctx.node()
134 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
134 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
135
135
136 yield tmpl('searchentry',
136 yield tmpl('searchentry',
137 parity=parity.next(),
137 parity=parity.next(),
138 author=ctx.user(),
138 author=ctx.user(),
139 parent=webutil.siblings(ctx.parents()),
139 parent=webutil.siblings(ctx.parents()),
140 child=webutil.siblings(ctx.children()),
140 child=webutil.siblings(ctx.children()),
141 changelogtag=showtags,
141 changelogtag=showtags,
142 desc=ctx.description(),
142 desc=ctx.description(),
143 date=ctx.date(),
143 date=ctx.date(),
144 files=web.listfilediffs(tmpl, ctx.files(), n),
144 files=web.listfilediffs(tmpl, ctx.files(), n),
145 rev=ctx.rev(),
145 rev=ctx.rev(),
146 node=hex(n),
146 node=hex(n),
147 tags=webutil.nodetagsdict(web.repo, n),
147 tags=webutil.nodetagsdict(web.repo, n),
148 inbranch=webutil.nodeinbranch(web.repo, ctx),
148 inbranch=webutil.nodeinbranch(web.repo, ctx),
149 branches=webutil.nodebranchdict(web.repo, ctx))
149 branches=webutil.nodebranchdict(web.repo, ctx))
150
150
151 if count >= web.maxchanges:
151 if count >= web.maxchanges:
152 break
152 break
153
153
154 cl = web.repo.changelog
154 cl = web.repo.changelog
155 parity = paritygen(web.stripecount)
155 parity = paritygen(web.stripecount)
156
156
157 return tmpl('search',
157 return tmpl('search',
158 query=query,
158 query=query,
159 node=hex(cl.tip()),
159 node=hex(cl.tip()),
160 entries=changelist,
160 entries=changelist,
161 archives=web.archivelist("tip"))
161 archives=web.archivelist("tip"))
162
162
163 def changelog(web, req, tmpl, shortlog = False):
163 def changelog(web, req, tmpl, shortlog = False):
164 if 'node' in req.form:
164 if 'node' in req.form:
165 ctx = webutil.changectx(web.repo, req)
165 ctx = webutil.changectx(web.repo, req)
166 else:
166 else:
167 if 'rev' in req.form:
167 if 'rev' in req.form:
168 hi = req.form['rev'][0]
168 hi = req.form['rev'][0]
169 else:
169 else:
170 hi = len(web.repo) - 1
170 hi = len(web.repo) - 1
171 try:
171 try:
172 ctx = web.repo[hi]
172 ctx = web.repo[hi]
173 except RepoError:
173 except RepoError:
174 return _search(web, tmpl, hi) # XXX redirect to 404 page?
174 return _search(web, tmpl, hi) # XXX redirect to 404 page?
175
175
176 def changelist(limit=0, **map):
176 def changelist(limit=0, **map):
177 cl = web.repo.changelog
177 cl = web.repo.changelog
178 l = [] # build a list in forward order for efficiency
178 l = [] # build a list in forward order for efficiency
179 for i in xrange(start, end):
179 for i in xrange(start, end):
180 ctx = web.repo[i]
180 ctx = web.repo[i]
181 n = ctx.node()
181 n = ctx.node()
182 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
182 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
183
183
184 l.insert(0, {"parity": parity.next(),
184 l.insert(0, {"parity": parity.next(),
185 "author": ctx.user(),
185 "author": ctx.user(),
186 "parent": webutil.siblings(ctx.parents(), i - 1),
186 "parent": webutil.siblings(ctx.parents(), i - 1),
187 "child": webutil.siblings(ctx.children(), i + 1),
187 "child": webutil.siblings(ctx.children(), i + 1),
188 "changelogtag": showtags,
188 "changelogtag": showtags,
189 "desc": ctx.description(),
189 "desc": ctx.description(),
190 "date": ctx.date(),
190 "date": ctx.date(),
191 "files": web.listfilediffs(tmpl, ctx.files(), n),
191 "files": web.listfilediffs(tmpl, ctx.files(), n),
192 "rev": i,
192 "rev": i,
193 "node": hex(n),
193 "node": hex(n),
194 "tags": webutil.nodetagsdict(web.repo, n),
194 "tags": webutil.nodetagsdict(web.repo, n),
195 "inbranch": webutil.nodeinbranch(web.repo, ctx),
195 "inbranch": webutil.nodeinbranch(web.repo, ctx),
196 "branches": webutil.nodebranchdict(web.repo, ctx)
196 "branches": webutil.nodebranchdict(web.repo, ctx)
197 })
197 })
198
198
199 if limit > 0:
199 if limit > 0:
200 l = l[:limit]
200 l = l[:limit]
201
201
202 for e in l:
202 for e in l:
203 yield e
203 yield e
204
204
205 maxchanges = shortlog and web.maxshortchanges or web.maxchanges
205 maxchanges = shortlog and web.maxshortchanges or web.maxchanges
206 cl = web.repo.changelog
206 cl = web.repo.changelog
207 count = len(cl)
207 count = len(cl)
208 pos = ctx.rev()
208 pos = ctx.rev()
209 start = max(0, pos - maxchanges + 1)
209 start = max(0, pos - maxchanges + 1)
210 end = min(count, start + maxchanges)
210 end = min(count, start + maxchanges)
211 pos = end - 1
211 pos = end - 1
212 parity = paritygen(web.stripecount, offset=start-end)
212 parity = paritygen(web.stripecount, offset=start-end)
213
213
214 changenav = webutil.revnavgen(pos, maxchanges, count, web.repo.changectx)
214 changenav = webutil.revnavgen(pos, maxchanges, count, web.repo.changectx)
215
215
216 return tmpl(shortlog and 'shortlog' or 'changelog',
216 return tmpl(shortlog and 'shortlog' or 'changelog',
217 changenav=changenav,
217 changenav=changenav,
218 node=hex(ctx.node()),
218 node=hex(ctx.node()),
219 rev=pos, changesets=count,
219 rev=pos, changesets=count,
220 entries=lambda **x: changelist(limit=0,**x),
220 entries=lambda **x: changelist(limit=0,**x),
221 latestentry=lambda **x: changelist(limit=1,**x),
221 latestentry=lambda **x: changelist(limit=1,**x),
222 archives=web.archivelist("tip"))
222 archives=web.archivelist("tip"))
223
223
224 def shortlog(web, req, tmpl):
224 def shortlog(web, req, tmpl):
225 return changelog(web, req, tmpl, shortlog = True)
225 return changelog(web, req, tmpl, shortlog = True)
226
226
227 def changeset(web, req, tmpl):
227 def changeset(web, req, tmpl):
228 ctx = webutil.changectx(web.repo, req)
228 ctx = webutil.changectx(web.repo, req)
229 n = ctx.node()
229 n = ctx.node()
230 showtags = webutil.showtag(web.repo, tmpl, 'changesettag', n)
230 showtags = webutil.showtag(web.repo, tmpl, 'changesettag', n)
231 parents = ctx.parents()
231 parents = ctx.parents()
232 p1 = parents[0].node()
232 p1 = parents[0].node()
233
233
234 files = []
234 files = []
235 parity = paritygen(web.stripecount)
235 parity = paritygen(web.stripecount)
236 for f in ctx.files():
236 for f in ctx.files():
237 files.append(tmpl("filenodelink",
237 files.append(tmpl("filenodelink",
238 node=hex(n), file=f,
238 node=hex(n), file=f,
239 parity=parity.next()))
239 parity=parity.next()))
240
240
241 diffs = web.diff(tmpl, p1, n, None)
241 diffs = web.diff(tmpl, p1, n, None)
242 return tmpl('changeset',
242 return tmpl('changeset',
243 diff=diffs,
243 diff=diffs,
244 rev=ctx.rev(),
244 rev=ctx.rev(),
245 node=hex(n),
245 node=hex(n),
246 parent=webutil.siblings(parents),
246 parent=webutil.siblings(parents),
247 child=webutil.siblings(ctx.children()),
247 child=webutil.siblings(ctx.children()),
248 changesettag=showtags,
248 changesettag=showtags,
249 author=ctx.user(),
249 author=ctx.user(),
250 desc=ctx.description(),
250 desc=ctx.description(),
251 date=ctx.date(),
251 date=ctx.date(),
252 files=files,
252 files=files,
253 archives=web.archivelist(hex(n)),
253 archives=web.archivelist(hex(n)),
254 tags=webutil.nodetagsdict(web.repo, n),
254 tags=webutil.nodetagsdict(web.repo, n),
255 branch=webutil.nodebranchnodefault(ctx),
255 branch=webutil.nodebranchnodefault(ctx),
256 inbranch=webutil.nodeinbranch(web.repo, ctx),
256 inbranch=webutil.nodeinbranch(web.repo, ctx),
257 branches=webutil.nodebranchdict(web.repo, ctx))
257 branches=webutil.nodebranchdict(web.repo, ctx))
258
258
259 rev = changeset
259 rev = changeset
260
260
261 def manifest(web, req, tmpl):
261 def manifest(web, req, tmpl):
262 ctx = webutil.changectx(web.repo, req)
262 ctx = webutil.changectx(web.repo, req)
263 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
263 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
264 mf = ctx.manifest()
264 mf = ctx.manifest()
265 node = ctx.node()
265 node = ctx.node()
266
266
267 files = {}
267 files = {}
268 parity = paritygen(web.stripecount)
268 parity = paritygen(web.stripecount)
269
269
270 if path and path[-1] != "/":
270 if path and path[-1] != "/":
271 path += "/"
271 path += "/"
272 l = len(path)
272 l = len(path)
273 abspath = "/" + path
273 abspath = "/" + path
274
274
275 for f, n in mf.items():
275 for f, n in mf.items():
276 if f[:l] != path:
276 if f[:l] != path:
277 continue
277 continue
278 remain = f[l:]
278 remain = f[l:]
279 idx = remain.find('/')
279 idx = remain.find('/')
280 if idx != -1:
280 if idx != -1:
281 remain = remain[:idx+1]
281 remain = remain[:idx+1]
282 n = None
282 n = None
283 files[remain] = (f, n)
283 files[remain] = (f, n)
284
284
285 if not files:
285 if not files:
286 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
286 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
287
287
288 def filelist(**map):
288 def filelist(**map):
289 for f in util.sort(files):
289 for f in util.sort(files):
290 full, fnode = files[f]
290 full, fnode = files[f]
291 if not fnode:
291 if not fnode:
292 continue
292 continue
293
293
294 fctx = ctx.filectx(full)
294 fctx = ctx.filectx(full)
295 yield {"file": full,
295 yield {"file": full,
296 "parity": parity.next(),
296 "parity": parity.next(),
297 "basename": f,
297 "basename": f,
298 "date": fctx.date(),
298 "date": fctx.date(),
299 "size": fctx.size(),
299 "size": fctx.size(),
300 "permissions": mf.flags(full)}
300 "permissions": mf.flags(full)}
301
301
302 def dirlist(**map):
302 def dirlist(**map):
303 for f in util.sort(files):
303 for f in util.sort(files):
304 full, fnode = files[f]
304 full, fnode = files[f]
305 if fnode:
305 if fnode:
306 continue
306 continue
307
307
308 yield {"parity": parity.next(),
308 yield {"parity": parity.next(),
309 "path": "%s%s" % (abspath, f),
309 "path": "%s%s" % (abspath, f),
310 "basename": f[:-1]}
310 "basename": f[:-1]}
311
311
312 return tmpl("manifest",
312 return tmpl("manifest",
313 rev=ctx.rev(),
313 rev=ctx.rev(),
314 node=hex(node),
314 node=hex(node),
315 path=abspath,
315 path=abspath,
316 up=webutil.up(abspath),
316 up=webutil.up(abspath),
317 upparity=parity.next(),
317 upparity=parity.next(),
318 fentries=filelist,
318 fentries=filelist,
319 dentries=dirlist,
319 dentries=dirlist,
320 archives=web.archivelist(hex(node)),
320 archives=web.archivelist(hex(node)),
321 tags=webutil.nodetagsdict(web.repo, node),
321 tags=webutil.nodetagsdict(web.repo, node),
322 inbranch=webutil.nodeinbranch(web.repo, ctx),
322 inbranch=webutil.nodeinbranch(web.repo, ctx),
323 branches=webutil.nodebranchdict(web.repo, ctx))
323 branches=webutil.nodebranchdict(web.repo, ctx))
324
324
325 def tags(web, req, tmpl):
325 def tags(web, req, tmpl):
326 i = web.repo.tagslist()
326 i = web.repo.tagslist()
327 i.reverse()
327 i.reverse()
328 parity = paritygen(web.stripecount)
328 parity = paritygen(web.stripecount)
329
329
330 def entries(notip=False,limit=0, **map):
330 def entries(notip=False,limit=0, **map):
331 count = 0
331 count = 0
332 for k, n in i:
332 for k, n in i:
333 if notip and k == "tip":
333 if notip and k == "tip":
334 continue
334 continue
335 if limit > 0 and count >= limit:
335 if limit > 0 and count >= limit:
336 continue
336 continue
337 count = count + 1
337 count = count + 1
338 yield {"parity": parity.next(),
338 yield {"parity": parity.next(),
339 "tag": k,
339 "tag": k,
340 "date": web.repo[n].date(),
340 "date": web.repo[n].date(),
341 "node": hex(n)}
341 "node": hex(n)}
342
342
343 return tmpl("tags",
343 return tmpl("tags",
344 node=hex(web.repo.changelog.tip()),
344 node=hex(web.repo.changelog.tip()),
345 entries=lambda **x: entries(False,0, **x),
345 entries=lambda **x: entries(False,0, **x),
346 entriesnotip=lambda **x: entries(True,0, **x),
346 entriesnotip=lambda **x: entries(True,0, **x),
347 latestentry=lambda **x: entries(True,1, **x))
347 latestentry=lambda **x: entries(True,1, **x))
348
348
349 def summary(web, req, tmpl):
349 def summary(web, req, tmpl):
350 i = web.repo.tagslist()
350 i = web.repo.tagslist()
351 i.reverse()
351 i.reverse()
352
352
353 def tagentries(**map):
353 def tagentries(**map):
354 parity = paritygen(web.stripecount)
354 parity = paritygen(web.stripecount)
355 count = 0
355 count = 0
356 for k, n in i:
356 for k, n in i:
357 if k == "tip": # skip tip
357 if k == "tip": # skip tip
358 continue
358 continue
359
359
360 count += 1
360 count += 1
361 if count > 10: # limit to 10 tags
361 if count > 10: # limit to 10 tags
362 break
362 break
363
363
364 yield tmpl("tagentry",
364 yield tmpl("tagentry",
365 parity=parity.next(),
365 parity=parity.next(),
366 tag=k,
366 tag=k,
367 node=hex(n),
367 node=hex(n),
368 date=web.repo[n].date())
368 date=web.repo[n].date())
369
369
370 def branches(**map):
370 def branches(**map):
371 parity = paritygen(web.stripecount)
371 parity = paritygen(web.stripecount)
372
372
373 b = web.repo.branchtags()
373 b = web.repo.branchtags()
374 l = [(-web.repo.changelog.rev(n), n, t) for t, n in b.items()]
374 l = [(-web.repo.changelog.rev(n), n, t) for t, n in b.items()]
375 for r,n,t in util.sort(l):
375 for r,n,t in util.sort(l):
376 yield {'parity': parity.next(),
376 yield {'parity': parity.next(),
377 'branch': t,
377 'branch': t,
378 'node': hex(n),
378 'node': hex(n),
379 'date': web.repo[n].date()}
379 'date': web.repo[n].date()}
380
380
381 def changelist(**map):
381 def changelist(**map):
382 parity = paritygen(web.stripecount, offset=start-end)
382 parity = paritygen(web.stripecount, offset=start-end)
383 l = [] # build a list in forward order for efficiency
383 l = [] # build a list in forward order for efficiency
384 for i in xrange(start, end):
384 for i in xrange(start, end):
385 ctx = web.repo[i]
385 ctx = web.repo[i]
386 n = ctx.node()
386 n = ctx.node()
387 hn = hex(n)
387 hn = hex(n)
388
388
389 l.insert(0, tmpl(
389 l.insert(0, tmpl(
390 'shortlogentry',
390 'shortlogentry',
391 parity=parity.next(),
391 parity=parity.next(),
392 author=ctx.user(),
392 author=ctx.user(),
393 desc=ctx.description(),
393 desc=ctx.description(),
394 date=ctx.date(),
394 date=ctx.date(),
395 rev=i,
395 rev=i,
396 node=hn,
396 node=hn,
397 tags=webutil.nodetagsdict(web.repo, n),
397 tags=webutil.nodetagsdict(web.repo, n),
398 inbranch=webutil.nodeinbranch(web.repo, ctx),
398 inbranch=webutil.nodeinbranch(web.repo, ctx),
399 branches=webutil.nodebranchdict(web.repo, ctx)))
399 branches=webutil.nodebranchdict(web.repo, ctx)))
400
400
401 yield l
401 yield l
402
402
403 cl = web.repo.changelog
403 cl = web.repo.changelog
404 count = len(cl)
404 count = len(cl)
405 start = max(0, count - web.maxchanges)
405 start = max(0, count - web.maxchanges)
406 end = min(count, start + web.maxchanges)
406 end = min(count, start + web.maxchanges)
407
407
408 return tmpl("summary",
408 return tmpl("summary",
409 desc=web.config("web", "description", "unknown"),
409 desc=web.config("web", "description", "unknown"),
410 owner=get_contact(web.config) or "unknown",
410 owner=get_contact(web.config) or "unknown",
411 lastchange=cl.read(cl.tip())[2],
411 lastchange=cl.read(cl.tip())[2],
412 tags=tagentries,
412 tags=tagentries,
413 branches=branches,
413 branches=branches,
414 shortlog=changelist,
414 shortlog=changelist,
415 node=hex(cl.tip()),
415 node=hex(cl.tip()),
416 archives=web.archivelist("tip"))
416 archives=web.archivelist("tip"))
417
417
418 def filediff(web, req, tmpl):
418 def filediff(web, req, tmpl):
419 fctx = webutil.filectx(web.repo, req)
419 fctx = webutil.filectx(web.repo, req)
420 n = fctx.node()
420 n = fctx.node()
421 path = fctx.path()
421 path = fctx.path()
422 parents = fctx.parents()
422 parents = fctx.parents()
423 p1 = parents and parents[0].node() or nullid
423 p1 = parents and parents[0].node() or nullid
424
424
425 diffs = web.diff(tmpl, p1, n, [path])
425 diffs = web.diff(tmpl, p1, n, [path])
426 return tmpl("filediff",
426 return tmpl("filediff",
427 file=path,
427 file=path,
428 node=hex(n),
428 node=hex(n),
429 rev=fctx.rev(),
429 rev=fctx.rev(),
430 date=fctx.date(),
430 date=fctx.date(),
431 desc=fctx.description(),
431 desc=fctx.description(),
432 author=fctx.user(),
432 author=fctx.user(),
433 rename=webutil.renamelink(fctx),
433 rename=webutil.renamelink(fctx),
434 branch=webutil.nodebranchnodefault(fctx),
434 branch=webutil.nodebranchnodefault(fctx),
435 parent=webutil.siblings(parents),
435 parent=webutil.siblings(parents),
436 child=webutil.siblings(fctx.children()),
436 child=webutil.siblings(fctx.children()),
437 diff=diffs)
437 diff=diffs)
438
438
439 diff = filediff
439 diff = filediff
440
440
441 def annotate(web, req, tmpl):
441 def annotate(web, req, tmpl):
442 fctx = webutil.filectx(web.repo, req)
442 fctx = webutil.filectx(web.repo, req)
443 f = fctx.path()
443 f = fctx.path()
444 n = fctx.filenode()
444 n = fctx.filenode()
445 fl = fctx.filelog()
445 fl = fctx.filelog()
446 parity = paritygen(web.stripecount)
446 parity = paritygen(web.stripecount)
447
447
448 def annotate(**map):
448 def annotate(**map):
449 last = None
449 last = None
450 if binary(fctx.data()):
450 if binary(fctx.data()):
451 mt = (mimetypes.guess_type(fctx.path())[0]
451 mt = (mimetypes.guess_type(fctx.path())[0]
452 or 'application/octet-stream')
452 or 'application/octet-stream')
453 lines = enumerate([((fctx.filectx(fctx.filerev()), 1),
453 lines = enumerate([((fctx.filectx(fctx.filerev()), 1),
454 '(binary:%s)' % mt)])
454 '(binary:%s)' % mt)])
455 else:
455 else:
456 lines = enumerate(fctx.annotate(follow=True, linenumber=True))
456 lines = enumerate(fctx.annotate(follow=True, linenumber=True))
457 for lineno, ((f, targetline), l) in lines:
457 for lineno, ((f, targetline), l) in lines:
458 fnode = f.filenode()
458 fnode = f.filenode()
459
459
460 if last != fnode:
460 if last != fnode:
461 last = fnode
461 last = fnode
462
462
463 yield {"parity": parity.next(),
463 yield {"parity": parity.next(),
464 "node": hex(f.node()),
464 "node": hex(f.node()),
465 "rev": f.rev(),
465 "rev": f.rev(),
466 "author": f.user(),
466 "author": f.user(),
467 "desc": f.description(),
467 "desc": f.description(),
468 "file": f.path(),
468 "file": f.path(),
469 "targetline": targetline,
469 "targetline": targetline,
470 "line": l,
470 "line": l,
471 "lineid": "l%d" % (lineno + 1),
471 "lineid": "l%d" % (lineno + 1),
472 "linenumber": "% 6d" % (lineno + 1)}
472 "linenumber": "% 6d" % (lineno + 1)}
473
473
474 return tmpl("fileannotate",
474 return tmpl("fileannotate",
475 file=f,
475 file=f,
476 annotate=annotate,
476 annotate=annotate,
477 path=webutil.up(f),
477 path=webutil.up(f),
478 rev=fctx.rev(),
478 rev=fctx.rev(),
479 node=hex(fctx.node()),
479 node=hex(fctx.node()),
480 author=fctx.user(),
480 author=fctx.user(),
481 date=fctx.date(),
481 date=fctx.date(),
482 desc=fctx.description(),
482 desc=fctx.description(),
483 rename=webutil.renamelink(fctx),
483 rename=webutil.renamelink(fctx),
484 branch=webutil.nodebranchnodefault(fctx),
484 branch=webutil.nodebranchnodefault(fctx),
485 parent=webutil.siblings(fctx.parents()),
485 parent=webutil.siblings(fctx.parents()),
486 child=webutil.siblings(fctx.children()),
486 child=webutil.siblings(fctx.children()),
487 permissions=fctx.manifest().flags(f))
487 permissions=fctx.manifest().flags(f))
488
488
489 def filelog(web, req, tmpl):
489 def filelog(web, req, tmpl):
490 fctx = webutil.filectx(web.repo, req)
490 fctx = webutil.filectx(web.repo, req)
491 f = fctx.path()
491 f = fctx.path()
492 fl = fctx.filelog()
492 fl = fctx.filelog()
493 count = len(fl)
493 count = len(fl)
494 pagelen = web.maxshortchanges
494 pagelen = web.maxshortchanges
495 pos = fctx.filerev()
495 pos = fctx.filerev()
496 start = max(0, pos - pagelen + 1)
496 start = max(0, pos - pagelen + 1)
497 end = min(count, start + pagelen)
497 end = min(count, start + pagelen)
498 pos = end - 1
498 pos = end - 1
499 parity = paritygen(web.stripecount, offset=start-end)
499 parity = paritygen(web.stripecount, offset=start-end)
500
500
501 def entries(limit=0, **map):
501 def entries(limit=0, **map):
502 l = []
502 l = []
503
503
504 for i in xrange(start, end):
504 for i in xrange(start, end):
505 ctx = fctx.filectx(i)
505 ctx = fctx.filectx(i)
506 n = fl.node(i)
506 n = fl.node(i)
507
507
508 l.insert(0, {"parity": parity.next(),
508 l.insert(0, {"parity": parity.next(),
509 "filerev": i,
509 "filerev": i,
510 "file": f,
510 "file": f,
511 "node": hex(ctx.node()),
511 "node": hex(ctx.node()),
512 "author": ctx.user(),
512 "author": ctx.user(),
513 "date": ctx.date(),
513 "date": ctx.date(),
514 "rename": webutil.renamelink(fctx),
514 "rename": webutil.renamelink(fctx),
515 "parent": webutil.siblings(fctx.parents()),
515 "parent": webutil.siblings(fctx.parents()),
516 "child": webutil.siblings(fctx.children()),
516 "child": webutil.siblings(fctx.children()),
517 "desc": ctx.description()})
517 "desc": ctx.description()})
518
518
519 if limit > 0:
519 if limit > 0:
520 l = l[:limit]
520 l = l[:limit]
521
521
522 for e in l:
522 for e in l:
523 yield e
523 yield e
524
524
525 nodefunc = lambda x: fctx.filectx(fileid=x)
525 nodefunc = lambda x: fctx.filectx(fileid=x)
526 nav = webutil.revnavgen(pos, pagelen, count, nodefunc)
526 nav = webutil.revnavgen(pos, pagelen, count, nodefunc)
527 return tmpl("filelog", file=f, node=hex(fctx.node()), nav=nav,
527 return tmpl("filelog", file=f, node=hex(fctx.node()), nav=nav,
528 entries=lambda **x: entries(limit=0, **x),
528 entries=lambda **x: entries(limit=0, **x),
529 latestentry=lambda **x: entries(limit=1, **x))
529 latestentry=lambda **x: entries(limit=1, **x))
530
530
531
531
532 def archive(web, req, tmpl):
532 def archive(web, req, tmpl):
533 type_ = req.form.get('type', [None])[0]
533 type_ = req.form.get('type', [None])[0]
534 allowed = web.configlist("web", "allow_archive")
534 allowed = web.configlist("web", "allow_archive")
535 key = req.form['node'][0]
535 key = req.form['node'][0]
536
536
537 if type_ not in web.archives:
537 if type_ not in web.archives:
538 msg = 'Unsupported archive type: %s' % type_
538 msg = 'Unsupported archive type: %s' % type_
539 raise ErrorResponse(HTTP_NOT_FOUND, msg)
539 raise ErrorResponse(HTTP_NOT_FOUND, msg)
540
540
541 if not ((type_ in allowed or
541 if not ((type_ in allowed or
542 web.configbool("web", "allow" + type_, False))):
542 web.configbool("web", "allow" + type_, False))):
543 msg = 'Archive type not allowed: %s' % type_
543 msg = 'Archive type not allowed: %s' % type_
544 raise ErrorResponse(HTTP_FORBIDDEN, msg)
544 raise ErrorResponse(HTTP_FORBIDDEN, msg)
545
545
546 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
546 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
547 cnode = web.repo.lookup(key)
547 cnode = web.repo.lookup(key)
548 arch_version = key
548 arch_version = key
549 if cnode == key or key == 'tip':
549 if cnode == key or key == 'tip':
550 arch_version = short(cnode)
550 arch_version = short(cnode)
551 name = "%s-%s" % (reponame, arch_version)
551 name = "%s-%s" % (reponame, arch_version)
552 mimetype, artype, extension, encoding = web.archive_specs[type_]
552 mimetype, artype, extension, encoding = web.archive_specs[type_]
553 headers = [
553 headers = [
554 ('Content-Type', mimetype),
554 ('Content-Type', mimetype),
555 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
555 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
556 ]
556 ]
557 if encoding:
557 if encoding:
558 headers.append(('Content-Encoding', encoding))
558 headers.append(('Content-Encoding', encoding))
559 req.header(headers)
559 req.header(headers)
560 req.respond(HTTP_OK)
560 req.respond(HTTP_OK)
561 archival.archive(web.repo, req, cnode, artype, prefix=name)
561 archival.archive(web.repo, req, cnode, artype, prefix=name)
562 return []
562 return []
563
563
564
564
565 def static(web, req, tmpl):
565 def static(web, req, tmpl):
566 fname = req.form['file'][0]
566 fname = req.form['file'][0]
567 # a repo owner may set web.static in .hg/hgrc to get any file
567 # a repo owner may set web.static in .hg/hgrc to get any file
568 # readable by the user running the CGI script
568 # readable by the user running the CGI script
569 static = web.config("web", "static",
569 static = web.config("web", "static", None, untrusted=False)
570 os.path.join(web.templatepath, "static"),
570 if not static:
571 untrusted=False)
571 tp = web.templatepath
572 if isinstance(tp, str):
573 tp = [tp]
574 for path in tp:
575 static = os.path.join(path, 'static')
576 if os.path.isdir(static):
577 break
572 return [staticfile(static, fname, req)]
578 return [staticfile(static, fname, req)]
573
579
574 def graph(web, req, tmpl):
580 def graph(web, req, tmpl):
575 rev = webutil.changectx(web.repo, req).rev()
581 rev = webutil.changectx(web.repo, req).rev()
576 bg_height = 39
582 bg_height = 39
577
583
578 max_rev = len(web.repo) - 1
584 max_rev = len(web.repo) - 1
579 revcount = min(max_rev, int(req.form.get('revcount', [25])[0]))
585 revcount = min(max_rev, int(req.form.get('revcount', [25])[0]))
580 revnode = web.repo.changelog.node(rev)
586 revnode = web.repo.changelog.node(rev)
581 revnode_hex = hex(revnode)
587 revnode_hex = hex(revnode)
582 uprev = min(max_rev, rev + revcount)
588 uprev = min(max_rev, rev + revcount)
583 downrev = max(0, rev - revcount)
589 downrev = max(0, rev - revcount)
584 lessrev = max(0, rev - revcount / 2)
590 lessrev = max(0, rev - revcount / 2)
585
591
586 maxchanges = web.maxshortchanges or web.maxchanges
592 maxchanges = web.maxshortchanges or web.maxchanges
587 count = len(web.repo)
593 count = len(web.repo)
588 changenav = webutil.revnavgen(rev, maxchanges, count, web.repo.changectx)
594 changenav = webutil.revnavgen(rev, maxchanges, count, web.repo.changectx)
589
595
590 tree = list(graphmod.graph(web.repo, rev, downrev))
596 tree = list(graphmod.graph(web.repo, rev, downrev))
591 canvasheight = (len(tree) + 1) * bg_height - 27;
597 canvasheight = (len(tree) + 1) * bg_height - 27;
592
598
593 data = []
599 data = []
594 for i, (ctx, vtx, edges) in enumerate(tree):
600 for i, (ctx, vtx, edges) in enumerate(tree):
595 node = short(ctx.node())
601 node = short(ctx.node())
596 age = templatefilters.age(ctx.date())
602 age = templatefilters.age(ctx.date())
597 desc = templatefilters.firstline(ctx.description())
603 desc = templatefilters.firstline(ctx.description())
598 desc = cgi.escape(desc)
604 desc = cgi.escape(desc)
599 user = cgi.escape(templatefilters.person(ctx.user()))
605 user = cgi.escape(templatefilters.person(ctx.user()))
600 branch = ctx.branch()
606 branch = ctx.branch()
601 branch = branch, web.repo.branchtags().get(branch) == ctx.node()
607 branch = branch, web.repo.branchtags().get(branch) == ctx.node()
602 data.append((node, vtx, edges, desc, user, age, branch, ctx.tags()))
608 data.append((node, vtx, edges, desc, user, age, branch, ctx.tags()))
603
609
604 return tmpl('graph', rev=rev, revcount=revcount, uprev=uprev,
610 return tmpl('graph', rev=rev, revcount=revcount, uprev=uprev,
605 lessrev=lessrev, revcountmore=revcount and 2 * revcount or 1,
611 lessrev=lessrev, revcountmore=revcount and 2 * revcount or 1,
606 revcountless=revcount / 2, downrev=downrev,
612 revcountless=revcount / 2, downrev=downrev,
607 canvasheight=canvasheight, bg_height=bg_height,
613 canvasheight=canvasheight, bg_height=bg_height,
608 jsdata=data, node=revnode_hex, changenav=changenav)
614 jsdata=data, node=revnode_hex, changenav=changenav)
@@ -1,171 +1,182 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
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
6 # of the GNU General Public License, incorporated herein by reference.
7
7
8 from i18n import _
8 from i18n import _
9 import re, sys, os
9 import re, sys, os
10 from mercurial import util
10 from mercurial import util
11
11
12 path = ['templates', '../templates']
13
12 def parsestring(s, quoted=True):
14 def parsestring(s, quoted=True):
13 '''parse a string using simple c-like syntax.
15 '''parse a string using simple c-like syntax.
14 string must be in quotes if quoted is True.'''
16 string must be in quotes if quoted is True.'''
15 if quoted:
17 if quoted:
16 if len(s) < 2 or s[0] != s[-1]:
18 if len(s) < 2 or s[0] != s[-1]:
17 raise SyntaxError(_('unmatched quotes'))
19 raise SyntaxError(_('unmatched quotes'))
18 return s[1:-1].decode('string_escape')
20 return s[1:-1].decode('string_escape')
19
21
20 return s.decode('string_escape')
22 return s.decode('string_escape')
21
23
22 class templater(object):
24 class templater(object):
23 '''template expansion engine.
25 '''template expansion engine.
24
26
25 template expansion works like this. a map file contains key=value
27 template expansion works like this. a map file contains key=value
26 pairs. if value is quoted, it is treated as string. otherwise, it
28 pairs. if value is quoted, it is treated as string. otherwise, it
27 is treated as name of template file.
29 is treated as name of template file.
28
30
29 templater is asked to expand a key in map. it looks up key, and
31 templater is asked to expand a key in map. it looks up key, and
30 looks for strings like this: {foo}. it expands {foo} by looking up
32 looks for strings like this: {foo}. it expands {foo} by looking up
31 foo in map, and substituting it. expansion is recursive: it stops
33 foo in map, and substituting it. expansion is recursive: it stops
32 when there is no more {foo} to replace.
34 when there is no more {foo} to replace.
33
35
34 expansion also allows formatting and filtering.
36 expansion also allows formatting and filtering.
35
37
36 format uses key to expand each item in list. syntax is
38 format uses key to expand each item in list. syntax is
37 {key%format}.
39 {key%format}.
38
40
39 filter uses function to transform value. syntax is
41 filter uses function to transform value. syntax is
40 {key|filter1|filter2|...}.'''
42 {key|filter1|filter2|...}.'''
41
43
42 template_re = re.compile(r"(?:(?:#(?=[\w\|%]+#))|(?:{(?=[\w\|%]+})))"
44 template_re = re.compile(r"(?:(?:#(?=[\w\|%]+#))|(?:{(?=[\w\|%]+})))"
43 r"(\w+)(?:(?:%(\w+))|((?:\|\w+)*))[#}]")
45 r"(\w+)(?:(?:%(\w+))|((?:\|\w+)*))[#}]")
44
46
45 def __init__(self, mapfile, filters={}, defaults={}, cache={}):
47 def __init__(self, mapfile, filters={}, defaults={}, cache={}):
46 '''set up template engine.
48 '''set up template engine.
47 mapfile is name of file to read map definitions from.
49 mapfile is name of file to read map definitions from.
48 filters is dict of functions. each transforms a value into another.
50 filters is dict of functions. each transforms a value into another.
49 defaults is dict of default map definitions.'''
51 defaults is dict of default map definitions.'''
50 self.mapfile = mapfile or 'template'
52 self.mapfile = mapfile or 'template'
51 self.cache = cache.copy()
53 self.cache = cache.copy()
52 self.map = {}
54 self.map = {}
53 self.base = (mapfile and os.path.dirname(mapfile)) or ''
55 self.base = (mapfile and os.path.dirname(mapfile)) or ''
54 self.filters = filters
56 self.filters = filters
55 self.defaults = defaults
57 self.defaults = defaults
56
58
57 if not mapfile:
59 if not mapfile:
58 return
60 return
59 if not os.path.exists(mapfile):
61 if not os.path.exists(mapfile):
60 raise util.Abort(_('style not found: %s') % mapfile)
62 raise util.Abort(_('style not found: %s') % mapfile)
61
63
62 i = 0
64 i = 0
63 for l in file(mapfile):
65 for l in file(mapfile):
64 l = l.strip()
66 l = l.strip()
65 i += 1
67 i += 1
66 if not l or l[0] in '#;': continue
68 if not l or l[0] in '#;': continue
67 m = re.match(r'([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$', l)
69 m = re.match(r'([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$', l)
68 if m:
70 if m:
69 key, val = m.groups()
71 key, val = m.groups()
70 if val[0] in "'\"":
72 if val[0] in "'\"":
71 try:
73 try:
72 self.cache[key] = parsestring(val)
74 self.cache[key] = parsestring(val)
73 except SyntaxError, inst:
75 except SyntaxError, inst:
74 raise SyntaxError('%s:%s: %s' %
76 raise SyntaxError('%s:%s: %s' %
75 (mapfile, i, inst.args[0]))
77 (mapfile, i, inst.args[0]))
76 else:
78 else:
77 self.map[key] = os.path.join(self.base, val)
79 self.map[key] = os.path.join(self.base, val)
78 else:
80 else:
79 raise SyntaxError(_("%s:%s: parse error") % (mapfile, i))
81 raise SyntaxError(_("%s:%s: parse error") % (mapfile, i))
80
82
81 def __contains__(self, key):
83 def __contains__(self, key):
82 return key in self.cache or key in self.map
84 return key in self.cache or key in self.map
83
85
84 def _template(self, t):
86 def _template(self, t):
85 '''Get the template for the given template name. Use a local cache.'''
87 '''Get the template for the given template name. Use a local cache.'''
86 if not t in self.cache:
88 if not t in self.cache:
87 try:
89 try:
88 self.cache[t] = file(self.map[t]).read()
90 self.cache[t] = file(self.map[t]).read()
89 except IOError, inst:
91 except IOError, inst:
90 raise IOError(inst.args[0], _('template file %s: %s') %
92 raise IOError(inst.args[0], _('template file %s: %s') %
91 (self.map[t], inst.args[1]))
93 (self.map[t], inst.args[1]))
92 return self.cache[t]
94 return self.cache[t]
93
95
94 def _process(self, tmpl, map):
96 def _process(self, tmpl, map):
95 '''Render a template. Returns a generator.'''
97 '''Render a template. Returns a generator.'''
96 while tmpl:
98 while tmpl:
97 m = self.template_re.search(tmpl)
99 m = self.template_re.search(tmpl)
98 if not m:
100 if not m:
99 yield tmpl
101 yield tmpl
100 break
102 break
101
103
102 start, end = m.span(0)
104 start, end = m.span(0)
103 key, format, fl = m.groups()
105 key, format, fl = m.groups()
104
106
105 if start:
107 if start:
106 yield tmpl[:start]
108 yield tmpl[:start]
107 tmpl = tmpl[end:]
109 tmpl = tmpl[end:]
108
110
109 if key in map:
111 if key in map:
110 v = map[key]
112 v = map[key]
111 else:
113 else:
112 v = self.defaults.get(key, "")
114 v = self.defaults.get(key, "")
113 if callable(v):
115 if callable(v):
114 v = v(**map)
116 v = v(**map)
115 if format:
117 if format:
116 if not hasattr(v, '__iter__'):
118 if not hasattr(v, '__iter__'):
117 raise SyntaxError(_("Error expanding '%s%%%s'")
119 raise SyntaxError(_("Error expanding '%s%%%s'")
118 % (key, format))
120 % (key, format))
119 lm = map.copy()
121 lm = map.copy()
120 for i in v:
122 for i in v:
121 lm.update(i)
123 lm.update(i)
122 t = self._template(format)
124 t = self._template(format)
123 yield self._process(t, lm)
125 yield self._process(t, lm)
124 else:
126 else:
125 if fl:
127 if fl:
126 for f in fl.split("|")[1:]:
128 for f in fl.split("|")[1:]:
127 v = self.filters[f](v)
129 v = self.filters[f](v)
128 yield v
130 yield v
129
131
130 def __call__(self, t, **map):
132 def __call__(self, t, **map):
131 '''Perform expansion. t is name of map element to expand. map contains
133 '''Perform expansion. t is name of map element to expand. map contains
132 added elements for use during expansion. Is a generator.'''
134 added elements for use during expansion. Is a generator.'''
133 tmpl = self._template(t)
135 tmpl = self._template(t)
134 iters = [self._process(tmpl, map)]
136 iters = [self._process(tmpl, map)]
135 while iters:
137 while iters:
136 try:
138 try:
137 item = iters[0].next()
139 item = iters[0].next()
138 except StopIteration:
140 except StopIteration:
139 iters.pop(0)
141 iters.pop(0)
140 continue
142 continue
141 if isinstance(item, str):
143 if isinstance(item, str):
142 yield item
144 yield item
143 elif item is None:
145 elif item is None:
144 yield ''
146 yield ''
145 elif hasattr(item, '__iter__'):
147 elif hasattr(item, '__iter__'):
146 iters.insert(0, iter(item))
148 iters.insert(0, iter(item))
147 else:
149 else:
148 yield str(item)
150 yield str(item)
149
151
150 def templatepath(name=None):
152 def templatepath(name=None):
151 '''return location of template file or directory (if no name).
153 '''return location of template file or directory (if no name).
152 returns None if not found.'''
154 returns None if not found.'''
155 normpaths = []
153
156
154 # executable version (py2exe) doesn't support __file__
157 # executable version (py2exe) doesn't support __file__
155 if hasattr(sys, 'frozen'):
158 if hasattr(sys, 'frozen'):
156 module = sys.executable
159 module = sys.executable
157 else:
160 else:
158 module = __file__
161 module = __file__
159 for f in 'templates', '../templates':
162 for f in path:
160 fl = f.split('/')
163 if f.startswith('/'):
161 if name: fl.append(name)
164 p = f
162 p = os.path.join(os.path.dirname(module), *fl)
165 else:
163 if (name and os.path.exists(p)) or os.path.isdir(p):
166 fl = f.split('/')
167 p = os.path.join(os.path.dirname(module), *fl)
168 if name:
169 p = os.path.join(p, name)
170 if name and os.path.exists(p):
164 return os.path.normpath(p)
171 return os.path.normpath(p)
172 elif os.path.isdir(p):
173 normpaths.append(os.path.normpath(p))
174
175 return normpaths
165
176
166 def stringify(thing):
177 def stringify(thing):
167 '''turn nested template iterator into string.'''
178 '''turn nested template iterator into string.'''
168 if hasattr(thing, '__iter__'):
179 if hasattr(thing, '__iter__'):
169 return "".join([stringify(t) for t in thing if t is not None])
180 return "".join([stringify(t) for t in thing if t is not None])
170 return str(thing)
181 return str(thing)
171
182
General Comments 0
You need to be logged in to leave comments. Login now