mercurial/hgweb/hgweb_mod.py
changeset 5591 08887121a652
parent 5579 e15f7db0f0ee
child 5595 b95b2525c6e8
equal deleted inserted replaced
5590:05451f6b5f07 5591:08887121a652
     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, sys, urllib, bz2
    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
    17 
    16 
    18 def _up(p):
    17 def _up(p):
    19     if p[0] != "/":
    18     if p[0] != "/":
    20         p = "/" + p
    19         p = "/" + p
    21     if p[-1] == "/":
    20     if p[-1] == "/":
   104             self.stripecount = int(self.config("web", "stripes", 1))
   103             self.stripecount = int(self.config("web", "stripes", 1))
   105             self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
   104             self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
   106             self.maxfiles = int(self.config("web", "maxfiles", 10))
   105             self.maxfiles = int(self.config("web", "maxfiles", 10))
   107             self.allowpull = self.configbool("web", "allowpull", True)
   106             self.allowpull = self.configbool("web", "allowpull", True)
   108             self.encoding = self.config("web", "encoding", util._encoding)
   107             self.encoding = self.config("web", "encoding", util._encoding)
       
   108 
       
   109     def run(self):
       
   110         if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
       
   111             raise RuntimeError("This function is only intended to be called while running as a CGI script.")
       
   112         import mercurial.hgweb.wsgicgi as wsgicgi
       
   113         wsgicgi.launch(self)
       
   114 
       
   115     def __call__(self, env, respond):
       
   116         req = wsgirequest(env, respond)
       
   117         self.run_wsgi(req)
       
   118         return req
       
   119 
       
   120     def run_wsgi(self, req):
       
   121         def header(**map):
       
   122             header_file = cStringIO.StringIO(
       
   123                 ''.join(self.t("header", encoding=self.encoding, **map)))
       
   124             msg = mimetools.Message(header_file, 0)
       
   125             req.header(msg.items())
       
   126             yield header_file.read()
       
   127 
       
   128         def rawfileheader(**map):
       
   129             req.header([('Content-type', map['mimetype']),
       
   130                         ('Content-disposition', 'filename=%s' % map['file']),
       
   131                         ('Content-length', str(len(map['raw'])))])
       
   132             yield ''
       
   133 
       
   134         def footer(**map):
       
   135             yield self.t("footer", **map)
       
   136 
       
   137         def motd(**map):
       
   138             yield self.config("web", "motd", "")
       
   139 
       
   140         def expand_form(form):
       
   141             shortcuts = {
       
   142                 'cl': [('cmd', ['changelog']), ('rev', None)],
       
   143                 'sl': [('cmd', ['shortlog']), ('rev', None)],
       
   144                 'cs': [('cmd', ['changeset']), ('node', None)],
       
   145                 'f': [('cmd', ['file']), ('filenode', None)],
       
   146                 'fl': [('cmd', ['filelog']), ('filenode', None)],
       
   147                 'fd': [('cmd', ['filediff']), ('node', None)],
       
   148                 'fa': [('cmd', ['annotate']), ('filenode', None)],
       
   149                 'mf': [('cmd', ['manifest']), ('manifest', None)],
       
   150                 'ca': [('cmd', ['archive']), ('node', None)],
       
   151                 'tags': [('cmd', ['tags'])],
       
   152                 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
       
   153                 'static': [('cmd', ['static']), ('file', None)]
       
   154             }
       
   155 
       
   156             for k in shortcuts.iterkeys():
       
   157                 if form.has_key(k):
       
   158                     for name, value in shortcuts[k]:
       
   159                         if value is None:
       
   160                             value = form[k]
       
   161                         form[name] = value
       
   162                     del form[k]
       
   163 
       
   164         def rewrite_request(req):
       
   165             '''translate new web interface to traditional format'''
       
   166 
       
   167             req.url = req.env['SCRIPT_NAME']
       
   168             if not req.url.endswith('/'):
       
   169                 req.url += '/'
       
   170             if req.env.has_key('REPO_NAME'):
       
   171                 req.url += req.env['REPO_NAME'] + '/'
       
   172             
       
   173             if req.env.get('PATH_INFO'):
       
   174                 parts = req.env.get('PATH_INFO').strip('/').split('/')
       
   175                 repo_parts = req.env.get('REPO_NAME', '').split('/')
       
   176                 if parts[:len(repo_parts)] == repo_parts:
       
   177                     parts = parts[len(repo_parts):]
       
   178                 query = '/'.join(parts)
       
   179             else:
       
   180                 query = req.env['QUERY_STRING'].split('&', 1)[0]
       
   181                 query = query.split(';', 1)[0]
       
   182 
       
   183             if req.form.has_key('cmd'):
       
   184                 # old style
       
   185                 return
       
   186 
       
   187             args = query.split('/', 2)
       
   188             if not args or not args[0]:
       
   189                 return
       
   190 
       
   191             cmd = args.pop(0)
       
   192             style = cmd.rfind('-')
       
   193             if style != -1:
       
   194                 req.form['style'] = [cmd[:style]]
       
   195                 cmd = cmd[style+1:]
       
   196             # avoid accepting e.g. style parameter as command
       
   197             if hasattr(webcommands, cmd):
       
   198                 req.form['cmd'] = [cmd]
       
   199 
       
   200             if args and args[0]:
       
   201                 node = args.pop(0)
       
   202                 req.form['node'] = [node]
       
   203             if args:
       
   204                 req.form['file'] = args
       
   205 
       
   206             if cmd == 'static':
       
   207                 req.form['file'] = req.form['node']
       
   208             elif cmd == 'archive':
       
   209                 fn = req.form['node'][0]
       
   210                 for type_, spec in self.archive_specs.iteritems():
       
   211                     ext = spec[2]
       
   212                     if fn.endswith(ext):
       
   213                         req.form['node'] = [fn[:-len(ext)]]
       
   214                         req.form['type'] = [type_]
       
   215 
       
   216         def sessionvars(**map):
       
   217             fields = []
       
   218             if req.form.has_key('style'):
       
   219                 style = req.form['style'][0]
       
   220                 if style != self.config('web', 'style', ''):
       
   221                     fields.append(('style', style))
       
   222 
       
   223             separator = req.url[-1] == '?' and ';' or '?'
       
   224             for name, value in fields:
       
   225                 yield dict(name=name, value=value, separator=separator)
       
   226                 separator = ';'
       
   227 
       
   228         self.refresh()
       
   229 
       
   230         expand_form(req.form)
       
   231         rewrite_request(req)
       
   232 
       
   233         style = self.config("web", "style", "")
       
   234         if req.form.has_key('style'):
       
   235             style = req.form['style'][0]
       
   236         mapfile = style_map(self.templatepath, style)
       
   237 
       
   238         proto = req.env.get('wsgi.url_scheme')
       
   239         if proto == 'https':
       
   240             proto = 'https'
       
   241             default_port = "443"
       
   242         else:
       
   243             proto = 'http'
       
   244             default_port = "80"
       
   245 
       
   246         port = req.env["SERVER_PORT"]
       
   247         port = port != default_port and (":" + port) or ""
       
   248         urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
       
   249         staticurl = self.config("web", "staticurl") or req.url + 'static/'
       
   250         if not staticurl.endswith('/'):
       
   251             staticurl += '/'
       
   252 
       
   253         if not self.reponame:
       
   254             self.reponame = (self.config("web", "name")
       
   255                              or req.env.get('REPO_NAME')
       
   256                              or req.url.strip('/') or self.repo.root)
       
   257 
       
   258         self.t = templater.templater(mapfile, templater.common_filters,
       
   259                                      defaults={"url": req.url,
       
   260                                                "staticurl": staticurl,
       
   261                                                "urlbase": urlbase,
       
   262                                                "repo": self.reponame,
       
   263                                                "header": header,
       
   264                                                "footer": footer,
       
   265                                                "motd": motd,
       
   266                                                "rawfileheader": rawfileheader,
       
   267                                                "sessionvars": sessionvars
       
   268                                                })
       
   269 
       
   270         try:
       
   271             if not req.form.has_key('cmd'):
       
   272                 req.form['cmd'] = [self.t.cache['default']]
       
   273 
       
   274             cmd = req.form['cmd'][0]
       
   275 
       
   276             try:
       
   277                 method = getattr(webcommands, cmd)
       
   278                 method(self, req)
       
   279             except revlog.LookupError, err:
       
   280                 req.respond(404, self.t(
       
   281                     'error', error='revision not found: %s' % err.name))
       
   282             except (hg.RepoError, revlog.RevlogError), inst:
       
   283                 req.respond('500 Internal Server Error',
       
   284                             self.t('error', error=str(inst)))
       
   285             except ErrorResponse, inst:
       
   286                 req.respond(inst.code, self.t('error', error=inst.message))
       
   287             except AttributeError:
       
   288                 req.respond(400,
       
   289                             self.t('error', error='No such method: ' + cmd))
       
   290         finally:
       
   291             self.t = None
   109 
   292 
   110     def archivelist(self, nodeid):
   293     def archivelist(self, nodeid):
   111         allowed = self.configlist("web", "allow_archive")
   294         allowed = self.configlist("web", "allow_archive")
   112         for i, spec in self.archive_specs.iteritems():
   295         for i, spec in self.archive_specs.iteritems():
   113             if i in allowed or self.configbool("web", "allow" + i):
   296             if i in allowed or self.configbool("web", "allow" + i):
   666 
   849 
   667     def cleanpath(self, path):
   850     def cleanpath(self, path):
   668         path = path.lstrip('/')
   851         path = path.lstrip('/')
   669         return util.canonpath(self.repo.root, '', path)
   852         return util.canonpath(self.repo.root, '', path)
   670 
   853 
   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('/') or self.repo.root)
       
   819 
       
   820         self.t = templater.templater(mapfile, templater.common_filters,
       
   821                                      defaults={"url": req.url,
       
   822                                                "staticurl": staticurl,
       
   823                                                "urlbase": urlbase,
       
   824                                                "repo": self.reponame,
       
   825                                                "header": header,
       
   826                                                "footer": footer,
       
   827                                                "motd": motd,
       
   828                                                "rawfileheader": rawfileheader,
       
   829                                                "sessionvars": sessionvars
       
   830                                                })
       
   831 
       
   832         try:
       
   833             if not req.form.has_key('cmd'):
       
   834                 req.form['cmd'] = [self.t.cache['default']]
       
   835 
       
   836             cmd = req.form['cmd'][0]
       
   837 
       
   838             try:
       
   839                 method = getattr(self, 'do_' + cmd)
       
   840                 method(req)
       
   841             except revlog.LookupError, err:
       
   842                 req.respond(404, self.t(
       
   843                     'error', error='revision not found: %s' % err.name))
       
   844             except (hg.RepoError, revlog.RevlogError), inst:
       
   845                 req.respond('500 Internal Server Error',
       
   846                             self.t('error', error=str(inst)))
       
   847             except ErrorResponse, inst:
       
   848                 req.respond(inst.code, self.t('error', error=inst.message))
       
   849             except AttributeError:
       
   850                 req.respond(400,
       
   851                             self.t('error', error='No such method: ' + cmd))
       
   852         finally:
       
   853             self.t = None
       
   854 
       
   855     def changectx(self, req):
   854     def changectx(self, req):
   856         if req.form.has_key('node'):
   855         if req.form.has_key('node'):
   857             changeid = req.form['node'][0]
   856             changeid = req.form['node'][0]
   858         elif req.form.has_key('manifest'):
   857         elif req.form.has_key('manifest'):
   859             changeid = req.form['manifest'][0]
   858             changeid = req.form['manifest'][0]
   881         except hg.RepoError:
   880         except hg.RepoError:
   882             fctx = self.repo.filectx(path, fileid=changeid)
   881             fctx = self.repo.filectx(path, fileid=changeid)
   883 
   882 
   884         return fctx
   883         return fctx
   885 
   884 
   886     def do_log(self, req):
       
   887         if req.form.has_key('file') and req.form['file'][0]:
       
   888             self.do_filelog(req)
       
   889         else:
       
   890             self.do_changelog(req)
       
   891 
       
   892     def do_rev(self, req):
       
   893         self.do_changeset(req)
       
   894 
       
   895     def do_file(self, req):
       
   896         path = self.cleanpath(req.form.get('file', [''])[0])
       
   897         if path:
       
   898             try:
       
   899                 req.write(self.filerevision(self.filectx(req)))
       
   900                 return
       
   901             except revlog.LookupError:
       
   902                 pass
       
   903 
       
   904         req.write(self.manifest(self.changectx(req), path))
       
   905 
       
   906     def do_diff(self, req):
       
   907         self.do_filediff(req)
       
   908 
       
   909     def do_changelog(self, req, shortlog = False):
       
   910         if req.form.has_key('node'):
       
   911             ctx = self.changectx(req)
       
   912         else:
       
   913             if req.form.has_key('rev'):
       
   914                 hi = req.form['rev'][0]
       
   915             else:
       
   916                 hi = self.repo.changelog.count() - 1
       
   917             try:
       
   918                 ctx = self.repo.changectx(hi)
       
   919             except hg.RepoError:
       
   920                 req.write(self.search(hi)) # XXX redirect to 404 page?
       
   921                 return
       
   922 
       
   923         req.write(self.changelog(ctx, shortlog = shortlog))
       
   924 
       
   925     def do_shortlog(self, req):
       
   926         self.do_changelog(req, shortlog = True)
       
   927 
       
   928     def do_changeset(self, req):
       
   929         req.write(self.changeset(self.changectx(req)))
       
   930 
       
   931     def do_manifest(self, req):
       
   932         req.write(self.manifest(self.changectx(req),
       
   933                                 self.cleanpath(req.form['path'][0])))
       
   934 
       
   935     def do_tags(self, req):
       
   936         req.write(self.tags())
       
   937 
       
   938     def do_summary(self, req):
       
   939         req.write(self.summary())
       
   940 
       
   941     def do_filediff(self, req):
       
   942         req.write(self.filediff(self.filectx(req)))
       
   943 
       
   944     def do_annotate(self, req):
       
   945         req.write(self.fileannotate(self.filectx(req)))
       
   946 
       
   947     def do_filelog(self, req):
       
   948         req.write(self.filelog(self.filectx(req)))
       
   949 
       
   950     def do_lookup(self, req):
       
   951         try:
       
   952             r = hex(self.repo.lookup(req.form['key'][0]))
       
   953             success = 1
       
   954         except Exception,inst:
       
   955             r = str(inst)
       
   956             success = 0
       
   957         resp = "%s %s\n" % (success, r)
       
   958         req.httphdr("application/mercurial-0.1", length=len(resp))
       
   959         req.write(resp)
       
   960 
       
   961     def do_heads(self, req):
       
   962         resp = " ".join(map(hex, self.repo.heads())) + "\n"
       
   963         req.httphdr("application/mercurial-0.1", length=len(resp))
       
   964         req.write(resp)
       
   965 
       
   966     def do_branches(self, req):
       
   967         nodes = []
       
   968         if req.form.has_key('nodes'):
       
   969             nodes = map(bin, req.form['nodes'][0].split(" "))
       
   970         resp = cStringIO.StringIO()
       
   971         for b in self.repo.branches(nodes):
       
   972             resp.write(" ".join(map(hex, b)) + "\n")
       
   973         resp = resp.getvalue()
       
   974         req.httphdr("application/mercurial-0.1", length=len(resp))
       
   975         req.write(resp)
       
   976 
       
   977     def do_between(self, req):
       
   978         if req.form.has_key('pairs'):
       
   979             pairs = [map(bin, p.split("-"))
       
   980                      for p in req.form['pairs'][0].split(" ")]
       
   981         resp = cStringIO.StringIO()
       
   982         for b in self.repo.between(pairs):
       
   983             resp.write(" ".join(map(hex, b)) + "\n")
       
   984         resp = resp.getvalue()
       
   985         req.httphdr("application/mercurial-0.1", length=len(resp))
       
   986         req.write(resp)
       
   987 
       
   988     def do_changegroup(self, req):
       
   989         req.httphdr("application/mercurial-0.1")
       
   990         nodes = []
       
   991         if not self.allowpull:
       
   992             return
       
   993 
       
   994         if req.form.has_key('roots'):
       
   995             nodes = map(bin, req.form['roots'][0].split(" "))
       
   996 
       
   997         z = zlib.compressobj()
       
   998         f = self.repo.changegroup(nodes, 'serve')
       
   999         while 1:
       
  1000             chunk = f.read(4096)
       
  1001             if not chunk:
       
  1002                 break
       
  1003             req.write(z.compress(chunk))
       
  1004 
       
  1005         req.write(z.flush())
       
  1006 
       
  1007     def do_changegroupsubset(self, req):
       
  1008         req.httphdr("application/mercurial-0.1")
       
  1009         bases = []
       
  1010         heads = []
       
  1011         if not self.allowpull:
       
  1012             return
       
  1013 
       
  1014         if req.form.has_key('bases'):
       
  1015             bases = [bin(x) for x in req.form['bases'][0].split(' ')]
       
  1016         if req.form.has_key('heads'):
       
  1017             heads = [bin(x) for x in req.form['heads'][0].split(' ')]
       
  1018 
       
  1019         z = zlib.compressobj()
       
  1020         f = self.repo.changegroupsubset(bases, heads, 'serve')
       
  1021         while 1:
       
  1022             chunk = f.read(4096)
       
  1023             if not chunk:
       
  1024                 break
       
  1025             req.write(z.compress(chunk))
       
  1026 
       
  1027         req.write(z.flush())
       
  1028 
       
  1029     def do_archive(self, req):
       
  1030         type_ = req.form['type'][0]
       
  1031         allowed = self.configlist("web", "allow_archive")
       
  1032         if (type_ in self.archives and (type_ in allowed or
       
  1033             self.configbool("web", "allow" + type_, False))):
       
  1034             self.archive(req, req.form['node'][0], type_)
       
  1035             return
       
  1036 
       
  1037         req.respond(400, self.t('error',
       
  1038                                 error='Unsupported archive type: %s' % type_))
       
  1039 
       
  1040     def do_static(self, req):
       
  1041         fname = req.form['file'][0]
       
  1042         # a repo owner may set web.static in .hg/hgrc to get any file
       
  1043         # readable by the user running the CGI script
       
  1044         static = self.config("web", "static",
       
  1045                              os.path.join(self.templatepath, "static"),
       
  1046                              untrusted=False)
       
  1047         req.write(staticfile(static, fname, req))
       
  1048 
       
  1049     def do_capabilities(self, req):
       
  1050         caps = ['lookup', 'changegroupsubset']
       
  1051         if self.configbool('server', 'uncompressed'):
       
  1052             caps.append('stream=%d' % self.repo.changelog.version)
       
  1053         # XXX: make configurable and/or share code with do_unbundle:
       
  1054         unbundleversions = ['HG10GZ', 'HG10BZ', 'HG10UN']
       
  1055         if unbundleversions:
       
  1056             caps.append('unbundle=%s' % ','.join(unbundleversions))
       
  1057         resp = ' '.join(caps)
       
  1058         req.httphdr("application/mercurial-0.1", length=len(resp))
       
  1059         req.write(resp)
       
  1060 
       
  1061     def check_perm(self, req, op, default):
   885     def check_perm(self, req, op, default):
  1062         '''check permission for operation based on user auth.
   886         '''check permission for operation based on user auth.
  1063         return true if op allowed, else false.
   887         return true if op allowed, else false.
  1064         default is policy to use if no config given.'''
   888         default is policy to use if no config given.'''
  1065 
   889 
  1069         if deny and (not user or deny == ['*'] or user in deny):
   893         if deny and (not user or deny == ['*'] or user in deny):
  1070             return False
   894             return False
  1071 
   895 
  1072         allow = self.configlist('web', 'allow_' + op)
   896         allow = self.configlist('web', 'allow_' + op)
  1073         return (allow and (allow == ['*'] or user in allow)) or default
   897         return (allow and (allow == ['*'] or user in allow)) or default
  1074 
       
  1075     def do_unbundle(self, req):
       
  1076         def bail(response, headers={}):
       
  1077             length = int(req.env['CONTENT_LENGTH'])
       
  1078             for s in util.filechunkiter(req, limit=length):
       
  1079                 # drain incoming bundle, else client will not see
       
  1080                 # response when run outside cgi script
       
  1081                 pass
       
  1082             req.httphdr("application/mercurial-0.1", headers=headers)
       
  1083             req.write('0\n')
       
  1084             req.write(response)
       
  1085 
       
  1086         # require ssl by default, auth info cannot be sniffed and
       
  1087         # replayed
       
  1088         ssl_req = self.configbool('web', 'push_ssl', True)
       
  1089         if ssl_req:
       
  1090             if req.env.get('wsgi.url_scheme') != 'https':
       
  1091                 bail(_('ssl required\n'))
       
  1092                 return
       
  1093             proto = 'https'
       
  1094         else:
       
  1095             proto = 'http'
       
  1096 
       
  1097         # do not allow push unless explicitly allowed
       
  1098         if not self.check_perm(req, 'push', False):
       
  1099             bail(_('push not authorized\n'),
       
  1100                  headers={'status': '401 Unauthorized'})
       
  1101             return
       
  1102 
       
  1103         their_heads = req.form['heads'][0].split(' ')
       
  1104 
       
  1105         def check_heads():
       
  1106             heads = map(hex, self.repo.heads())
       
  1107             return their_heads == [hex('force')] or their_heads == heads
       
  1108 
       
  1109         # fail early if possible
       
  1110         if not check_heads():
       
  1111             bail(_('unsynced changes\n'))
       
  1112             return
       
  1113 
       
  1114         req.httphdr("application/mercurial-0.1")
       
  1115 
       
  1116         # do not lock repo until all changegroup data is
       
  1117         # streamed. save to temporary file.
       
  1118 
       
  1119         fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
       
  1120         fp = os.fdopen(fd, 'wb+')
       
  1121         try:
       
  1122             length = int(req.env['CONTENT_LENGTH'])
       
  1123             for s in util.filechunkiter(req, limit=length):
       
  1124                 fp.write(s)
       
  1125 
       
  1126             try:
       
  1127                 lock = self.repo.lock()
       
  1128                 try:
       
  1129                     if not check_heads():
       
  1130                         req.write('0\n')
       
  1131                         req.write(_('unsynced changes\n'))
       
  1132                         return
       
  1133 
       
  1134                     fp.seek(0)
       
  1135                     header = fp.read(6)
       
  1136                     if not header.startswith("HG"):
       
  1137                         # old client with uncompressed bundle
       
  1138                         def generator(f):
       
  1139                             yield header
       
  1140                             for chunk in f:
       
  1141                                 yield chunk
       
  1142                     elif not header.startswith("HG10"):
       
  1143                         req.write("0\n")
       
  1144                         req.write(_("unknown bundle version\n"))
       
  1145                         return
       
  1146                     elif header == "HG10GZ":
       
  1147                         def generator(f):
       
  1148                             zd = zlib.decompressobj()
       
  1149                             for chunk in f:
       
  1150                                 yield zd.decompress(chunk)
       
  1151                     elif header == "HG10BZ":
       
  1152                         def generator(f):
       
  1153                             zd = bz2.BZ2Decompressor()
       
  1154                             zd.decompress("BZ")
       
  1155                             for chunk in f:
       
  1156                                 yield zd.decompress(chunk)
       
  1157                     elif header == "HG10UN":
       
  1158                         def generator(f):
       
  1159                             for chunk in f:
       
  1160                                 yield chunk
       
  1161                     else:
       
  1162                         req.write("0\n")
       
  1163                         req.write(_("unknown bundle compression type\n"))
       
  1164                         return
       
  1165                     gen = generator(util.filechunkiter(fp, 4096))
       
  1166 
       
  1167                     # send addchangegroup output to client
       
  1168 
       
  1169                     old_stdout = sys.stdout
       
  1170                     sys.stdout = cStringIO.StringIO()
       
  1171 
       
  1172                     try:
       
  1173                         url = 'remote:%s:%s' % (proto,
       
  1174                                                 req.env.get('REMOTE_HOST', ''))
       
  1175                         try:
       
  1176                             ret = self.repo.addchangegroup(
       
  1177                                         util.chunkbuffer(gen), 'serve', url)
       
  1178                         except util.Abort, inst:
       
  1179                             sys.stdout.write("abort: %s\n" % inst)
       
  1180                             ret = 0
       
  1181                     finally:
       
  1182                         val = sys.stdout.getvalue()
       
  1183                         sys.stdout = old_stdout
       
  1184                     req.write('%d\n' % ret)
       
  1185                     req.write(val)
       
  1186                 finally:
       
  1187                     del lock
       
  1188             except (OSError, IOError), inst:
       
  1189                 req.write('0\n')
       
  1190                 filename = getattr(inst, 'filename', '')
       
  1191                 # Don't send our filesystem layout to the client
       
  1192                 if filename.startswith(self.repo.root):
       
  1193                     filename = filename[len(self.repo.root)+1:]
       
  1194                 else:
       
  1195                     filename = ''
       
  1196                 error = getattr(inst, 'strerror', 'Unknown error')
       
  1197                 if inst.errno == errno.ENOENT:
       
  1198                     code = 404
       
  1199                 else:
       
  1200                     code = 500
       
  1201                 req.respond(code, '%s: %s\n' % (error, filename))
       
  1202         finally:
       
  1203             fp.close()
       
  1204             os.unlink(tempname)
       
  1205 
       
  1206     def do_stream_out(self, req):
       
  1207         req.httphdr("application/mercurial-0.1")
       
  1208         streamclone.stream_out(self.repo, req, untrusted=True)