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