##// END OF EJS Templates
hgweb: move capabilities calculation back into hgweb.protocol
Dirkjan Ochtman -
r6780:4c1d67e0 default
parent child Browse files
Show More
@@ -1,390 +1,377 b''
1 # hgweb/hgweb_mod.py - Web interface for a repository.
1 # hgweb/hgweb_mod.py - Web interface for a repository.
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-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 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, mimetypes
9 import os, mimetypes
10 from mercurial.node import hex, nullid
10 from mercurial.node import hex, nullid
11 from mercurial.repo import RepoError
11 from mercurial.repo import RepoError
12 from mercurial import mdiff, ui, hg, util, patch, hook
12 from mercurial import mdiff, ui, hg, util, patch, hook
13 from mercurial import revlog, templater, templatefilters, changegroup
13 from mercurial import revlog, templater, templatefilters
14 from common import get_mtime, style_map, paritygen, countgen, ErrorResponse
14 from common import get_mtime, style_map, paritygen, countgen, ErrorResponse
15 from common import HTTP_OK, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
15 from common import HTTP_OK, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
16 from request import wsgirequest
16 from request import wsgirequest
17 import webcommands, protocol, webutil
17 import webcommands, protocol, webutil
18
18
19 perms = {
19 perms = {
20 'changegroup': 'pull',
20 'changegroup': 'pull',
21 'changegroupsubset': 'pull',
21 'changegroupsubset': 'pull',
22 'unbundle': 'push',
22 'unbundle': 'push',
23 'stream_out': 'pull',
23 'stream_out': 'pull',
24 }
24 }
25
25
26 class hgweb(object):
26 class hgweb(object):
27 def __init__(self, repo, name=None):
27 def __init__(self, repo, name=None):
28 if isinstance(repo, str):
28 if isinstance(repo, str):
29 parentui = ui.ui(report_untrusted=False, interactive=False)
29 parentui = ui.ui(report_untrusted=False, interactive=False)
30 self.repo = hg.repository(parentui, repo)
30 self.repo = hg.repository(parentui, repo)
31 else:
31 else:
32 self.repo = repo
32 self.repo = repo
33
33
34 hook.redirect(True)
34 hook.redirect(True)
35 self.mtime = -1
35 self.mtime = -1
36 self.reponame = name
36 self.reponame = name
37 self.archives = 'zip', 'gz', 'bz2'
37 self.archives = 'zip', 'gz', 'bz2'
38 self.stripecount = 1
38 self.stripecount = 1
39 self._capabilities = None
40 # a repo owner may set web.templates in .hg/hgrc to get any file
39 # a repo owner may set web.templates in .hg/hgrc to get any file
41 # readable by the user running the CGI script
40 # readable by the user running the CGI script
42 self.templatepath = self.config("web", "templates",
41 self.templatepath = self.config("web", "templates",
43 templater.templatepath(),
42 templater.templatepath(),
44 untrusted=False)
43 untrusted=False)
45
44
46 # The CGI scripts are often run by a user different from the repo owner.
45 # The CGI scripts are often run by a user different from the repo owner.
47 # Trust the settings from the .hg/hgrc files by default.
46 # Trust the settings from the .hg/hgrc files by default.
48 def config(self, section, name, default=None, untrusted=True):
47 def config(self, section, name, default=None, untrusted=True):
49 return self.repo.ui.config(section, name, default,
48 return self.repo.ui.config(section, name, default,
50 untrusted=untrusted)
49 untrusted=untrusted)
51
50
52 def configbool(self, section, name, default=False, untrusted=True):
51 def configbool(self, section, name, default=False, untrusted=True):
53 return self.repo.ui.configbool(section, name, default,
52 return self.repo.ui.configbool(section, name, default,
54 untrusted=untrusted)
53 untrusted=untrusted)
55
54
56 def configlist(self, section, name, default=None, untrusted=True):
55 def configlist(self, section, name, default=None, untrusted=True):
57 return self.repo.ui.configlist(section, name, default,
56 return self.repo.ui.configlist(section, name, default,
58 untrusted=untrusted)
57 untrusted=untrusted)
59
58
60 def refresh(self):
59 def refresh(self):
61 mtime = get_mtime(self.repo.root)
60 mtime = get_mtime(self.repo.root)
62 if mtime != self.mtime:
61 if mtime != self.mtime:
63 self.mtime = mtime
62 self.mtime = mtime
64 self.repo = hg.repository(self.repo.ui, self.repo.root)
63 self.repo = hg.repository(self.repo.ui, self.repo.root)
65 self.maxchanges = int(self.config("web", "maxchanges", 10))
64 self.maxchanges = int(self.config("web", "maxchanges", 10))
66 self.stripecount = int(self.config("web", "stripes", 1))
65 self.stripecount = int(self.config("web", "stripes", 1))
67 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
66 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
68 self.maxfiles = int(self.config("web", "maxfiles", 10))
67 self.maxfiles = int(self.config("web", "maxfiles", 10))
69 self.allowpull = self.configbool("web", "allowpull", True)
68 self.allowpull = self.configbool("web", "allowpull", True)
70 self.encoding = self.config("web", "encoding", util._encoding)
69 self.encoding = self.config("web", "encoding", util._encoding)
71 self._capabilities = None
72
73 def capabilities(self):
74 if self._capabilities is not None:
75 return self._capabilities
76 caps = ['lookup', 'changegroupsubset']
77 if self.configbool('server', 'uncompressed'):
78 caps.append('stream=%d' % self.repo.changelog.version)
79 if changegroup.bundlepriority:
80 caps.append('unbundle=%s' % ','.join(changegroup.bundlepriority))
81 self._capabilities = caps
82 return caps
83
70
84 def run(self):
71 def run(self):
85 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
72 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
86 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
73 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
87 import mercurial.hgweb.wsgicgi as wsgicgi
74 import mercurial.hgweb.wsgicgi as wsgicgi
88 wsgicgi.launch(self)
75 wsgicgi.launch(self)
89
76
90 def __call__(self, env, respond):
77 def __call__(self, env, respond):
91 req = wsgirequest(env, respond)
78 req = wsgirequest(env, respond)
92 self.run_wsgi(req)
79 self.run_wsgi(req)
93 return req
80 return req
94
81
95 def run_wsgi(self, req):
82 def run_wsgi(self, req):
96
83
97 self.refresh()
84 self.refresh()
98
85
99 # process this if it's a protocol request
86 # process this if it's a protocol request
100 # protocol bits don't need to create any URLs
87 # protocol bits don't need to create any URLs
101 # and the clients always use the old URL structure
88 # and the clients always use the old URL structure
102
89
103 cmd = req.form.get('cmd', [''])[0]
90 cmd = req.form.get('cmd', [''])[0]
104 if cmd and cmd in protocol.__all__:
91 if cmd and cmd in protocol.__all__:
105 if cmd in perms and not self.check_perm(req, perms[cmd]):
92 if cmd in perms and not self.check_perm(req, perms[cmd]):
106 return
93 return
107 method = getattr(protocol, cmd)
94 method = getattr(protocol, cmd)
108 method(self, req)
95 method(self, req)
109 return
96 return
110
97
111 # work with CGI variables to create coherent structure
98 # work with CGI variables to create coherent structure
112 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
99 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
113
100
114 req.url = req.env['SCRIPT_NAME']
101 req.url = req.env['SCRIPT_NAME']
115 if not req.url.endswith('/'):
102 if not req.url.endswith('/'):
116 req.url += '/'
103 req.url += '/'
117 if 'REPO_NAME' in req.env:
104 if 'REPO_NAME' in req.env:
118 req.url += req.env['REPO_NAME'] + '/'
105 req.url += req.env['REPO_NAME'] + '/'
119
106
120 if 'PATH_INFO' in req.env:
107 if 'PATH_INFO' in req.env:
121 parts = req.env['PATH_INFO'].strip('/').split('/')
108 parts = req.env['PATH_INFO'].strip('/').split('/')
122 repo_parts = req.env.get('REPO_NAME', '').split('/')
109 repo_parts = req.env.get('REPO_NAME', '').split('/')
123 if parts[:len(repo_parts)] == repo_parts:
110 if parts[:len(repo_parts)] == repo_parts:
124 parts = parts[len(repo_parts):]
111 parts = parts[len(repo_parts):]
125 query = '/'.join(parts)
112 query = '/'.join(parts)
126 else:
113 else:
127 query = req.env['QUERY_STRING'].split('&', 1)[0]
114 query = req.env['QUERY_STRING'].split('&', 1)[0]
128 query = query.split(';', 1)[0]
115 query = query.split(';', 1)[0]
129
116
130 # translate user-visible url structure to internal structure
117 # translate user-visible url structure to internal structure
131
118
132 args = query.split('/', 2)
119 args = query.split('/', 2)
133 if 'cmd' not in req.form and args and args[0]:
120 if 'cmd' not in req.form and args and args[0]:
134
121
135 cmd = args.pop(0)
122 cmd = args.pop(0)
136 style = cmd.rfind('-')
123 style = cmd.rfind('-')
137 if style != -1:
124 if style != -1:
138 req.form['style'] = [cmd[:style]]
125 req.form['style'] = [cmd[:style]]
139 cmd = cmd[style+1:]
126 cmd = cmd[style+1:]
140
127
141 # avoid accepting e.g. style parameter as command
128 # avoid accepting e.g. style parameter as command
142 if hasattr(webcommands, cmd):
129 if hasattr(webcommands, cmd):
143 req.form['cmd'] = [cmd]
130 req.form['cmd'] = [cmd]
144 else:
131 else:
145 cmd = ''
132 cmd = ''
146
133
147 if args and args[0]:
134 if args and args[0]:
148 node = args.pop(0)
135 node = args.pop(0)
149 req.form['node'] = [node]
136 req.form['node'] = [node]
150 if args:
137 if args:
151 req.form['file'] = args
138 req.form['file'] = args
152
139
153 if cmd == 'static':
140 if cmd == 'static':
154 req.form['file'] = req.form['node']
141 req.form['file'] = req.form['node']
155 elif cmd == 'archive':
142 elif cmd == 'archive':
156 fn = req.form['node'][0]
143 fn = req.form['node'][0]
157 for type_, spec in self.archive_specs.iteritems():
144 for type_, spec in self.archive_specs.iteritems():
158 ext = spec[2]
145 ext = spec[2]
159 if fn.endswith(ext):
146 if fn.endswith(ext):
160 req.form['node'] = [fn[:-len(ext)]]
147 req.form['node'] = [fn[:-len(ext)]]
161 req.form['type'] = [type_]
148 req.form['type'] = [type_]
162
149
163 # process the web interface request
150 # process the web interface request
164
151
165 try:
152 try:
166
153
167 tmpl = self.templater(req)
154 tmpl = self.templater(req)
168 ctype = tmpl('mimetype', encoding=self.encoding)
155 ctype = tmpl('mimetype', encoding=self.encoding)
169 ctype = templater.stringify(ctype)
156 ctype = templater.stringify(ctype)
170
157
171 if cmd == '':
158 if cmd == '':
172 req.form['cmd'] = [tmpl.cache['default']]
159 req.form['cmd'] = [tmpl.cache['default']]
173 cmd = req.form['cmd'][0]
160 cmd = req.form['cmd'][0]
174
161
175 if cmd not in webcommands.__all__:
162 if cmd not in webcommands.__all__:
176 msg = 'no such method: %s' % cmd
163 msg = 'no such method: %s' % cmd
177 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
164 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
178 elif cmd == 'file' and 'raw' in req.form.get('style', []):
165 elif cmd == 'file' and 'raw' in req.form.get('style', []):
179 self.ctype = ctype
166 self.ctype = ctype
180 content = webcommands.rawfile(self, req, tmpl)
167 content = webcommands.rawfile(self, req, tmpl)
181 else:
168 else:
182 content = getattr(webcommands, cmd)(self, req, tmpl)
169 content = getattr(webcommands, cmd)(self, req, tmpl)
183 req.respond(HTTP_OK, ctype)
170 req.respond(HTTP_OK, ctype)
184
171
185 req.write(content)
172 req.write(content)
186 del tmpl
173 del tmpl
187
174
188 except revlog.LookupError, err:
175 except revlog.LookupError, err:
189 req.respond(HTTP_NOT_FOUND, ctype)
176 req.respond(HTTP_NOT_FOUND, ctype)
190 msg = str(err)
177 msg = str(err)
191 if 'manifest' not in msg:
178 if 'manifest' not in msg:
192 msg = 'revision not found: %s' % err.name
179 msg = 'revision not found: %s' % err.name
193 req.write(tmpl('error', error=msg))
180 req.write(tmpl('error', error=msg))
194 except (RepoError, revlog.RevlogError), inst:
181 except (RepoError, revlog.RevlogError), inst:
195 req.respond(HTTP_SERVER_ERROR, ctype)
182 req.respond(HTTP_SERVER_ERROR, ctype)
196 req.write(tmpl('error', error=str(inst)))
183 req.write(tmpl('error', error=str(inst)))
197 except ErrorResponse, inst:
184 except ErrorResponse, inst:
198 req.respond(inst.code, ctype)
185 req.respond(inst.code, ctype)
199 req.write(tmpl('error', error=inst.message))
186 req.write(tmpl('error', error=inst.message))
200
187
201 def templater(self, req):
188 def templater(self, req):
202
189
203 # determine scheme, port and server name
190 # determine scheme, port and server name
204 # this is needed to create absolute urls
191 # this is needed to create absolute urls
205
192
206 proto = req.env.get('wsgi.url_scheme')
193 proto = req.env.get('wsgi.url_scheme')
207 if proto == 'https':
194 if proto == 'https':
208 proto = 'https'
195 proto = 'https'
209 default_port = "443"
196 default_port = "443"
210 else:
197 else:
211 proto = 'http'
198 proto = 'http'
212 default_port = "80"
199 default_port = "80"
213
200
214 port = req.env["SERVER_PORT"]
201 port = req.env["SERVER_PORT"]
215 port = port != default_port and (":" + port) or ""
202 port = port != default_port and (":" + port) or ""
216 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
203 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
217 staticurl = self.config("web", "staticurl") or req.url + 'static/'
204 staticurl = self.config("web", "staticurl") or req.url + 'static/'
218 if not staticurl.endswith('/'):
205 if not staticurl.endswith('/'):
219 staticurl += '/'
206 staticurl += '/'
220
207
221 # some functions for the templater
208 # some functions for the templater
222
209
223 def header(**map):
210 def header(**map):
224 yield tmpl('header', encoding=self.encoding, **map)
211 yield tmpl('header', encoding=self.encoding, **map)
225
212
226 def footer(**map):
213 def footer(**map):
227 yield tmpl("footer", **map)
214 yield tmpl("footer", **map)
228
215
229 def motd(**map):
216 def motd(**map):
230 yield self.config("web", "motd", "")
217 yield self.config("web", "motd", "")
231
218
232 def sessionvars(**map):
219 def sessionvars(**map):
233 fields = []
220 fields = []
234 if 'style' in req.form:
221 if 'style' in req.form:
235 style = req.form['style'][0]
222 style = req.form['style'][0]
236 if style != self.config('web', 'style', ''):
223 if style != self.config('web', 'style', ''):
237 fields.append(('style', style))
224 fields.append(('style', style))
238
225
239 separator = req.url[-1] == '?' and ';' or '?'
226 separator = req.url[-1] == '?' and ';' or '?'
240 for name, value in fields:
227 for name, value in fields:
241 yield dict(name=name, value=value, separator=separator)
228 yield dict(name=name, value=value, separator=separator)
242 separator = ';'
229 separator = ';'
243
230
244 # figure out which style to use
231 # figure out which style to use
245
232
246 style = self.config("web", "style", "")
233 style = self.config("web", "style", "")
247 if 'style' in req.form:
234 if 'style' in req.form:
248 style = req.form['style'][0]
235 style = req.form['style'][0]
249 mapfile = style_map(self.templatepath, style)
236 mapfile = style_map(self.templatepath, style)
250
237
251 if not self.reponame:
238 if not self.reponame:
252 self.reponame = (self.config("web", "name")
239 self.reponame = (self.config("web", "name")
253 or req.env.get('REPO_NAME')
240 or req.env.get('REPO_NAME')
254 or req.url.strip('/') or self.repo.root)
241 or req.url.strip('/') or self.repo.root)
255
242
256 # create the templater
243 # create the templater
257
244
258 tmpl = templater.templater(mapfile, templatefilters.filters,
245 tmpl = templater.templater(mapfile, templatefilters.filters,
259 defaults={"url": req.url,
246 defaults={"url": req.url,
260 "staticurl": staticurl,
247 "staticurl": staticurl,
261 "urlbase": urlbase,
248 "urlbase": urlbase,
262 "repo": self.reponame,
249 "repo": self.reponame,
263 "header": header,
250 "header": header,
264 "footer": footer,
251 "footer": footer,
265 "motd": motd,
252 "motd": motd,
266 "sessionvars": sessionvars
253 "sessionvars": sessionvars
267 })
254 })
268 return tmpl
255 return tmpl
269
256
270 def archivelist(self, nodeid):
257 def archivelist(self, nodeid):
271 allowed = self.configlist("web", "allow_archive")
258 allowed = self.configlist("web", "allow_archive")
272 for i, spec in self.archive_specs.iteritems():
259 for i, spec in self.archive_specs.iteritems():
273 if i in allowed or self.configbool("web", "allow" + i):
260 if i in allowed or self.configbool("web", "allow" + i):
274 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
261 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
275
262
276 def listfilediffs(self, tmpl, files, changeset):
263 def listfilediffs(self, tmpl, files, changeset):
277 for f in files[:self.maxfiles]:
264 for f in files[:self.maxfiles]:
278 yield tmpl("filedifflink", node=hex(changeset), file=f)
265 yield tmpl("filedifflink", node=hex(changeset), file=f)
279 if len(files) > self.maxfiles:
266 if len(files) > self.maxfiles:
280 yield tmpl("fileellipses")
267 yield tmpl("fileellipses")
281
268
282 def diff(self, tmpl, node1, node2, files):
269 def diff(self, tmpl, node1, node2, files):
283 def filterfiles(filters, files):
270 def filterfiles(filters, files):
284 l = [x for x in files if x in filters]
271 l = [x for x in files if x in filters]
285
272
286 for t in filters:
273 for t in filters:
287 if t and t[-1] != os.sep:
274 if t and t[-1] != os.sep:
288 t += os.sep
275 t += os.sep
289 l += [x for x in files if x.startswith(t)]
276 l += [x for x in files if x.startswith(t)]
290 return l
277 return l
291
278
292 parity = paritygen(self.stripecount)
279 parity = paritygen(self.stripecount)
293 def diffblock(diff, f, fn):
280 def diffblock(diff, f, fn):
294 yield tmpl("diffblock",
281 yield tmpl("diffblock",
295 lines=prettyprintlines(diff),
282 lines=prettyprintlines(diff),
296 parity=parity.next(),
283 parity=parity.next(),
297 file=f,
284 file=f,
298 filenode=hex(fn or nullid))
285 filenode=hex(fn or nullid))
299
286
300 blockcount = countgen()
287 blockcount = countgen()
301 def prettyprintlines(diff):
288 def prettyprintlines(diff):
302 blockno = blockcount.next()
289 blockno = blockcount.next()
303 for lineno, l in enumerate(diff.splitlines(1)):
290 for lineno, l in enumerate(diff.splitlines(1)):
304 if blockno == 0:
291 if blockno == 0:
305 lineno = lineno + 1
292 lineno = lineno + 1
306 else:
293 else:
307 lineno = "%d.%d" % (blockno, lineno + 1)
294 lineno = "%d.%d" % (blockno, lineno + 1)
308 if l.startswith('+'):
295 if l.startswith('+'):
309 ltype = "difflineplus"
296 ltype = "difflineplus"
310 elif l.startswith('-'):
297 elif l.startswith('-'):
311 ltype = "difflineminus"
298 ltype = "difflineminus"
312 elif l.startswith('@'):
299 elif l.startswith('@'):
313 ltype = "difflineat"
300 ltype = "difflineat"
314 else:
301 else:
315 ltype = "diffline"
302 ltype = "diffline"
316 yield tmpl(ltype,
303 yield tmpl(ltype,
317 line=l,
304 line=l,
318 lineid="l%s" % lineno,
305 lineid="l%s" % lineno,
319 linenumber="% 8s" % lineno)
306 linenumber="% 8s" % lineno)
320
307
321 r = self.repo
308 r = self.repo
322 c1 = r[node1]
309 c1 = r[node1]
323 c2 = r[node2]
310 c2 = r[node2]
324 date1 = util.datestr(c1.date())
311 date1 = util.datestr(c1.date())
325 date2 = util.datestr(c2.date())
312 date2 = util.datestr(c2.date())
326
313
327 modified, added, removed, deleted, unknown = r.status(node1, node2)[:5]
314 modified, added, removed, deleted, unknown = r.status(node1, node2)[:5]
328 if files:
315 if files:
329 modified, added, removed = map(lambda x: filterfiles(files, x),
316 modified, added, removed = map(lambda x: filterfiles(files, x),
330 (modified, added, removed))
317 (modified, added, removed))
331
318
332 diffopts = patch.diffopts(self.repo.ui, untrusted=True)
319 diffopts = patch.diffopts(self.repo.ui, untrusted=True)
333 for f in modified:
320 for f in modified:
334 to = c1.filectx(f).data()
321 to = c1.filectx(f).data()
335 tn = c2.filectx(f).data()
322 tn = c2.filectx(f).data()
336 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
323 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
337 opts=diffopts), f, tn)
324 opts=diffopts), f, tn)
338 for f in added:
325 for f in added:
339 to = None
326 to = None
340 tn = c2.filectx(f).data()
327 tn = c2.filectx(f).data()
341 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
328 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
342 opts=diffopts), f, tn)
329 opts=diffopts), f, tn)
343 for f in removed:
330 for f in removed:
344 to = c1.filectx(f).data()
331 to = c1.filectx(f).data()
345 tn = None
332 tn = None
346 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
333 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
347 opts=diffopts), f, tn)
334 opts=diffopts), f, tn)
348
335
349 archive_specs = {
336 archive_specs = {
350 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
337 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
351 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
338 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
352 'zip': ('application/zip', 'zip', '.zip', None),
339 'zip': ('application/zip', 'zip', '.zip', None),
353 }
340 }
354
341
355 def check_perm(self, req, op):
342 def check_perm(self, req, op):
356 '''Check permission for operation based on request data (including
343 '''Check permission for operation based on request data (including
357 authentication info. Return true if op allowed, else false.'''
344 authentication info. Return true if op allowed, else false.'''
358
345
359 def error(status, message):
346 def error(status, message):
360 req.respond(status, protocol.HGTYPE)
347 req.respond(status, protocol.HGTYPE)
361 req.write('0\n%s\n' % message)
348 req.write('0\n%s\n' % message)
362
349
363 if op == 'pull':
350 if op == 'pull':
364 return self.allowpull
351 return self.allowpull
365
352
366 # enforce that you can only push using POST requests
353 # enforce that you can only push using POST requests
367 if req.env['REQUEST_METHOD'] != 'POST':
354 if req.env['REQUEST_METHOD'] != 'POST':
368 error('405 Method Not Allowed', 'push requires POST request')
355 error('405 Method Not Allowed', 'push requires POST request')
369 return False
356 return False
370
357
371 # require ssl by default for pushing, auth info cannot be sniffed
358 # require ssl by default for pushing, auth info cannot be sniffed
372 # and replayed
359 # and replayed
373 scheme = req.env.get('wsgi.url_scheme')
360 scheme = req.env.get('wsgi.url_scheme')
374 if self.configbool('web', 'push_ssl', True) and scheme != 'https':
361 if self.configbool('web', 'push_ssl', True) and scheme != 'https':
375 error(HTTP_OK, 'ssl required')
362 error(HTTP_OK, 'ssl required')
376 return False
363 return False
377
364
378 user = req.env.get('REMOTE_USER')
365 user = req.env.get('REMOTE_USER')
379
366
380 deny = self.configlist('web', 'deny_push')
367 deny = self.configlist('web', 'deny_push')
381 if deny and (not user or deny == ['*'] or user in deny):
368 if deny and (not user or deny == ['*'] or user in deny):
382 error('401 Unauthorized', 'push not authorized')
369 error('401 Unauthorized', 'push not authorized')
383 return False
370 return False
384
371
385 allow = self.configlist('web', 'allow_push')
372 allow = self.configlist('web', 'allow_push')
386 result = allow and (allow == ['*'] or user in allow)
373 result = allow and (allow == ['*'] or user in allow)
387 if not result:
374 if not result:
388 error('401 Unauthorized', 'push not authorized')
375 error('401 Unauthorized', 'push not authorized')
389
376
390 return result
377 return result
@@ -1,203 +1,208 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 cStringIO, zlib, tempfile, errno, os, sys
8 import cStringIO, zlib, tempfile, errno, os, sys
9 from mercurial import util, streamclone
9 from mercurial import util, streamclone
10 from mercurial.node import bin, hex
10 from mercurial.node import bin, hex
11 from mercurial import changegroup as changegroupmod
11 from mercurial import changegroup as changegroupmod
12 from common import HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
12 from common import HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
13
13
14 # __all__ is populated with the allowed commands. Be sure to add to it if
14 # __all__ is populated with the allowed commands. Be sure to add to it if
15 # you're adding a new command, or the new command won't work.
15 # you're adding a new command, or the new command won't work.
16
16
17 __all__ = [
17 __all__ = [
18 'lookup', 'heads', 'branches', 'between', 'changegroup',
18 'lookup', 'heads', 'branches', 'between', 'changegroup',
19 'changegroupsubset', 'capabilities', 'unbundle', 'stream_out',
19 'changegroupsubset', 'capabilities', 'unbundle', 'stream_out',
20 ]
20 ]
21
21
22 HGTYPE = 'application/mercurial-0.1'
22 HGTYPE = 'application/mercurial-0.1'
23
23
24 def lookup(web, req):
24 def lookup(web, req):
25 try:
25 try:
26 r = hex(web.repo.lookup(req.form['key'][0]))
26 r = hex(web.repo.lookup(req.form['key'][0]))
27 success = 1
27 success = 1
28 except Exception,inst:
28 except Exception,inst:
29 r = str(inst)
29 r = str(inst)
30 success = 0
30 success = 0
31 resp = "%s %s\n" % (success, r)
31 resp = "%s %s\n" % (success, r)
32 req.respond(HTTP_OK, HGTYPE, length=len(resp))
32 req.respond(HTTP_OK, HGTYPE, length=len(resp))
33 req.write(resp)
33 req.write(resp)
34
34
35 def heads(web, req):
35 def heads(web, req):
36 resp = " ".join(map(hex, web.repo.heads())) + "\n"
36 resp = " ".join(map(hex, web.repo.heads())) + "\n"
37 req.respond(HTTP_OK, HGTYPE, length=len(resp))
37 req.respond(HTTP_OK, HGTYPE, length=len(resp))
38 req.write(resp)
38 req.write(resp)
39
39
40 def branches(web, req):
40 def branches(web, req):
41 nodes = []
41 nodes = []
42 if 'nodes' in req.form:
42 if 'nodes' in req.form:
43 nodes = map(bin, req.form['nodes'][0].split(" "))
43 nodes = map(bin, req.form['nodes'][0].split(" "))
44 resp = cStringIO.StringIO()
44 resp = cStringIO.StringIO()
45 for b in web.repo.branches(nodes):
45 for b in web.repo.branches(nodes):
46 resp.write(" ".join(map(hex, b)) + "\n")
46 resp.write(" ".join(map(hex, b)) + "\n")
47 resp = resp.getvalue()
47 resp = resp.getvalue()
48 req.respond(HTTP_OK, HGTYPE, length=len(resp))
48 req.respond(HTTP_OK, HGTYPE, length=len(resp))
49 req.write(resp)
49 req.write(resp)
50
50
51 def between(web, req):
51 def between(web, req):
52 if 'pairs' in req.form:
52 if 'pairs' in req.form:
53 pairs = [map(bin, p.split("-"))
53 pairs = [map(bin, p.split("-"))
54 for p in req.form['pairs'][0].split(" ")]
54 for p in req.form['pairs'][0].split(" ")]
55 resp = cStringIO.StringIO()
55 resp = cStringIO.StringIO()
56 for b in web.repo.between(pairs):
56 for b in web.repo.between(pairs):
57 resp.write(" ".join(map(hex, b)) + "\n")
57 resp.write(" ".join(map(hex, b)) + "\n")
58 resp = resp.getvalue()
58 resp = resp.getvalue()
59 req.respond(HTTP_OK, HGTYPE, length=len(resp))
59 req.respond(HTTP_OK, HGTYPE, length=len(resp))
60 req.write(resp)
60 req.write(resp)
61
61
62 def changegroup(web, req):
62 def changegroup(web, req):
63 req.respond(HTTP_OK, HGTYPE)
63 req.respond(HTTP_OK, HGTYPE)
64 nodes = []
64 nodes = []
65
65
66 if 'roots' in req.form:
66 if 'roots' in req.form:
67 nodes = map(bin, req.form['roots'][0].split(" "))
67 nodes = map(bin, req.form['roots'][0].split(" "))
68
68
69 z = zlib.compressobj()
69 z = zlib.compressobj()
70 f = web.repo.changegroup(nodes, 'serve')
70 f = web.repo.changegroup(nodes, 'serve')
71 while 1:
71 while 1:
72 chunk = f.read(4096)
72 chunk = f.read(4096)
73 if not chunk:
73 if not chunk:
74 break
74 break
75 req.write(z.compress(chunk))
75 req.write(z.compress(chunk))
76
76
77 req.write(z.flush())
77 req.write(z.flush())
78
78
79 def changegroupsubset(web, req):
79 def changegroupsubset(web, req):
80 req.respond(HTTP_OK, HGTYPE)
80 req.respond(HTTP_OK, HGTYPE)
81 bases = []
81 bases = []
82 heads = []
82 heads = []
83
83
84 if 'bases' in req.form:
84 if 'bases' in req.form:
85 bases = [bin(x) for x in req.form['bases'][0].split(' ')]
85 bases = [bin(x) for x in req.form['bases'][0].split(' ')]
86 if 'heads' in req.form:
86 if 'heads' in req.form:
87 heads = [bin(x) for x in req.form['heads'][0].split(' ')]
87 heads = [bin(x) for x in req.form['heads'][0].split(' ')]
88
88
89 z = zlib.compressobj()
89 z = zlib.compressobj()
90 f = web.repo.changegroupsubset(bases, heads, 'serve')
90 f = web.repo.changegroupsubset(bases, heads, 'serve')
91 while 1:
91 while 1:
92 chunk = f.read(4096)
92 chunk = f.read(4096)
93 if not chunk:
93 if not chunk:
94 break
94 break
95 req.write(z.compress(chunk))
95 req.write(z.compress(chunk))
96
96
97 req.write(z.flush())
97 req.write(z.flush())
98
98
99 def capabilities(web, req):
99 def capabilities(web, req):
100 resp = ' '.join(web.capabilities())
100 caps = ['lookup', 'changegroupsubset']
101 req.respond(HTTP_OK, HGTYPE, length=len(resp))
101 if web.repo.ui.configbool('server', 'uncompressed', untrusted=True):
102 req.write(resp)
102 caps.append('stream=%d' % web.repo.changelog.version)
103 if changegroupmod.bundlepriority:
104 caps.append('unbundle=%s' % ','.join(changegroupmod.bundlepriority))
105 rsp = ' '.join(caps)
106 req.respond(HTTP_OK, HGTYPE, length=len(rsp))
107 req.write(rsp)
103
108
104 def unbundle(web, req):
109 def unbundle(web, req):
105
110
106 def bail(response, headers={}):
111 def bail(response, headers={}):
107 length = int(req.env.get('CONTENT_LENGTH', 0))
112 length = int(req.env.get('CONTENT_LENGTH', 0))
108 for s in util.filechunkiter(req, limit=length):
113 for s in util.filechunkiter(req, limit=length):
109 # drain incoming bundle, else client will not see
114 # drain incoming bundle, else client will not see
110 # response when run outside cgi script
115 # response when run outside cgi script
111 pass
116 pass
112
117
113 status = headers.pop('status', HTTP_OK)
118 status = headers.pop('status', HTTP_OK)
114 req.header(headers.items())
119 req.header(headers.items())
115 req.respond(status, HGTYPE)
120 req.respond(status, HGTYPE)
116 req.write('0\n')
121 req.write('0\n')
117 req.write(response)
122 req.write(response)
118
123
119 proto = req.env.get('wsgi.url_scheme') or 'http'
124 proto = req.env.get('wsgi.url_scheme') or 'http'
120 their_heads = req.form['heads'][0].split(' ')
125 their_heads = req.form['heads'][0].split(' ')
121
126
122 def check_heads():
127 def check_heads():
123 heads = map(hex, web.repo.heads())
128 heads = map(hex, web.repo.heads())
124 return their_heads == [hex('force')] or their_heads == heads
129 return their_heads == [hex('force')] or their_heads == heads
125
130
126 # fail early if possible
131 # fail early if possible
127 if not check_heads():
132 if not check_heads():
128 bail('unsynced changes\n')
133 bail('unsynced changes\n')
129 return
134 return
130
135
131 req.respond(HTTP_OK, HGTYPE)
136 req.respond(HTTP_OK, HGTYPE)
132
137
133 # do not lock repo until all changegroup data is
138 # do not lock repo until all changegroup data is
134 # streamed. save to temporary file.
139 # streamed. save to temporary file.
135
140
136 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
141 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
137 fp = os.fdopen(fd, 'wb+')
142 fp = os.fdopen(fd, 'wb+')
138 try:
143 try:
139 length = int(req.env['CONTENT_LENGTH'])
144 length = int(req.env['CONTENT_LENGTH'])
140 for s in util.filechunkiter(req, limit=length):
145 for s in util.filechunkiter(req, limit=length):
141 fp.write(s)
146 fp.write(s)
142
147
143 try:
148 try:
144 lock = web.repo.lock()
149 lock = web.repo.lock()
145 try:
150 try:
146 if not check_heads():
151 if not check_heads():
147 req.write('0\n')
152 req.write('0\n')
148 req.write('unsynced changes\n')
153 req.write('unsynced changes\n')
149 return
154 return
150
155
151 fp.seek(0)
156 fp.seek(0)
152 header = fp.read(6)
157 header = fp.read(6)
153 if header.startswith('HG') and not header.startswith('HG10'):
158 if header.startswith('HG') and not header.startswith('HG10'):
154 raise ValueError('unknown bundle version')
159 raise ValueError('unknown bundle version')
155 elif header not in changegroupmod.bundletypes:
160 elif header not in changegroupmod.bundletypes:
156 raise ValueError('unknown bundle compression type')
161 raise ValueError('unknown bundle compression type')
157 gen = changegroupmod.unbundle(header, fp)
162 gen = changegroupmod.unbundle(header, fp)
158
163
159 # send addchangegroup output to client
164 # send addchangegroup output to client
160
165
161 oldio = sys.stdout, sys.stderr
166 oldio = sys.stdout, sys.stderr
162 sys.stderr = sys.stdout = cStringIO.StringIO()
167 sys.stderr = sys.stdout = cStringIO.StringIO()
163
168
164 try:
169 try:
165 url = 'remote:%s:%s' % (proto,
170 url = 'remote:%s:%s' % (proto,
166 req.env.get('REMOTE_HOST', ''))
171 req.env.get('REMOTE_HOST', ''))
167 try:
172 try:
168 ret = web.repo.addchangegroup(gen, 'serve', url)
173 ret = web.repo.addchangegroup(gen, 'serve', url)
169 except util.Abort, inst:
174 except util.Abort, inst:
170 sys.stdout.write("abort: %s\n" % inst)
175 sys.stdout.write("abort: %s\n" % inst)
171 ret = 0
176 ret = 0
172 finally:
177 finally:
173 val = sys.stdout.getvalue()
178 val = sys.stdout.getvalue()
174 sys.stdout, sys.stderr = oldio
179 sys.stdout, sys.stderr = oldio
175 req.write('%d\n' % ret)
180 req.write('%d\n' % ret)
176 req.write(val)
181 req.write(val)
177 finally:
182 finally:
178 del lock
183 del lock
179 except ValueError, inst:
184 except ValueError, inst:
180 req.write('0\n')
185 req.write('0\n')
181 req.write(str(inst) + '\n')
186 req.write(str(inst) + '\n')
182 except (OSError, IOError), inst:
187 except (OSError, IOError), inst:
183 req.write('0\n')
188 req.write('0\n')
184 filename = getattr(inst, 'filename', '')
189 filename = getattr(inst, 'filename', '')
185 # Don't send our filesystem layout to the client
190 # Don't send our filesystem layout to the client
186 if filename.startswith(web.repo.root):
191 if filename.startswith(web.repo.root):
187 filename = filename[len(web.repo.root)+1:]
192 filename = filename[len(web.repo.root)+1:]
188 else:
193 else:
189 filename = ''
194 filename = ''
190 error = getattr(inst, 'strerror', 'Unknown error')
195 error = getattr(inst, 'strerror', 'Unknown error')
191 if inst.errno == errno.ENOENT:
196 if inst.errno == errno.ENOENT:
192 code = HTTP_NOT_FOUND
197 code = HTTP_NOT_FOUND
193 else:
198 else:
194 code = HTTP_SERVER_ERROR
199 code = HTTP_SERVER_ERROR
195 req.respond(code)
200 req.respond(code)
196 req.write('%s: %s\n' % (error, filename))
201 req.write('%s: %s\n' % (error, filename))
197 finally:
202 finally:
198 fp.close()
203 fp.close()
199 os.unlink(tempname)
204 os.unlink(tempname)
200
205
201 def stream_out(web, req):
206 def stream_out(web, req):
202 req.respond(HTTP_OK, HGTYPE)
207 req.respond(HTTP_OK, HGTYPE)
203 streamclone.stream_out(web.repo, req, untrusted=True)
208 streamclone.stream_out(web.repo, req, untrusted=True)
General Comments 0
You need to be logged in to leave comments. Login now