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