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) |
|