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 errno, os, mimetypes, re, zlib, mimetools, cStringIO, sys |
9 import os, mimetypes, re, mimetools, cStringIO |
10 import tempfile, urllib, bz2 |
|
11 from mercurial.node import * |
10 from mercurial.node import * |
12 from mercurial.i18n import gettext as _ |
11 from mercurial import mdiff, ui, hg, util, archival, patch |
13 from mercurial import mdiff, ui, hg, util, archival, streamclone, patch |
|
14 from mercurial import revlog, templater |
12 from mercurial import revlog, templater |
15 from common import ErrorResponse, get_mtime, staticfile, style_map, paritygen |
13 from common import ErrorResponse, get_mtime, style_map, paritygen |
16 from request import wsgirequest |
14 from request import wsgirequest |
|
15 import webcommands, protocol |
|
16 |
|
17 shortcuts = { |
|
18 'cl': [('cmd', ['changelog']), ('rev', None)], |
|
19 'sl': [('cmd', ['shortlog']), ('rev', None)], |
|
20 'cs': [('cmd', ['changeset']), ('node', None)], |
|
21 'f': [('cmd', ['file']), ('filenode', None)], |
|
22 'fl': [('cmd', ['filelog']), ('filenode', None)], |
|
23 'fd': [('cmd', ['filediff']), ('node', None)], |
|
24 'fa': [('cmd', ['annotate']), ('filenode', None)], |
|
25 'mf': [('cmd', ['manifest']), ('manifest', None)], |
|
26 'ca': [('cmd', ['archive']), ('node', None)], |
|
27 'tags': [('cmd', ['tags'])], |
|
28 'tip': [('cmd', ['changeset']), ('node', ['tip'])], |
|
29 'static': [('cmd', ['static']), ('file', None)] |
|
30 } |
17 |
31 |
18 def _up(p): |
32 def _up(p): |
19 if p[0] != "/": |
33 if p[0] != "/": |
20 p = "/" + p |
34 p = "/" + p |
21 if p[-1] == "/": |
35 if p[-1] == "/": |
105 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60)) |
119 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60)) |
106 self.maxfiles = int(self.config("web", "maxfiles", 10)) |
120 self.maxfiles = int(self.config("web", "maxfiles", 10)) |
107 self.allowpull = self.configbool("web", "allowpull", True) |
121 self.allowpull = self.configbool("web", "allowpull", True) |
108 self.encoding = self.config("web", "encoding", util._encoding) |
122 self.encoding = self.config("web", "encoding", util._encoding) |
109 |
123 |
|
124 def run(self): |
|
125 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."): |
|
126 raise RuntimeError("This function is only intended to be called while running as a CGI script.") |
|
127 import mercurial.hgweb.wsgicgi as wsgicgi |
|
128 wsgicgi.launch(self) |
|
129 |
|
130 def __call__(self, env, respond): |
|
131 req = wsgirequest(env, respond) |
|
132 self.run_wsgi(req) |
|
133 return req |
|
134 |
|
135 def run_wsgi(self, req): |
|
136 |
|
137 self.refresh() |
|
138 |
|
139 # expand form shortcuts |
|
140 |
|
141 for k in shortcuts.iterkeys(): |
|
142 if k in req.form: |
|
143 for name, value in shortcuts[k]: |
|
144 if value is None: |
|
145 value = req.form[k] |
|
146 req.form[name] = value |
|
147 del req.form[k] |
|
148 |
|
149 # work with CGI variables to create coherent structure |
|
150 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME |
|
151 |
|
152 req.url = req.env['SCRIPT_NAME'] |
|
153 if not req.url.endswith('/'): |
|
154 req.url += '/' |
|
155 if req.env.has_key('REPO_NAME'): |
|
156 req.url += req.env['REPO_NAME'] + '/' |
|
157 |
|
158 if req.env.get('PATH_INFO'): |
|
159 parts = req.env.get('PATH_INFO').strip('/').split('/') |
|
160 repo_parts = req.env.get('REPO_NAME', '').split('/') |
|
161 if parts[:len(repo_parts)] == repo_parts: |
|
162 parts = parts[len(repo_parts):] |
|
163 query = '/'.join(parts) |
|
164 else: |
|
165 query = req.env['QUERY_STRING'].split('&', 1)[0] |
|
166 query = query.split(';', 1)[0] |
|
167 |
|
168 # translate user-visible url structure to internal structure |
|
169 |
|
170 args = query.split('/', 2) |
|
171 if 'cmd' not in req.form and args and args[0]: |
|
172 |
|
173 cmd = args.pop(0) |
|
174 style = cmd.rfind('-') |
|
175 if style != -1: |
|
176 req.form['style'] = [cmd[:style]] |
|
177 cmd = cmd[style+1:] |
|
178 |
|
179 # avoid accepting e.g. style parameter as command |
|
180 if hasattr(webcommands, cmd) or hasattr(protocol, cmd): |
|
181 req.form['cmd'] = [cmd] |
|
182 |
|
183 if args and args[0]: |
|
184 node = args.pop(0) |
|
185 req.form['node'] = [node] |
|
186 if args: |
|
187 req.form['file'] = args |
|
188 |
|
189 if cmd == 'static': |
|
190 req.form['file'] = req.form['node'] |
|
191 elif cmd == 'archive': |
|
192 fn = req.form['node'][0] |
|
193 for type_, spec in self.archive_specs.iteritems(): |
|
194 ext = spec[2] |
|
195 if fn.endswith(ext): |
|
196 req.form['node'] = [fn[:-len(ext)]] |
|
197 req.form['type'] = [type_] |
|
198 |
|
199 # actually process the request |
|
200 |
|
201 try: |
|
202 |
|
203 cmd = req.form.get('cmd', [''])[0] |
|
204 if hasattr(protocol, cmd): |
|
205 method = getattr(protocol, cmd) |
|
206 method(self, req) |
|
207 else: |
|
208 tmpl = self.templater(req) |
|
209 if cmd == '': |
|
210 req.form['cmd'] = [tmpl.cache['default']] |
|
211 cmd = req.form['cmd'][0] |
|
212 method = getattr(webcommands, cmd) |
|
213 method(self, req, tmpl) |
|
214 del tmpl |
|
215 |
|
216 except revlog.LookupError, err: |
|
217 req.respond(404, tmpl( |
|
218 'error', error='revision not found: %s' % err.name)) |
|
219 except (hg.RepoError, revlog.RevlogError), inst: |
|
220 req.respond('500 Internal Server Error', |
|
221 tmpl('error', error=str(inst))) |
|
222 except ErrorResponse, inst: |
|
223 req.respond(inst.code, tmpl('error', error=inst.message)) |
|
224 except AttributeError: |
|
225 req.respond(400, tmpl('error', error='No such method: ' + cmd)) |
|
226 |
|
227 def templater(self, req): |
|
228 |
|
229 # determine scheme, port and server name |
|
230 # this is needed to create absolute urls |
|
231 |
|
232 proto = req.env.get('wsgi.url_scheme') |
|
233 if proto == 'https': |
|
234 proto = 'https' |
|
235 default_port = "443" |
|
236 else: |
|
237 proto = 'http' |
|
238 default_port = "80" |
|
239 |
|
240 port = req.env["SERVER_PORT"] |
|
241 port = port != default_port and (":" + port) or "" |
|
242 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port) |
|
243 staticurl = self.config("web", "staticurl") or req.url + 'static/' |
|
244 if not staticurl.endswith('/'): |
|
245 staticurl += '/' |
|
246 |
|
247 # some functions for the templater |
|
248 |
|
249 def header(**map): |
|
250 header_file = cStringIO.StringIO( |
|
251 ''.join(tmpl("header", encoding=self.encoding, **map))) |
|
252 msg = mimetools.Message(header_file, 0) |
|
253 req.header(msg.items()) |
|
254 yield header_file.read() |
|
255 |
|
256 def rawfileheader(**map): |
|
257 req.header([('Content-type', map['mimetype']), |
|
258 ('Content-disposition', 'filename=%s' % map['file']), |
|
259 ('Content-length', str(len(map['raw'])))]) |
|
260 yield '' |
|
261 |
|
262 def footer(**map): |
|
263 yield tmpl("footer", **map) |
|
264 |
|
265 def motd(**map): |
|
266 yield self.config("web", "motd", "") |
|
267 |
|
268 def sessionvars(**map): |
|
269 fields = [] |
|
270 if req.form.has_key('style'): |
|
271 style = req.form['style'][0] |
|
272 if style != self.config('web', 'style', ''): |
|
273 fields.append(('style', style)) |
|
274 |
|
275 separator = req.url[-1] == '?' and ';' or '?' |
|
276 for name, value in fields: |
|
277 yield dict(name=name, value=value, separator=separator) |
|
278 separator = ';' |
|
279 |
|
280 # figure out which style to use |
|
281 |
|
282 style = self.config("web", "style", "") |
|
283 if req.form.has_key('style'): |
|
284 style = req.form['style'][0] |
|
285 mapfile = style_map(self.templatepath, style) |
|
286 |
|
287 if not self.reponame: |
|
288 self.reponame = (self.config("web", "name") |
|
289 or req.env.get('REPO_NAME') |
|
290 or req.url.strip('/') or self.repo.root) |
|
291 |
|
292 # create the templater |
|
293 |
|
294 tmpl = templater.templater(mapfile, templater.common_filters, |
|
295 defaults={"url": req.url, |
|
296 "staticurl": staticurl, |
|
297 "urlbase": urlbase, |
|
298 "repo": self.reponame, |
|
299 "header": header, |
|
300 "footer": footer, |
|
301 "motd": motd, |
|
302 "rawfileheader": rawfileheader, |
|
303 "sessionvars": sessionvars |
|
304 }) |
|
305 return tmpl |
|
306 |
110 def archivelist(self, nodeid): |
307 def archivelist(self, nodeid): |
111 allowed = self.configlist("web", "allow_archive") |
308 allowed = self.configlist("web", "allow_archive") |
112 for i, spec in self.archive_specs.iteritems(): |
309 for i, spec in self.archive_specs.iteritems(): |
113 if i in allowed or self.configbool("web", "allow" + i): |
310 if i in allowed or self.configbool("web", "allow" + i): |
114 yield {"type" : i, "extension" : spec[2], "node" : nodeid} |
311 yield {"type" : i, "extension" : spec[2], "node" : nodeid} |
115 |
312 |
116 def listfilediffs(self, files, changeset): |
313 def listfilediffs(self, tmpl, files, changeset): |
117 for f in files[:self.maxfiles]: |
314 for f in files[:self.maxfiles]: |
118 yield self.t("filedifflink", node=hex(changeset), file=f) |
315 yield tmpl("filedifflink", node=hex(changeset), file=f) |
119 if len(files) > self.maxfiles: |
316 if len(files) > self.maxfiles: |
120 yield self.t("fileellipses") |
317 yield tmpl("fileellipses") |
121 |
318 |
122 def siblings(self, siblings=[], hiderev=None, **args): |
319 def siblings(self, siblings=[], hiderev=None, **args): |
123 siblings = [s for s in siblings if s.node() != nullid] |
320 siblings = [s for s in siblings if s.node() != nullid] |
124 if len(siblings) == 1 and siblings[0].rev() == hiderev: |
321 if len(siblings) == 1 and siblings[0].rev() == hiderev: |
125 return |
322 return |
163 l += [x for x in files if x.startswith(t)] |
360 l += [x for x in files if x.startswith(t)] |
164 return l |
361 return l |
165 |
362 |
166 parity = paritygen(self.stripecount) |
363 parity = paritygen(self.stripecount) |
167 def diffblock(diff, f, fn): |
364 def diffblock(diff, f, fn): |
168 yield self.t("diffblock", |
365 yield tmpl("diffblock", |
169 lines=prettyprintlines(diff), |
366 lines=prettyprintlines(diff), |
170 parity=parity.next(), |
367 parity=parity.next(), |
171 file=f, |
368 file=f, |
172 filenode=hex(fn or nullid)) |
369 filenode=hex(fn or nullid)) |
173 |
370 |
174 def prettyprintlines(diff): |
371 def prettyprintlines(diff): |
175 for l in diff.splitlines(1): |
372 for l in diff.splitlines(1): |
176 if l.startswith('+'): |
373 if l.startswith('+'): |
177 yield self.t("difflineplus", line=l) |
374 yield tmpl("difflineplus", line=l) |
178 elif l.startswith('-'): |
375 elif l.startswith('-'): |
179 yield self.t("difflineminus", line=l) |
376 yield tmpl("difflineminus", line=l) |
180 elif l.startswith('@'): |
377 elif l.startswith('@'): |
181 yield self.t("difflineat", line=l) |
378 yield tmpl("difflineat", line=l) |
182 else: |
379 else: |
183 yield self.t("diffline", line=l) |
380 yield tmpl("diffline", line=l) |
184 |
381 |
185 r = self.repo |
382 r = self.repo |
186 c1 = r.changectx(node1) |
383 c1 = r.changectx(node1) |
187 c2 = r.changectx(node2) |
384 c2 = r.changectx(node2) |
188 date1 = util.datestr(c1.date()) |
385 date1 = util.datestr(c1.date()) |
285 continue |
482 continue |
286 |
483 |
287 count += 1 |
484 count += 1 |
288 n = ctx.node() |
485 n = ctx.node() |
289 |
486 |
290 yield self.t('searchentry', |
487 yield tmpl('searchentry', |
291 parity=parity.next(), |
488 parity=parity.next(), |
292 author=ctx.user(), |
489 author=ctx.user(), |
293 parent=self.siblings(ctx.parents()), |
490 parent=self.siblings(ctx.parents()), |
294 child=self.siblings(ctx.children()), |
491 child=self.siblings(ctx.children()), |
295 changelogtag=self.showtag("changelogtag",n), |
492 changelogtag=self.showtag("changelogtag",n), |
296 desc=ctx.description(), |
493 desc=ctx.description(), |
297 date=ctx.date(), |
494 date=ctx.date(), |
298 files=self.listfilediffs(ctx.files(), n), |
495 files=self.listfilediffs(tmpl, ctx.files(), n), |
299 rev=ctx.rev(), |
496 rev=ctx.rev(), |
300 node=hex(n), |
497 node=hex(n), |
301 tags=self.nodetagsdict(n), |
498 tags=self.nodetagsdict(n), |
302 branches=self.nodebranchdict(ctx)) |
499 branches=self.nodebranchdict(ctx)) |
303 |
500 |
304 if count >= self.maxchanges: |
501 if count >= self.maxchanges: |
305 break |
502 break |
306 |
503 |
307 cl = self.repo.changelog |
504 cl = self.repo.changelog |
308 parity = paritygen(self.stripecount) |
505 parity = paritygen(self.stripecount) |
309 |
506 |
310 yield self.t('search', |
507 yield tmpl('search', |
311 query=query, |
508 query=query, |
312 node=hex(cl.tip()), |
509 node=hex(cl.tip()), |
313 entries=changelist, |
510 entries=changelist, |
314 archives=self.archivelist("tip")) |
511 archives=self.archivelist("tip")) |
315 |
512 |
316 def changeset(self, ctx): |
513 def changeset(self, tmpl, ctx): |
317 n = ctx.node() |
514 n = ctx.node() |
318 parents = ctx.parents() |
515 parents = ctx.parents() |
319 p1 = parents[0].node() |
516 p1 = parents[0].node() |
320 |
517 |
321 files = [] |
518 files = [] |
322 parity = paritygen(self.stripecount) |
519 parity = paritygen(self.stripecount) |
323 for f in ctx.files(): |
520 for f in ctx.files(): |
324 files.append(self.t("filenodelink", |
521 files.append(tmpl("filenodelink", |
325 node=hex(n), file=f, |
522 node=hex(n), file=f, |
326 parity=parity.next())) |
523 parity=parity.next())) |
327 |
524 |
328 def diff(**map): |
525 def diff(**map): |
329 yield self.diff(p1, n, None) |
526 yield self.diff(tmpl, p1, n, None) |
330 |
527 |
331 yield self.t('changeset', |
528 yield tmpl('changeset', |
332 diff=diff, |
529 diff=diff, |
333 rev=ctx.rev(), |
530 rev=ctx.rev(), |
334 node=hex(n), |
531 node=hex(n), |
335 parent=self.siblings(parents), |
532 parent=self.siblings(parents), |
336 child=self.siblings(ctx.children()), |
533 child=self.siblings(ctx.children()), |
337 changesettag=self.showtag("changesettag",n), |
534 changesettag=self.showtag("changesettag",n), |
338 author=ctx.user(), |
535 author=ctx.user(), |
339 desc=ctx.description(), |
536 desc=ctx.description(), |
340 date=ctx.date(), |
537 date=ctx.date(), |
341 files=files, |
538 files=files, |
342 archives=self.archivelist(hex(n)), |
539 archives=self.archivelist(hex(n)), |
343 tags=self.nodetagsdict(n), |
540 tags=self.nodetagsdict(n), |
344 branches=self.nodebranchdict(ctx)) |
541 branches=self.nodebranchdict(ctx)) |
345 |
542 |
346 def filelog(self, fctx): |
543 def filelog(self, tmpl, fctx): |
347 f = fctx.path() |
544 f = fctx.path() |
348 fl = fctx.filelog() |
545 fl = fctx.filelog() |
349 count = fl.count() |
546 count = fl.count() |
350 pagelen = self.maxshortchanges |
547 pagelen = self.maxshortchanges |
351 pos = fctx.filerev() |
548 pos = fctx.filerev() |
607 cl = self.repo.changelog |
804 cl = self.repo.changelog |
608 count = cl.count() |
805 count = cl.count() |
609 start = max(0, count - self.maxchanges) |
806 start = max(0, count - self.maxchanges) |
610 end = min(count, start + self.maxchanges) |
807 end = min(count, start + self.maxchanges) |
611 |
808 |
612 yield self.t("summary", |
809 yield tmpl("summary", |
613 desc=self.config("web", "description", "unknown"), |
810 desc=self.config("web", "description", "unknown"), |
614 owner=(self.config("ui", "username") or # preferred |
811 owner=(self.config("ui", "username") or # preferred |
615 self.config("web", "contact") or # deprecated |
812 self.config("web", "contact") or # deprecated |
616 self.config("web", "author", "unknown")), # also |
813 self.config("web", "author", "unknown")), # also |
617 lastchange=cl.read(cl.tip())[2], |
814 lastchange=cl.read(cl.tip())[2], |
618 tags=tagentries, |
815 tags=tagentries, |
619 branches=branches, |
816 branches=branches, |
620 shortlog=changelist, |
817 shortlog=changelist, |
621 node=hex(cl.tip()), |
818 node=hex(cl.tip()), |
622 archives=self.archivelist("tip")) |
819 archives=self.archivelist("tip")) |
623 |
820 |
624 def filediff(self, fctx): |
821 def filediff(self, tmpl, fctx): |
625 n = fctx.node() |
822 n = fctx.node() |
626 path = fctx.path() |
823 path = fctx.path() |
627 parents = fctx.parents() |
824 parents = fctx.parents() |
628 p1 = parents and parents[0].node() or nullid |
825 p1 = parents and parents[0].node() or nullid |
629 |
826 |
630 def diff(**map): |
827 def diff(**map): |
631 yield self.diff(p1, n, [path]) |
828 yield self.diff(tmpl, p1, n, [path]) |
632 |
829 |
633 yield self.t("filediff", |
830 yield tmpl("filediff", |
634 file=path, |
831 file=path, |
635 node=hex(n), |
832 node=hex(n), |
636 rev=fctx.rev(), |
833 rev=fctx.rev(), |
637 parent=self.siblings(parents), |
834 parent=self.siblings(parents), |
638 child=self.siblings(fctx.children()), |
835 child=self.siblings(fctx.children()), |
639 diff=diff) |
836 diff=diff) |
640 |
837 |
641 archive_specs = { |
838 archive_specs = { |
642 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None), |
839 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None), |
643 'gz': ('application/x-tar', 'tgz', '.tar.gz', None), |
840 'gz': ('application/x-tar', 'tgz', '.tar.gz', None), |
644 'zip': ('application/zip', 'zip', '.zip', None), |
841 'zip': ('application/zip', 'zip', '.zip', None), |
645 } |
842 } |
646 |
843 |
647 def archive(self, req, key, type_): |
844 def archive(self, tmpl, req, key, type_): |
648 reponame = re.sub(r"\W+", "-", os.path.basename(self.reponame)) |
845 reponame = re.sub(r"\W+", "-", os.path.basename(self.reponame)) |
649 cnode = self.repo.lookup(key) |
846 cnode = self.repo.lookup(key) |
650 arch_version = key |
847 arch_version = key |
651 if cnode == key or key == 'tip': |
848 if cnode == key or key == 'tip': |
652 arch_version = short(cnode) |
849 arch_version = short(cnode) |
666 |
863 |
667 def cleanpath(self, path): |
864 def cleanpath(self, path): |
668 path = path.lstrip('/') |
865 path = path.lstrip('/') |
669 return util.canonpath(self.repo.root, '', path) |
866 return util.canonpath(self.repo.root, '', path) |
670 |
867 |
671 def run(self): |
|
672 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."): |
|
673 raise RuntimeError("This function is only intended to be called while running as a CGI script.") |
|
674 import mercurial.hgweb.wsgicgi as wsgicgi |
|
675 wsgicgi.launch(self) |
|
676 |
|
677 def __call__(self, env, respond): |
|
678 req = wsgirequest(env, respond) |
|
679 self.run_wsgi(req) |
|
680 return req |
|
681 |
|
682 def run_wsgi(self, req): |
|
683 def header(**map): |
|
684 header_file = cStringIO.StringIO( |
|
685 ''.join(self.t("header", encoding=self.encoding, **map))) |
|
686 msg = mimetools.Message(header_file, 0) |
|
687 req.header(msg.items()) |
|
688 yield header_file.read() |
|
689 |
|
690 def rawfileheader(**map): |
|
691 req.header([('Content-type', map['mimetype']), |
|
692 ('Content-disposition', 'filename=%s' % map['file']), |
|
693 ('Content-length', str(len(map['raw'])))]) |
|
694 yield '' |
|
695 |
|
696 def footer(**map): |
|
697 yield self.t("footer", **map) |
|
698 |
|
699 def motd(**map): |
|
700 yield self.config("web", "motd", "") |
|
701 |
|
702 def expand_form(form): |
|
703 shortcuts = { |
|
704 'cl': [('cmd', ['changelog']), ('rev', None)], |
|
705 'sl': [('cmd', ['shortlog']), ('rev', None)], |
|
706 'cs': [('cmd', ['changeset']), ('node', None)], |
|
707 'f': [('cmd', ['file']), ('filenode', None)], |
|
708 'fl': [('cmd', ['filelog']), ('filenode', None)], |
|
709 'fd': [('cmd', ['filediff']), ('node', None)], |
|
710 'fa': [('cmd', ['annotate']), ('filenode', None)], |
|
711 'mf': [('cmd', ['manifest']), ('manifest', None)], |
|
712 'ca': [('cmd', ['archive']), ('node', None)], |
|
713 'tags': [('cmd', ['tags'])], |
|
714 'tip': [('cmd', ['changeset']), ('node', ['tip'])], |
|
715 'static': [('cmd', ['static']), ('file', None)] |
|
716 } |
|
717 |
|
718 for k in shortcuts.iterkeys(): |
|
719 if form.has_key(k): |
|
720 for name, value in shortcuts[k]: |
|
721 if value is None: |
|
722 value = form[k] |
|
723 form[name] = value |
|
724 del form[k] |
|
725 |
|
726 def rewrite_request(req): |
|
727 '''translate new web interface to traditional format''' |
|
728 |
|
729 req.url = req.env['SCRIPT_NAME'] |
|
730 if not req.url.endswith('/'): |
|
731 req.url += '/' |
|
732 if req.env.has_key('REPO_NAME'): |
|
733 req.url += req.env['REPO_NAME'] + '/' |
|
734 |
|
735 if req.env.get('PATH_INFO'): |
|
736 parts = req.env.get('PATH_INFO').strip('/').split('/') |
|
737 repo_parts = req.env.get('REPO_NAME', '').split('/') |
|
738 if parts[:len(repo_parts)] == repo_parts: |
|
739 parts = parts[len(repo_parts):] |
|
740 query = '/'.join(parts) |
|
741 else: |
|
742 query = req.env['QUERY_STRING'].split('&', 1)[0] |
|
743 query = query.split(';', 1)[0] |
|
744 |
|
745 if req.form.has_key('cmd'): |
|
746 # old style |
|
747 return |
|
748 |
|
749 args = query.split('/', 2) |
|
750 if not args or not args[0]: |
|
751 return |
|
752 |
|
753 cmd = args.pop(0) |
|
754 style = cmd.rfind('-') |
|
755 if style != -1: |
|
756 req.form['style'] = [cmd[:style]] |
|
757 cmd = cmd[style+1:] |
|
758 # avoid accepting e.g. style parameter as command |
|
759 if hasattr(self, 'do_' + cmd): |
|
760 req.form['cmd'] = [cmd] |
|
761 |
|
762 if args and args[0]: |
|
763 node = args.pop(0) |
|
764 req.form['node'] = [node] |
|
765 if args: |
|
766 req.form['file'] = args |
|
767 |
|
768 if cmd == 'static': |
|
769 req.form['file'] = req.form['node'] |
|
770 elif cmd == 'archive': |
|
771 fn = req.form['node'][0] |
|
772 for type_, spec in self.archive_specs.iteritems(): |
|
773 ext = spec[2] |
|
774 if fn.endswith(ext): |
|
775 req.form['node'] = [fn[:-len(ext)]] |
|
776 req.form['type'] = [type_] |
|
777 |
|
778 def sessionvars(**map): |
|
779 fields = [] |
|
780 if req.form.has_key('style'): |
|
781 style = req.form['style'][0] |
|
782 if style != self.config('web', 'style', ''): |
|
783 fields.append(('style', style)) |
|
784 |
|
785 separator = req.url[-1] == '?' and ';' or '?' |
|
786 for name, value in fields: |
|
787 yield dict(name=name, value=value, separator=separator) |
|
788 separator = ';' |
|
789 |
|
790 self.refresh() |
|
791 |
|
792 expand_form(req.form) |
|
793 rewrite_request(req) |
|
794 |
|
795 style = self.config("web", "style", "") |
|
796 if req.form.has_key('style'): |
|
797 style = req.form['style'][0] |
|
798 mapfile = style_map(self.templatepath, style) |
|
799 |
|
800 proto = req.env.get('wsgi.url_scheme') |
|
801 if proto == 'https': |
|
802 proto = 'https' |
|
803 default_port = "443" |
|
804 else: |
|
805 proto = 'http' |
|
806 default_port = "80" |
|
807 |
|
808 port = req.env["SERVER_PORT"] |
|
809 port = port != default_port and (":" + port) or "" |
|
810 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port) |
|
811 staticurl = self.config("web", "staticurl") or req.url + 'static/' |
|
812 if not staticurl.endswith('/'): |
|
813 staticurl += '/' |
|
814 |
|
815 if not self.reponame: |
|
816 self.reponame = (self.config("web", "name") |
|
817 or req.env.get('REPO_NAME') |
|
818 or req.url.strip('/') |
|
819 or os.path.basename(self.repo.root)) |
|
820 |
|
821 self.t = templater.templater(mapfile, templater.common_filters, |
|
822 defaults={"url": req.url, |
|
823 "staticurl": staticurl, |
|
824 "urlbase": urlbase, |
|
825 "repo": self.reponame, |
|
826 "header": header, |
|
827 "footer": footer, |
|
828 "motd": motd, |
|
829 "rawfileheader": rawfileheader, |
|
830 "sessionvars": sessionvars |
|
831 }) |
|
832 |
|
833 try: |
|
834 if not req.form.has_key('cmd'): |
|
835 req.form['cmd'] = [self.t.cache['default']] |
|
836 |
|
837 cmd = req.form['cmd'][0] |
|
838 |
|
839 try: |
|
840 method = getattr(self, 'do_' + cmd) |
|
841 method(req) |
|
842 except revlog.LookupError, err: |
|
843 req.respond(404, self.t( |
|
844 'error', error='revision not found: %s' % err.name)) |
|
845 except (hg.RepoError, revlog.RevlogError), inst: |
|
846 req.respond('500 Internal Server Error', |
|
847 self.t('error', error=str(inst))) |
|
848 except ErrorResponse, inst: |
|
849 req.respond(inst.code, self.t('error', error=inst.message)) |
|
850 except AttributeError: |
|
851 req.respond(400, |
|
852 self.t('error', error='No such method: ' + cmd)) |
|
853 finally: |
|
854 self.t = None |
|
855 |
|
856 def changectx(self, req): |
868 def changectx(self, req): |
857 if req.form.has_key('node'): |
869 if req.form.has_key('node'): |
858 changeid = req.form['node'][0] |
870 changeid = req.form['node'][0] |
859 elif req.form.has_key('manifest'): |
871 elif req.form.has_key('manifest'): |
860 changeid = req.form['manifest'][0] |
872 changeid = req.form['manifest'][0] |
882 except hg.RepoError: |
894 except hg.RepoError: |
883 fctx = self.repo.filectx(path, fileid=changeid) |
895 fctx = self.repo.filectx(path, fileid=changeid) |
884 |
896 |
885 return fctx |
897 return fctx |
886 |
898 |
887 def do_log(self, req): |
|
888 if req.form.has_key('file') and req.form['file'][0]: |
|
889 self.do_filelog(req) |
|
890 else: |
|
891 self.do_changelog(req) |
|
892 |
|
893 def do_rev(self, req): |
|
894 self.do_changeset(req) |
|
895 |
|
896 def do_file(self, req): |
|
897 path = self.cleanpath(req.form.get('file', [''])[0]) |
|
898 if path: |
|
899 try: |
|
900 req.write(self.filerevision(self.filectx(req))) |
|
901 return |
|
902 except revlog.LookupError: |
|
903 pass |
|
904 |
|
905 req.write(self.manifest(self.changectx(req), path)) |
|
906 |
|
907 def do_diff(self, req): |
|
908 self.do_filediff(req) |
|
909 |
|
910 def do_changelog(self, req, shortlog = False): |
|
911 if req.form.has_key('node'): |
|
912 ctx = self.changectx(req) |
|
913 else: |
|
914 if req.form.has_key('rev'): |
|
915 hi = req.form['rev'][0] |
|
916 else: |
|
917 hi = self.repo.changelog.count() - 1 |
|
918 try: |
|
919 ctx = self.repo.changectx(hi) |
|
920 except hg.RepoError: |
|
921 req.write(self.search(hi)) # XXX redirect to 404 page? |
|
922 return |
|
923 |
|
924 req.write(self.changelog(ctx, shortlog = shortlog)) |
|
925 |
|
926 def do_shortlog(self, req): |
|
927 self.do_changelog(req, shortlog = True) |
|
928 |
|
929 def do_changeset(self, req): |
|
930 req.write(self.changeset(self.changectx(req))) |
|
931 |
|
932 def do_manifest(self, req): |
|
933 req.write(self.manifest(self.changectx(req), |
|
934 self.cleanpath(req.form['path'][0]))) |
|
935 |
|
936 def do_tags(self, req): |
|
937 req.write(self.tags()) |
|
938 |
|
939 def do_summary(self, req): |
|
940 req.write(self.summary()) |
|
941 |
|
942 def do_filediff(self, req): |
|
943 req.write(self.filediff(self.filectx(req))) |
|
944 |
|
945 def do_annotate(self, req): |
|
946 req.write(self.fileannotate(self.filectx(req))) |
|
947 |
|
948 def do_filelog(self, req): |
|
949 req.write(self.filelog(self.filectx(req))) |
|
950 |
|
951 def do_lookup(self, req): |
|
952 try: |
|
953 r = hex(self.repo.lookup(req.form['key'][0])) |
|
954 success = 1 |
|
955 except Exception,inst: |
|
956 r = str(inst) |
|
957 success = 0 |
|
958 resp = "%s %s\n" % (success, r) |
|
959 req.httphdr("application/mercurial-0.1", length=len(resp)) |
|
960 req.write(resp) |
|
961 |
|
962 def do_heads(self, req): |
|
963 resp = " ".join(map(hex, self.repo.heads())) + "\n" |
|
964 req.httphdr("application/mercurial-0.1", length=len(resp)) |
|
965 req.write(resp) |
|
966 |
|
967 def do_branches(self, req): |
|
968 nodes = [] |
|
969 if req.form.has_key('nodes'): |
|
970 nodes = map(bin, req.form['nodes'][0].split(" ")) |
|
971 resp = cStringIO.StringIO() |
|
972 for b in self.repo.branches(nodes): |
|
973 resp.write(" ".join(map(hex, b)) + "\n") |
|
974 resp = resp.getvalue() |
|
975 req.httphdr("application/mercurial-0.1", length=len(resp)) |
|
976 req.write(resp) |
|
977 |
|
978 def do_between(self, req): |
|
979 if req.form.has_key('pairs'): |
|
980 pairs = [map(bin, p.split("-")) |
|
981 for p in req.form['pairs'][0].split(" ")] |
|
982 resp = cStringIO.StringIO() |
|
983 for b in self.repo.between(pairs): |
|
984 resp.write(" ".join(map(hex, b)) + "\n") |
|
985 resp = resp.getvalue() |
|
986 req.httphdr("application/mercurial-0.1", length=len(resp)) |
|
987 req.write(resp) |
|
988 |
|
989 def do_changegroup(self, req): |
|
990 req.httphdr("application/mercurial-0.1") |
|
991 nodes = [] |
|
992 if not self.allowpull: |
|
993 return |
|
994 |
|
995 if req.form.has_key('roots'): |
|
996 nodes = map(bin, req.form['roots'][0].split(" ")) |
|
997 |
|
998 z = zlib.compressobj() |
|
999 f = self.repo.changegroup(nodes, 'serve') |
|
1000 while 1: |
|
1001 chunk = f.read(4096) |
|
1002 if not chunk: |
|
1003 break |
|
1004 req.write(z.compress(chunk)) |
|
1005 |
|
1006 req.write(z.flush()) |
|
1007 |
|
1008 def do_changegroupsubset(self, req): |
|
1009 req.httphdr("application/mercurial-0.1") |
|
1010 bases = [] |
|
1011 heads = [] |
|
1012 if not self.allowpull: |
|
1013 return |
|
1014 |
|
1015 if req.form.has_key('bases'): |
|
1016 bases = [bin(x) for x in req.form['bases'][0].split(' ')] |
|
1017 if req.form.has_key('heads'): |
|
1018 heads = [bin(x) for x in req.form['heads'][0].split(' ')] |
|
1019 |
|
1020 z = zlib.compressobj() |
|
1021 f = self.repo.changegroupsubset(bases, heads, 'serve') |
|
1022 while 1: |
|
1023 chunk = f.read(4096) |
|
1024 if not chunk: |
|
1025 break |
|
1026 req.write(z.compress(chunk)) |
|
1027 |
|
1028 req.write(z.flush()) |
|
1029 |
|
1030 def do_archive(self, req): |
|
1031 type_ = req.form['type'][0] |
|
1032 allowed = self.configlist("web", "allow_archive") |
|
1033 if (type_ in self.archives and (type_ in allowed or |
|
1034 self.configbool("web", "allow" + type_, False))): |
|
1035 self.archive(req, req.form['node'][0], type_) |
|
1036 return |
|
1037 |
|
1038 req.respond(400, self.t('error', |
|
1039 error='Unsupported archive type: %s' % type_)) |
|
1040 |
|
1041 def do_static(self, req): |
|
1042 fname = req.form['file'][0] |
|
1043 # a repo owner may set web.static in .hg/hgrc to get any file |
|
1044 # readable by the user running the CGI script |
|
1045 static = self.config("web", "static", |
|
1046 os.path.join(self.templatepath, "static"), |
|
1047 untrusted=False) |
|
1048 req.write(staticfile(static, fname, req)) |
|
1049 |
|
1050 def do_capabilities(self, req): |
|
1051 caps = ['lookup', 'changegroupsubset'] |
|
1052 if self.configbool('server', 'uncompressed'): |
|
1053 caps.append('stream=%d' % self.repo.changelog.version) |
|
1054 # XXX: make configurable and/or share code with do_unbundle: |
|
1055 unbundleversions = ['HG10GZ', 'HG10BZ', 'HG10UN'] |
|
1056 if unbundleversions: |
|
1057 caps.append('unbundle=%s' % ','.join(unbundleversions)) |
|
1058 resp = ' '.join(caps) |
|
1059 req.httphdr("application/mercurial-0.1", length=len(resp)) |
|
1060 req.write(resp) |
|
1061 |
|
1062 def check_perm(self, req, op, default): |
899 def check_perm(self, req, op, default): |
1063 '''check permission for operation based on user auth. |
900 '''check permission for operation based on user auth. |
1064 return true if op allowed, else false. |
901 return true if op allowed, else false. |
1065 default is policy to use if no config given.''' |
902 default is policy to use if no config given.''' |
1066 |
903 |
1070 if deny and (not user or deny == ['*'] or user in deny): |
907 if deny and (not user or deny == ['*'] or user in deny): |
1071 return False |
908 return False |
1072 |
909 |
1073 allow = self.configlist('web', 'allow_' + op) |
910 allow = self.configlist('web', 'allow_' + op) |
1074 return (allow and (allow == ['*'] or user in allow)) or default |
911 return (allow and (allow == ['*'] or user in allow)) or default |
1075 |
|
1076 def do_unbundle(self, req): |
|
1077 def bail(response, headers={}): |
|
1078 length = int(req.env['CONTENT_LENGTH']) |
|
1079 for s in util.filechunkiter(req, limit=length): |
|
1080 # drain incoming bundle, else client will not see |
|
1081 # response when run outside cgi script |
|
1082 pass |
|
1083 req.httphdr("application/mercurial-0.1", headers=headers) |
|
1084 req.write('0\n') |
|
1085 req.write(response) |
|
1086 |
|
1087 # require ssl by default, auth info cannot be sniffed and |
|
1088 # replayed |
|
1089 ssl_req = self.configbool('web', 'push_ssl', True) |
|
1090 if ssl_req: |
|
1091 if req.env.get('wsgi.url_scheme') != 'https': |
|
1092 bail(_('ssl required\n')) |
|
1093 return |
|
1094 proto = 'https' |
|
1095 else: |
|
1096 proto = 'http' |
|
1097 |
|
1098 # do not allow push unless explicitly allowed |
|
1099 if not self.check_perm(req, 'push', False): |
|
1100 bail(_('push not authorized\n'), |
|
1101 headers={'status': '401 Unauthorized'}) |
|
1102 return |
|
1103 |
|
1104 their_heads = req.form['heads'][0].split(' ') |
|
1105 |
|
1106 def check_heads(): |
|
1107 heads = map(hex, self.repo.heads()) |
|
1108 return their_heads == [hex('force')] or their_heads == heads |
|
1109 |
|
1110 # fail early if possible |
|
1111 if not check_heads(): |
|
1112 bail(_('unsynced changes\n')) |
|
1113 return |
|
1114 |
|
1115 req.httphdr("application/mercurial-0.1") |
|
1116 |
|
1117 # do not lock repo until all changegroup data is |
|
1118 # streamed. save to temporary file. |
|
1119 |
|
1120 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-') |
|
1121 fp = os.fdopen(fd, 'wb+') |
|
1122 try: |
|
1123 length = int(req.env['CONTENT_LENGTH']) |
|
1124 for s in util.filechunkiter(req, limit=length): |
|
1125 fp.write(s) |
|
1126 |
|
1127 try: |
|
1128 lock = self.repo.lock() |
|
1129 try: |
|
1130 if not check_heads(): |
|
1131 req.write('0\n') |
|
1132 req.write(_('unsynced changes\n')) |
|
1133 return |
|
1134 |
|
1135 fp.seek(0) |
|
1136 header = fp.read(6) |
|
1137 if not header.startswith("HG"): |
|
1138 # old client with uncompressed bundle |
|
1139 def generator(f): |
|
1140 yield header |
|
1141 for chunk in f: |
|
1142 yield chunk |
|
1143 elif not header.startswith("HG10"): |
|
1144 req.write("0\n") |
|
1145 req.write(_("unknown bundle version\n")) |
|
1146 return |
|
1147 elif header == "HG10GZ": |
|
1148 def generator(f): |
|
1149 zd = zlib.decompressobj() |
|
1150 for chunk in f: |
|
1151 yield zd.decompress(chunk) |
|
1152 elif header == "HG10BZ": |
|
1153 def generator(f): |
|
1154 zd = bz2.BZ2Decompressor() |
|
1155 zd.decompress("BZ") |
|
1156 for chunk in f: |
|
1157 yield zd.decompress(chunk) |
|
1158 elif header == "HG10UN": |
|
1159 def generator(f): |
|
1160 for chunk in f: |
|
1161 yield chunk |
|
1162 else: |
|
1163 req.write("0\n") |
|
1164 req.write(_("unknown bundle compression type\n")) |
|
1165 return |
|
1166 gen = generator(util.filechunkiter(fp, 4096)) |
|
1167 |
|
1168 # send addchangegroup output to client |
|
1169 |
|
1170 old_stdout = sys.stdout |
|
1171 sys.stdout = cStringIO.StringIO() |
|
1172 |
|
1173 try: |
|
1174 url = 'remote:%s:%s' % (proto, |
|
1175 req.env.get('REMOTE_HOST', '')) |
|
1176 try: |
|
1177 ret = self.repo.addchangegroup( |
|
1178 util.chunkbuffer(gen), 'serve', url) |
|
1179 except util.Abort, inst: |
|
1180 sys.stdout.write("abort: %s\n" % inst) |
|
1181 ret = 0 |
|
1182 finally: |
|
1183 val = sys.stdout.getvalue() |
|
1184 sys.stdout = old_stdout |
|
1185 req.write('%d\n' % ret) |
|
1186 req.write(val) |
|
1187 finally: |
|
1188 del lock |
|
1189 except (OSError, IOError), inst: |
|
1190 req.write('0\n') |
|
1191 filename = getattr(inst, 'filename', '') |
|
1192 # Don't send our filesystem layout to the client |
|
1193 if filename.startswith(self.repo.root): |
|
1194 filename = filename[len(self.repo.root)+1:] |
|
1195 else: |
|
1196 filename = '' |
|
1197 error = getattr(inst, 'strerror', 'Unknown error') |
|
1198 if inst.errno == errno.ENOENT: |
|
1199 code = 404 |
|
1200 else: |
|
1201 code = 500 |
|
1202 req.respond(code, '%s: %s\n' % (error, filename)) |
|
1203 finally: |
|
1204 fp.close() |
|
1205 os.unlink(tempname) |
|
1206 |
|
1207 def do_stream_out(self, req): |
|
1208 req.httphdr("application/mercurial-0.1") |
|
1209 streamclone.stream_out(self.repo, req, untrusted=True) |
|