##// END OF EJS Templates
hgwebdir: split out makeindex function, facilitate test failure diagnosis
Dirkjan Ochtman -
r5601:8279cb84 default
parent child Browse files
Show More
@@ -1,262 +1,261 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, mimetools, cStringIO
10 10 from mercurial.i18n import gettext as _
11 11 from mercurial import ui, hg, util, templater
12 12 from common import ErrorResponse, get_mtime, staticfile, style_map, paritygen
13 13 from hgweb_mod import hgweb
14 14 from request import wsgirequest
15 15
16 16 # This is a stopgap
17 17 class hgwebdir(object):
18 18 def __init__(self, config, parentui=None):
19 19 def cleannames(items):
20 20 return [(util.pconvert(name).strip('/'), path)
21 21 for name, path in items]
22 22
23 self.parentui = parentui
23 self.parentui = parentui or ui.ui(report_untrusted=False,
24 interactive = False)
24 25 self.motd = None
25 26 self.style = None
26 27 self.stripecount = None
27 28 self.repos_sorted = ('name', False)
28 29 if isinstance(config, (list, tuple)):
29 30 self.repos = cleannames(config)
30 31 self.repos_sorted = ('', False)
31 32 elif isinstance(config, dict):
32 33 self.repos = cleannames(config.items())
33 34 self.repos.sort()
34 35 else:
35 36 if isinstance(config, util.configparser):
36 37 cp = config
37 38 else:
38 39 cp = util.configparser()
39 40 cp.read(config)
40 41 self.repos = []
41 42 if cp.has_section('web'):
42 43 if cp.has_option('web', 'motd'):
43 44 self.motd = cp.get('web', 'motd')
44 45 if cp.has_option('web', 'style'):
45 46 self.style = cp.get('web', 'style')
46 47 if cp.has_option('web', 'stripes'):
47 48 self.stripecount = int(cp.get('web', 'stripes'))
48 49 if cp.has_section('paths'):
49 50 self.repos.extend(cleannames(cp.items('paths')))
50 51 if cp.has_section('collections'):
51 52 for prefix, root in cp.items('collections'):
52 53 for path in util.walkrepos(root):
53 54 repo = os.path.normpath(path)
54 55 name = repo
55 56 if name.startswith(prefix):
56 57 name = name[len(prefix):]
57 58 self.repos.append((name.lstrip(os.sep), repo))
58 59 self.repos.sort()
59 60
60 61 def run(self):
61 62 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
62 63 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
63 64 import mercurial.hgweb.wsgicgi as wsgicgi
64 65 wsgicgi.launch(self)
65 66
66 67 def __call__(self, env, respond):
67 68 req = wsgirequest(env, respond)
68 69 self.run_wsgi(req)
69 70 return req
70 71
71 72 def run_wsgi(self, req):
72 73 def header(**map):
73 74 header_file = cStringIO.StringIO(
74 75 ''.join(tmpl("header", encoding=util._encoding, **map)))
75 76 msg = mimetools.Message(header_file, 0)
76 77 req.header(msg.items())
77 78 yield header_file.read()
78 79
79 80 def footer(**map):
80 81 yield tmpl("footer", **map)
81 82
82 83 def motd(**map):
83 84 if self.motd is not None:
84 85 yield self.motd
85 86 else:
86 87 yield config('web', 'motd', '')
87 88
88 parentui = self.parentui or ui.ui(report_untrusted=False,
89 interactive=False)
90
91 89 def config(section, name, default=None, untrusted=True):
92 return parentui.config(section, name, default, untrusted)
90 return self.parentui.config(section, name, default, untrusted)
93 91
94 92 url = req.env.get('SCRIPT_NAME', '')
95 93 if not url.endswith('/'):
96 94 url += '/'
97 95
98 96 staticurl = config('web', 'staticurl') or url + 'static/'
99 97 if not staticurl.endswith('/'):
100 98 staticurl += '/'
101 99
102 100 style = self.style
103 101 if style is None:
104 102 style = config('web', 'style', '')
105 103 if req.form.has_key('style'):
106 104 style = req.form['style'][0]
107 105 if self.stripecount is None:
108 106 self.stripecount = int(config('web', 'stripes', 1))
109 107 mapfile = style_map(templater.templatepath(), style)
110 108 tmpl = templater.templater(mapfile, templater.common_filters,
111 109 defaults={"header": header,
112 110 "footer": footer,
113 111 "motd": motd,
114 112 "url": url,
115 113 "staticurl": staticurl})
116 114
115 try:
116 try:
117 virtual = req.env.get("PATH_INFO", "").strip('/')
118 if virtual.startswith('static/'):
119 static = os.path.join(templater.templatepath(), 'static')
120 fname = virtual[7:]
121 req.write(staticfile(static, fname, req))
122 elif virtual:
123 repos = dict(self.repos)
124 while virtual:
125 real = repos.get(virtual)
126 if real:
127 req.env['REPO_NAME'] = virtual
128 try:
129 repo = hg.repository(self.parentui, real)
130 hgweb(repo).run_wsgi(req)
131 return
132 except IOError, inst:
133 raise ErrorResponse(500, inst.strerror)
134 except hg.RepoError, inst:
135 raise ErrorResponse(500, str(inst))
136
137 # browse subdirectories
138 subdir = virtual + '/'
139 if [r for r in repos if r.startswith(subdir)]:
140 self.makeindex(req, tmpl, subdir)
141 return
142
143 up = virtual.rfind('/')
144 if up < 0:
145 break
146 virtual = virtual[:up]
147
148 req.respond(404, tmpl("notfound", repo=virtual))
149 else:
150 if req.form.has_key('static'):
151 static = os.path.join(templater.templatepath(), "static")
152 fname = req.form['static'][0]
153 req.write(staticfile(static, fname, req))
154 else:
155 self.makeindex(req, tmpl)
156 except ErrorResponse, err:
157 req.respond(err.code, tmpl('error', error=err.message or ''))
158 finally:
159 tmpl = None
160
161 def makeindex(self, req, tmpl, subdir=""):
162
117 163 def archivelist(ui, nodeid, url):
118 164 allowed = ui.configlist("web", "allow_archive", untrusted=True)
119 165 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
120 166 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
121 167 untrusted=True):
122 168 yield {"type" : i[0], "extension": i[1],
123 169 "node": nodeid, "url": url}
124 170
125 171 def entries(sortcolumn="", descending=False, subdir="", **map):
126 172 def sessionvars(**map):
127 173 fields = []
128 174 if req.form.has_key('style'):
129 175 style = req.form['style'][0]
130 176 if style != get('web', 'style', ''):
131 177 fields.append(('style', style))
132 178
133 179 separator = url[-1] == '?' and ';' or '?'
134 180 for name, value in fields:
135 181 yield dict(name=name, value=value, separator=separator)
136 182 separator = ';'
137 183
138 184 rows = []
139 185 parity = paritygen(self.stripecount)
140 186 for name, path in self.repos:
141 187 if not name.startswith(subdir):
142 188 continue
143 189 name = name[len(subdir):]
144 190
145 u = ui.ui(parentui=parentui)
191 u = ui.ui(parentui=self.parentui)
146 192 try:
147 193 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
148 194 except Exception, e:
149 195 u.warn(_('error reading %s/.hg/hgrc: %s\n' % (path, e)))
150 196 continue
151 197 def get(section, name, default=None):
152 198 return u.config(section, name, default, untrusted=True)
153 199
154 200 if u.configbool("web", "hidden", untrusted=True):
155 201 continue
156 202
157 203 parts = [req.env['PATH_INFO'], name]
158 204 if req.env['SCRIPT_NAME']:
159 205 parts.insert(0, req.env['SCRIPT_NAME'])
160 206 url = ('/'.join(parts).replace("//", "/")) + '/'
161 207
162 208 # update time with local timezone
163 209 try:
164 210 d = (get_mtime(path), util.makedate()[1])
165 211 except OSError:
166 212 continue
167 213
168 214 contact = (get("ui", "username") or # preferred
169 215 get("web", "contact") or # deprecated
170 216 get("web", "author", "")) # also
171 217 description = get("web", "description", "")
172 218 name = get("web", "name", name)
173 219 row = dict(contact=contact or "unknown",
174 220 contact_sort=contact.upper() or "unknown",
175 221 name=name,
176 222 name_sort=name,
177 223 url=url,
178 224 description=description or "unknown",
179 225 description_sort=description.upper() or "unknown",
180 226 lastchange=d,
181 227 lastchange_sort=d[1]-d[0],
182 228 sessionvars=sessionvars,
183 229 archives=archivelist(u, "tip", url))
184 230 if (not sortcolumn
185 231 or (sortcolumn, descending) == self.repos_sorted):
186 232 # fast path for unsorted output
187 233 row['parity'] = parity.next()
188 234 yield row
189 235 else:
190 236 rows.append((row["%s_sort" % sortcolumn], row))
191 237 if rows:
192 238 rows.sort()
193 239 if descending:
194 240 rows.reverse()
195 241 for key, row in rows:
196 242 row['parity'] = parity.next()
197 243 yield row
198 244
199 def makeindex(req, subdir=""):
200 sortable = ["name", "description", "contact", "lastchange"]
201 sortcolumn, descending = self.repos_sorted
202 if req.form.has_key('sort'):
203 sortcolumn = req.form['sort'][0]
204 descending = sortcolumn.startswith('-')
205 if descending:
206 sortcolumn = sortcolumn[1:]
207 if sortcolumn not in sortable:
208 sortcolumn = ""
209
210 sort = [("sort_%s" % column,
211 "%s%s" % ((not descending and column == sortcolumn)
212 and "-" or "", column))
213 for column in sortable]
214 req.write(tmpl("index", entries=entries, subdir=subdir,
215 sortcolumn=sortcolumn, descending=descending,
216 **dict(sort)))
245 sortable = ["name", "description", "contact", "lastchange"]
246 sortcolumn, descending = self.repos_sorted
247 if req.form.has_key('sort'):
248 sortcolumn = req.form['sort'][0]
249 descending = sortcolumn.startswith('-')
250 if descending:
251 sortcolumn = sortcolumn[1:]
252 if sortcolumn not in sortable:
253 sortcolumn = ""
217 254
218 try:
219 try:
220 virtual = req.env.get("PATH_INFO", "").strip('/')
221 if virtual.startswith('static/'):
222 static = os.path.join(templater.templatepath(), 'static')
223 fname = virtual[7:]
224 req.write(staticfile(static, fname, req))
225 elif virtual:
226 repos = dict(self.repos)
227 while virtual:
228 real = repos.get(virtual)
229 if real:
230 req.env['REPO_NAME'] = virtual
231 try:
232 repo = hg.repository(parentui, real)
233 hgweb(repo).run_wsgi(req)
234 return
235 except IOError, inst:
236 raise ErrorResponse(500, inst.strerror)
237 except hg.RepoError, inst:
238 raise ErrorResponse(500, str(inst))
239
240 # browse subdirectories
241 subdir = virtual + '/'
242 if [r for r in repos if r.startswith(subdir)]:
243 makeindex(req, subdir)
244 return
245
246 up = virtual.rfind('/')
247 if up < 0:
248 break
249 virtual = virtual[:up]
250
251 req.respond(404, tmpl("notfound", repo=virtual))
252 else:
253 if req.form.has_key('static'):
254 static = os.path.join(templater.templatepath(), "static")
255 fname = req.form['static'][0]
256 req.write(staticfile(static, fname, req))
257 else:
258 makeindex(req)
259 except ErrorResponse, err:
260 req.respond(err.code, tmpl('error', error=err.message or ''))
261 finally:
262 tmpl = None
255 sort = [("sort_%s" % column,
256 "%s%s" % ((not descending and column == sortcolumn)
257 and "-" or "", column))
258 for column in sortable]
259 req.write(tmpl("index", entries=entries, subdir=subdir,
260 sortcolumn=sortcolumn, descending=descending,
261 **dict(sort)))
@@ -1,77 +1,84 b''
1 1 #!/bin/sh
2 2 # Tests some basic hgwebdir functionality. Tests setting up paths and
3 3 # collection, different forms of 404s and the subdirectory support.
4 4
5 5 mkdir webdir
6 6 cd webdir
7 7
8 8 hg init a
9 9 echo a > a/a
10 10 hg --cwd a ci -Ama -d'1 0'
11 11
12 12 hg init b
13 13 echo b > b/b
14 14 hg --cwd b ci -Amb -d'2 0'
15 15
16 16 hg init c
17 17 echo c > c/c
18 18 hg --cwd c ci -Amc -d'3 0'
19 19 root=`pwd`
20 20
21 21 cd ..
22 22
23 23 cat > paths.conf <<EOF
24 24 [paths]
25 25 a=$root/a
26 26 b=$root/b
27 27 EOF
28 28
29 29 hg serve -p $HGPORT -d --pid-file=hg.pid --webdir-conf paths.conf \
30 -A access-paths.log -E error-paths.log
30 -A access-paths.log -E error-paths-1.log
31 31 cat hg.pid >> $DAEMON_PIDS
32 32
33 33 echo % should give a 404 - file does not exist
34 34 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/a/file/tip/bork?style=raw'
35 35
36 36 echo % should succeed
37 37 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/?style=raw'
38 38 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/a/file/tip/a?style=raw'
39 39 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/b/file/tip/b?style=raw'
40 40
41 41 echo % should give a 404 - repo is not published
42 42 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/c/file/tip/c?style=raw'
43 43
44 44 cat > paths.conf <<EOF
45 45 [paths]
46 46 t/a/=$root/a
47 47 b=$root/b
48 48 EOF
49 49
50 50 hg serve -p $HGPORT1 -d --pid-file=hg.pid --webdir-conf paths.conf \
51 -A access-paths.log -E error-paths.log
51 -A access-paths.log -E error-paths-2.log
52 52 cat hg.pid >> $DAEMON_PIDS
53 53
54 54 echo % should succeed, slashy names
55 55 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/?style=raw'
56 56 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t?style=raw'
57 57 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t/?style=raw'
58 58 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t/a?style=atom' \
59 59 | sed "s/http:\/\/[^/]*\//http:\/\/127.0.0.1\//"
60 60 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t/a/?style=atom' \
61 61 | sed "s/http:\/\/[^/]*\//http:\/\/127.0.0.1\//"
62 62 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t/a/file/tip/a?style=raw'
63 63
64 64 cat > collections.conf <<EOF
65 65 [collections]
66 66 $root=$root
67 67 EOF
68 68
69 69 hg serve -p $HGPORT2 -d --pid-file=hg.pid --webdir-conf collections.conf \
70 70 -A access-collections.log -E error-collections.log
71 71 cat hg.pid >> $DAEMON_PIDS
72 72
73 73 echo % should succeed
74 74 "$TESTDIR/get-with-headers.py" localhost:$HGPORT2 '/?style=raw'
75 75 "$TESTDIR/get-with-headers.py" localhost:$HGPORT2 '/a/file/tip/a?style=raw'
76 76 "$TESTDIR/get-with-headers.py" localhost:$HGPORT2 '/b/file/tip/b?style=raw'
77 77 "$TESTDIR/get-with-headers.py" localhost:$HGPORT2 '/c/file/tip/c?style=raw'
78
79 echo % paths errors 1
80 cat error-paths-1.log
81 echo % paths errors 2
82 cat error-paths-2.log
83 echo % collections errors
84 cat error-collections.log
@@ -1,121 +1,124 b''
1 1 adding a
2 2 adding b
3 3 adding c
4 4 % should give a 404 - file does not exist
5 5 404 Not Found
6 6
7 7
8 8 error: Path not found: bork/
9 9 % should succeed
10 10 200 Script output follows
11 11
12 12
13 13 /a/
14 14 /b/
15 15
16 16 200 Script output follows
17 17
18 18 a
19 19 200 Script output follows
20 20
21 21 b
22 22 % should give a 404 - repo is not published
23 23 404 Not Found
24 24
25 25
26 26 error: repository c not found
27 27 % should succeed, slashy names
28 28 200 Script output follows
29 29
30 30
31 31 /b/
32 32 /t/a/
33 33
34 34 200 Script output follows
35 35
36 36
37 37 /t/a/
38 38
39 39 200 Script output follows
40 40
41 41
42 42 /t/a/
43 43
44 44 200 Script output follows
45 45
46 46 <?xml version="1.0" encoding="ascii"?>
47 47 <feed xmlns="http://127.0.0.1/2005/Atom">
48 48 <!-- Changelog -->
49 49 <id>http://127.0.0.1/t/a/</id>
50 50 <link rel="self" href="http://127.0.0.1/t/a/atom-log"/>
51 51 <link rel="alternate" href="http://127.0.0.1/t/a/"/>
52 52 <title>t/a Changelog</title>
53 53 <updated>1970-01-01T00:00:01+00:00</updated>
54 54
55 55 <entry>
56 56 <title>a</title>
57 57 <id>http://127.0.0.1/mercurial/#changeset-8580ff50825a50c8f716709acdf8de0deddcd6ab</id>
58 58 <link href="http://127.0.0.1/t/a/rev/8580ff50825a50c8f716709acdf8de0deddcd6ab"/>
59 59 <author>
60 60 <name>test</name>
61 61 <email>&#116;&#101;&#115;&#116;</email>
62 62 </author>
63 63 <updated>1970-01-01T00:00:01+00:00</updated>
64 64 <published>1970-01-01T00:00:01+00:00</published>
65 65 <content type="xhtml">
66 66 <div xmlns="http://127.0.0.1/1999/xhtml">
67 67 <pre xml:space="preserve">a</pre>
68 68 </div>
69 69 </content>
70 70 </entry>
71 71
72 72 </feed>
73 73 200 Script output follows
74 74
75 75 <?xml version="1.0" encoding="ascii"?>
76 76 <feed xmlns="http://127.0.0.1/2005/Atom">
77 77 <!-- Changelog -->
78 78 <id>http://127.0.0.1/t/a/</id>
79 79 <link rel="self" href="http://127.0.0.1/t/a/atom-log"/>
80 80 <link rel="alternate" href="http://127.0.0.1/t/a/"/>
81 81 <title>t/a Changelog</title>
82 82 <updated>1970-01-01T00:00:01+00:00</updated>
83 83
84 84 <entry>
85 85 <title>a</title>
86 86 <id>http://127.0.0.1/mercurial/#changeset-8580ff50825a50c8f716709acdf8de0deddcd6ab</id>
87 87 <link href="http://127.0.0.1/t/a/rev/8580ff50825a50c8f716709acdf8de0deddcd6ab"/>
88 88 <author>
89 89 <name>test</name>
90 90 <email>&#116;&#101;&#115;&#116;</email>
91 91 </author>
92 92 <updated>1970-01-01T00:00:01+00:00</updated>
93 93 <published>1970-01-01T00:00:01+00:00</published>
94 94 <content type="xhtml">
95 95 <div xmlns="http://127.0.0.1/1999/xhtml">
96 96 <pre xml:space="preserve">a</pre>
97 97 </div>
98 98 </content>
99 99 </entry>
100 100
101 101 </feed>
102 102 200 Script output follows
103 103
104 104 a
105 105 % should succeed
106 106 200 Script output follows
107 107
108 108
109 109 /a/
110 110 /b/
111 111 /c/
112 112
113 113 200 Script output follows
114 114
115 115 a
116 116 200 Script output follows
117 117
118 118 b
119 119 200 Script output follows
120 120
121 121 c
122 % paths errors 1
123 % paths errors 2
124 % collections errors
General Comments 0
You need to be logged in to leave comments. Login now