##// END OF EJS Templates
hgweb: let hgwebdir browse subdirectories
Brendan Cully -
r4841:9b0ebb5e default
parent child Browse files
Show More
@@ -1,240 +1,254 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 from mercurial import demandimport; demandimport.enable()
9 from mercurial import demandimport; demandimport.enable()
10 import os, mimetools, cStringIO
10 import os, mimetools, cStringIO
11 from mercurial.i18n import gettext as _
11 from mercurial.i18n import gettext as _
12 from mercurial import ui, hg, util, templater
12 from mercurial import ui, hg, util, templater
13 from common import get_mtime, staticfile, style_map, paritygen
13 from common import get_mtime, staticfile, style_map, paritygen
14 from hgweb_mod import hgweb
14 from hgweb_mod import hgweb
15
15
16 # This is a stopgap
16 # This is a stopgap
17 class hgwebdir(object):
17 class hgwebdir(object):
18 def __init__(self, config, parentui=None):
18 def __init__(self, config, parentui=None):
19 def cleannames(items):
19 def cleannames(items):
20 return [(name.strip(os.sep), path) for name, path in items]
20 return [(name.strip(os.sep), path) for name, path in items]
21
21
22 self.parentui = parentui
22 self.parentui = parentui
23 self.motd = None
23 self.motd = None
24 self.style = None
24 self.style = None
25 self.stripecount = None
25 self.stripecount = None
26 self.repos_sorted = ('name', False)
26 self.repos_sorted = ('name', False)
27 if isinstance(config, (list, tuple)):
27 if isinstance(config, (list, tuple)):
28 self.repos = cleannames(config)
28 self.repos = cleannames(config)
29 self.repos_sorted = ('', False)
29 self.repos_sorted = ('', False)
30 elif isinstance(config, dict):
30 elif isinstance(config, dict):
31 self.repos = cleannames(config.items())
31 self.repos = cleannames(config.items())
32 self.repos.sort()
32 self.repos.sort()
33 else:
33 else:
34 if isinstance(config, util.configparser):
34 if isinstance(config, util.configparser):
35 cp = config
35 cp = config
36 else:
36 else:
37 cp = util.configparser()
37 cp = util.configparser()
38 cp.read(config)
38 cp.read(config)
39 self.repos = []
39 self.repos = []
40 if cp.has_section('web'):
40 if cp.has_section('web'):
41 if cp.has_option('web', 'motd'):
41 if cp.has_option('web', 'motd'):
42 self.motd = cp.get('web', 'motd')
42 self.motd = cp.get('web', 'motd')
43 if cp.has_option('web', 'style'):
43 if cp.has_option('web', 'style'):
44 self.style = cp.get('web', 'style')
44 self.style = cp.get('web', 'style')
45 if cp.has_option('web', 'stripes'):
45 if cp.has_option('web', 'stripes'):
46 self.stripecount = int(cp.get('web', 'stripes'))
46 self.stripecount = int(cp.get('web', 'stripes'))
47 if cp.has_section('paths'):
47 if cp.has_section('paths'):
48 self.repos.extend(cleannames(cp.items('paths')))
48 self.repos.extend(cleannames(cp.items('paths')))
49 if cp.has_section('collections'):
49 if cp.has_section('collections'):
50 for prefix, root in cp.items('collections'):
50 for prefix, root in cp.items('collections'):
51 for path in util.walkrepos(root):
51 for path in util.walkrepos(root):
52 repo = os.path.normpath(path)
52 repo = os.path.normpath(path)
53 name = repo
53 name = repo
54 if name.startswith(prefix):
54 if name.startswith(prefix):
55 name = name[len(prefix):]
55 name = name[len(prefix):]
56 self.repos.append((name.lstrip(os.sep), repo))
56 self.repos.append((name.lstrip(os.sep), repo))
57 self.repos.sort()
57 self.repos.sort()
58
58
59 def run(self):
59 def run(self):
60 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
60 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
61 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
61 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
62 import mercurial.hgweb.wsgicgi as wsgicgi
62 import mercurial.hgweb.wsgicgi as wsgicgi
63 from request import wsgiapplication
63 from request import wsgiapplication
64 def make_web_app():
64 def make_web_app():
65 return self
65 return self
66 wsgicgi.launch(wsgiapplication(make_web_app))
66 wsgicgi.launch(wsgiapplication(make_web_app))
67
67
68 def run_wsgi(self, req):
68 def run_wsgi(self, req):
69 def header(**map):
69 def header(**map):
70 header_file = cStringIO.StringIO(
70 header_file = cStringIO.StringIO(
71 ''.join(tmpl("header", encoding=util._encoding, **map)))
71 ''.join(tmpl("header", encoding=util._encoding, **map)))
72 msg = mimetools.Message(header_file, 0)
72 msg = mimetools.Message(header_file, 0)
73 req.header(msg.items())
73 req.header(msg.items())
74 yield header_file.read()
74 yield header_file.read()
75
75
76 def footer(**map):
76 def footer(**map):
77 yield tmpl("footer", **map)
77 yield tmpl("footer", **map)
78
78
79 def motd(**map):
79 def motd(**map):
80 if self.motd is not None:
80 if self.motd is not None:
81 yield self.motd
81 yield self.motd
82 else:
82 else:
83 yield config('web', 'motd', '')
83 yield config('web', 'motd', '')
84
84
85 parentui = self.parentui or ui.ui(report_untrusted=False)
85 parentui = self.parentui or ui.ui(report_untrusted=False)
86
86
87 def config(section, name, default=None, untrusted=True):
87 def config(section, name, default=None, untrusted=True):
88 return parentui.config(section, name, default, untrusted)
88 return parentui.config(section, name, default, untrusted)
89
89
90 url = req.env['REQUEST_URI'].split('?')[0]
90 url = req.env['REQUEST_URI'].split('?')[0]
91 if not url.endswith('/'):
91 if not url.endswith('/'):
92 url += '/'
92 url += '/'
93 pathinfo = req.env.get('PATH_INFO', '').strip('/') + '/'
94 base = url[:len(url) - len(pathinfo)]
95 if not base.endswith('/'):
96 base += '/'
93
97
94 staticurl = config('web', 'staticurl') or url + 'static/'
98 staticurl = config('web', 'staticurl') or base + 'static/'
95 if not staticurl.endswith('/'):
99 if not staticurl.endswith('/'):
96 staticurl += '/'
100 staticurl += '/'
97
101
98 style = self.style
102 style = self.style
99 if style is None:
103 if style is None:
100 style = config('web', 'style', '')
104 style = config('web', 'style', '')
101 if req.form.has_key('style'):
105 if req.form.has_key('style'):
102 style = req.form['style'][0]
106 style = req.form['style'][0]
103 if self.stripecount is None:
107 if self.stripecount is None:
104 self.stripecount = int(config('web', 'stripes', 1))
108 self.stripecount = int(config('web', 'stripes', 1))
105 mapfile = style_map(templater.templatepath(), style)
109 mapfile = style_map(templater.templatepath(), style)
106 tmpl = templater.templater(mapfile, templater.common_filters,
110 tmpl = templater.templater(mapfile, templater.common_filters,
107 defaults={"header": header,
111 defaults={"header": header,
108 "footer": footer,
112 "footer": footer,
109 "motd": motd,
113 "motd": motd,
110 "url": url,
114 "url": url,
111 "staticurl": staticurl})
115 "staticurl": staticurl})
112
116
113 def archivelist(ui, nodeid, url):
117 def archivelist(ui, nodeid, url):
114 allowed = ui.configlist("web", "allow_archive", untrusted=True)
118 allowed = ui.configlist("web", "allow_archive", untrusted=True)
115 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
119 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
116 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
120 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
117 untrusted=True):
121 untrusted=True):
118 yield {"type" : i[0], "extension": i[1],
122 yield {"type" : i[0], "extension": i[1],
119 "node": nodeid, "url": url}
123 "node": nodeid, "url": url}
120
124
121 def entries(sortcolumn="", descending=False, **map):
125 def entries(sortcolumn="", descending=False, subdir="", **map):
122 def sessionvars(**map):
126 def sessionvars(**map):
123 fields = []
127 fields = []
124 if req.form.has_key('style'):
128 if req.form.has_key('style'):
125 style = req.form['style'][0]
129 style = req.form['style'][0]
126 if style != get('web', 'style', ''):
130 if style != get('web', 'style', ''):
127 fields.append(('style', style))
131 fields.append(('style', style))
128
132
129 separator = url[-1] == '?' and ';' or '?'
133 separator = url[-1] == '?' and ';' or '?'
130 for name, value in fields:
134 for name, value in fields:
131 yield dict(name=name, value=value, separator=separator)
135 yield dict(name=name, value=value, separator=separator)
132 separator = ';'
136 separator = ';'
133
137
134 rows = []
138 rows = []
135 parity = paritygen(self.stripecount)
139 parity = paritygen(self.stripecount)
136 for name, path in self.repos:
140 for name, path in self.repos:
141 if not name.startswith(subdir):
142 continue
143
137 u = ui.ui(parentui=parentui)
144 u = ui.ui(parentui=parentui)
138 try:
145 try:
139 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
146 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
140 except IOError:
147 except IOError:
141 pass
148 pass
142 def get(section, name, default=None):
149 def get(section, name, default=None):
143 return u.config(section, name, default, untrusted=True)
150 return u.config(section, name, default, untrusted=True)
144
151
145 if u.configbool("web", "hidden", untrusted=True):
152 if u.configbool("web", "hidden", untrusted=True):
146 continue
153 continue
147
154
148 url = ('/'.join([req.env["REQUEST_URI"].split('?')[0], name])
155 url = ('/'.join([req.env["REQUEST_URI"].split('?')[0], name[len(subdir):]])
149 .replace("//", "/")) + '/'
156 .replace("//", "/")) + '/'
150
157
151 # update time with local timezone
158 # update time with local timezone
152 try:
159 try:
153 d = (get_mtime(path), util.makedate()[1])
160 d = (get_mtime(path), util.makedate()[1])
154 except OSError:
161 except OSError:
155 continue
162 continue
156
163
157 contact = (get("ui", "username") or # preferred
164 contact = (get("ui", "username") or # preferred
158 get("web", "contact") or # deprecated
165 get("web", "contact") or # deprecated
159 get("web", "author", "")) # also
166 get("web", "author", "")) # also
160 description = get("web", "description", "")
167 description = get("web", "description", "")
161 name = get("web", "name", name)
168 name = get("web", "name", name)
162 row = dict(contact=contact or "unknown",
169 row = dict(contact=contact or "unknown",
163 contact_sort=contact.upper() or "unknown",
170 contact_sort=contact.upper() or "unknown",
164 name=name,
171 name=name,
165 name_sort=name,
172 name_sort=name,
166 url=url,
173 url=url,
167 description=description or "unknown",
174 description=description or "unknown",
168 description_sort=description.upper() or "unknown",
175 description_sort=description.upper() or "unknown",
169 lastchange=d,
176 lastchange=d,
170 lastchange_sort=d[1]-d[0],
177 lastchange_sort=d[1]-d[0],
171 sessionvars=sessionvars,
178 sessionvars=sessionvars,
172 archives=archivelist(u, "tip", url))
179 archives=archivelist(u, "tip", url))
173 if (not sortcolumn
180 if (not sortcolumn
174 or (sortcolumn, descending) == self.repos_sorted):
181 or (sortcolumn, descending) == self.repos_sorted):
175 # fast path for unsorted output
182 # fast path for unsorted output
176 row['parity'] = parity.next()
183 row['parity'] = parity.next()
177 yield row
184 yield row
178 else:
185 else:
179 rows.append((row["%s_sort" % sortcolumn], row))
186 rows.append((row["%s_sort" % sortcolumn], row))
180 if rows:
187 if rows:
181 rows.sort()
188 rows.sort()
182 if descending:
189 if descending:
183 rows.reverse()
190 rows.reverse()
184 for key, row in rows:
191 for key, row in rows:
185 row['parity'] = parity.next()
192 row['parity'] = parity.next()
186 yield row
193 yield row
187
194
195 def makeindex(req, subdir=""):
196 sortable = ["name", "description", "contact", "lastchange"]
197 sortcolumn, descending = self.repos_sorted
198 if req.form.has_key('sort'):
199 sortcolumn = req.form['sort'][0]
200 descending = sortcolumn.startswith('-')
201 if descending:
202 sortcolumn = sortcolumn[1:]
203 if sortcolumn not in sortable:
204 sortcolumn = ""
205
206 sort = [("sort_%s" % column,
207 "%s%s" % ((not descending and column == sortcolumn)
208 and "-" or "", column))
209 for column in sortable]
210 req.write(tmpl("index", entries=entries, subdir=subdir,
211 sortcolumn=sortcolumn, descending=descending,
212 **dict(sort)))
213
188 try:
214 try:
189 virtual = req.env.get("PATH_INFO", "").strip('/')
215 virtual = req.env.get("PATH_INFO", "").strip('/')
190 if virtual.startswith('static/'):
216 if virtual.startswith('static/'):
191 static = os.path.join(templater.templatepath(), 'static')
217 static = os.path.join(templater.templatepath(), 'static')
192 fname = virtual[7:]
218 fname = virtual[7:]
193 req.write(staticfile(static, fname, req) or
219 req.write(staticfile(static, fname, req) or
194 tmpl('error', error='%r not found' % fname))
220 tmpl('error', error='%r not found' % fname))
195 elif virtual:
221 elif virtual:
196 while virtual:
222 while virtual:
197 real = dict(self.repos).get(virtual)
223 real = dict(self.repos).get(virtual)
198 if real:
224 if real:
199 break
225 break
200 up = virtual.rfind('/')
226 up = virtual.rfind('/')
201 if up < 0:
227 if up < 0:
202 break
228 break
203 virtual = virtual[:up]
229 virtual = virtual[:up]
204 if real:
230 if real:
205 req.env['REPO_NAME'] = virtual
231 req.env['REPO_NAME'] = virtual
206 try:
232 try:
207 repo = hg.repository(parentui, real)
233 repo = hg.repository(parentui, real)
208 hgweb(repo).run_wsgi(req)
234 hgweb(repo).run_wsgi(req)
209 except IOError, inst:
235 except IOError, inst:
210 req.write(tmpl("error", error=inst.strerror))
236 req.write(tmpl("error", error=inst.strerror))
211 except hg.RepoError, inst:
237 except hg.RepoError, inst:
212 req.write(tmpl("error", error=str(inst)))
238 req.write(tmpl("error", error=str(inst)))
213 else:
239 else:
240 subdir=req.env.get("PATH_INFO", "").strip('/') + '/'
241 if [r for r in self.repos if r[0].startswith(subdir)]:
242 makeindex(req, subdir)
243 else:
214 req.write(tmpl("notfound", repo=virtual))
244 req.write(tmpl("notfound", repo=virtual))
215 else:
245 else:
216 if req.form.has_key('static'):
246 if req.form.has_key('static'):
217 static = os.path.join(templater.templatepath(), "static")
247 static = os.path.join(templater.templatepath(), "static")
218 fname = req.form['static'][0]
248 fname = req.form['static'][0]
219 req.write(staticfile(static, fname, req)
249 req.write(staticfile(static, fname, req)
220 or tmpl("error", error="%r not found" % fname))
250 or tmpl("error", error="%r not found" % fname))
221 else:
251 else:
222 sortable = ["name", "description", "contact", "lastchange"]
252 makeindex(req)
223 sortcolumn, descending = self.repos_sorted
224 if req.form.has_key('sort'):
225 sortcolumn = req.form['sort'][0]
226 descending = sortcolumn.startswith('-')
227 if descending:
228 sortcolumn = sortcolumn[1:]
229 if sortcolumn not in sortable:
230 sortcolumn = ""
231
232 sort = [("sort_%s" % column,
233 "%s%s" % ((not descending and column == sortcolumn)
234 and "-" or "", column))
235 for column in sortable]
236 req.write(tmpl("index", entries=entries,
237 sortcolumn=sortcolumn, descending=descending,
238 **dict(sort)))
239 finally:
253 finally:
240 tmpl = None
254 tmpl = None
General Comments 0
You need to be logged in to leave comments. Login now