##// END OF EJS Templates
hgweb: pass the actual response body to request.response, not just the length...
Mads Kiilerich -
r18352:e33b9b92 default
parent child Browse files
Show More
@@ -1,186 +1,186
1 # hgweb/common.py - Utility functions needed by hgweb_mod and hgwebdir_mod
1 # hgweb/common.py - Utility functions needed by hgweb_mod and hgwebdir_mod
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 import errno, mimetypes, os
9 import errno, mimetypes, os
10
10
11 HTTP_OK = 200
11 HTTP_OK = 200
12 HTTP_NOT_MODIFIED = 304
12 HTTP_NOT_MODIFIED = 304
13 HTTP_BAD_REQUEST = 400
13 HTTP_BAD_REQUEST = 400
14 HTTP_UNAUTHORIZED = 401
14 HTTP_UNAUTHORIZED = 401
15 HTTP_FORBIDDEN = 403
15 HTTP_FORBIDDEN = 403
16 HTTP_NOT_FOUND = 404
16 HTTP_NOT_FOUND = 404
17 HTTP_METHOD_NOT_ALLOWED = 405
17 HTTP_METHOD_NOT_ALLOWED = 405
18 HTTP_SERVER_ERROR = 500
18 HTTP_SERVER_ERROR = 500
19
19
20
20
21 def checkauthz(hgweb, req, op):
21 def checkauthz(hgweb, req, op):
22 '''Check permission for operation based on request data (including
22 '''Check permission for operation based on request data (including
23 authentication info). Return if op allowed, else raise an ErrorResponse
23 authentication info). Return if op allowed, else raise an ErrorResponse
24 exception.'''
24 exception.'''
25
25
26 user = req.env.get('REMOTE_USER')
26 user = req.env.get('REMOTE_USER')
27
27
28 deny_read = hgweb.configlist('web', 'deny_read')
28 deny_read = hgweb.configlist('web', 'deny_read')
29 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
29 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
30 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
30 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
31
31
32 allow_read = hgweb.configlist('web', 'allow_read')
32 allow_read = hgweb.configlist('web', 'allow_read')
33 result = (not allow_read) or (allow_read == ['*'])
33 result = (not allow_read) or (allow_read == ['*'])
34 if not (result or user in allow_read):
34 if not (result or user in allow_read):
35 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
35 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
36
36
37 if op == 'pull' and not hgweb.allowpull:
37 if op == 'pull' and not hgweb.allowpull:
38 raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized')
38 raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized')
39 elif op == 'pull' or op is None: # op is None for interface requests
39 elif op == 'pull' or op is None: # op is None for interface requests
40 return
40 return
41
41
42 # enforce that you can only push using POST requests
42 # enforce that you can only push using POST requests
43 if req.env['REQUEST_METHOD'] != 'POST':
43 if req.env['REQUEST_METHOD'] != 'POST':
44 msg = 'push requires POST request'
44 msg = 'push requires POST request'
45 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
45 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
46
46
47 # require ssl by default for pushing, auth info cannot be sniffed
47 # require ssl by default for pushing, auth info cannot be sniffed
48 # and replayed
48 # and replayed
49 scheme = req.env.get('wsgi.url_scheme')
49 scheme = req.env.get('wsgi.url_scheme')
50 if hgweb.configbool('web', 'push_ssl', True) and scheme != 'https':
50 if hgweb.configbool('web', 'push_ssl', True) and scheme != 'https':
51 raise ErrorResponse(HTTP_FORBIDDEN, 'ssl required')
51 raise ErrorResponse(HTTP_FORBIDDEN, 'ssl required')
52
52
53 deny = hgweb.configlist('web', 'deny_push')
53 deny = hgweb.configlist('web', 'deny_push')
54 if deny and (not user or deny == ['*'] or user in deny):
54 if deny and (not user or deny == ['*'] or user in deny):
55 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
55 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
56
56
57 allow = hgweb.configlist('web', 'allow_push')
57 allow = hgweb.configlist('web', 'allow_push')
58 result = allow and (allow == ['*'] or user in allow)
58 result = allow and (allow == ['*'] or user in allow)
59 if not result:
59 if not result:
60 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
60 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
61
61
62 # Hooks for hgweb permission checks; extensions can add hooks here.
62 # Hooks for hgweb permission checks; extensions can add hooks here.
63 # Each hook is invoked like this: hook(hgweb, request, operation),
63 # Each hook is invoked like this: hook(hgweb, request, operation),
64 # where operation is either read, pull or push. Hooks should either
64 # where operation is either read, pull or push. Hooks should either
65 # raise an ErrorResponse exception, or just return.
65 # raise an ErrorResponse exception, or just return.
66 #
66 #
67 # It is possible to do both authentication and authorization through
67 # It is possible to do both authentication and authorization through
68 # this.
68 # this.
69 permhooks = [checkauthz]
69 permhooks = [checkauthz]
70
70
71
71
72 class ErrorResponse(Exception):
72 class ErrorResponse(Exception):
73 def __init__(self, code, message=None, headers=[]):
73 def __init__(self, code, message=None, headers=[]):
74 if message is None:
74 if message is None:
75 message = _statusmessage(code)
75 message = _statusmessage(code)
76 Exception.__init__(self)
76 Exception.__init__(self)
77 self.code = code
77 self.code = code
78 self.message = message
78 self.message = message
79 self.headers = headers
79 self.headers = headers
80 def __str__(self):
80 def __str__(self):
81 return self.message
81 return self.message
82
82
83 class continuereader(object):
83 class continuereader(object):
84 def __init__(self, f, write):
84 def __init__(self, f, write):
85 self.f = f
85 self.f = f
86 self._write = write
86 self._write = write
87 self.continued = False
87 self.continued = False
88
88
89 def read(self, amt=-1):
89 def read(self, amt=-1):
90 if not self.continued:
90 if not self.continued:
91 self.continued = True
91 self.continued = True
92 self._write('HTTP/1.1 100 Continue\r\n\r\n')
92 self._write('HTTP/1.1 100 Continue\r\n\r\n')
93 return self.f.read(amt)
93 return self.f.read(amt)
94
94
95 def __getattr__(self, attr):
95 def __getattr__(self, attr):
96 if attr in ('close', 'readline', 'readlines', '__iter__'):
96 if attr in ('close', 'readline', 'readlines', '__iter__'):
97 return getattr(self.f, attr)
97 return getattr(self.f, attr)
98 raise AttributeError
98 raise AttributeError
99
99
100 def _statusmessage(code):
100 def _statusmessage(code):
101 from BaseHTTPServer import BaseHTTPRequestHandler
101 from BaseHTTPServer import BaseHTTPRequestHandler
102 responses = BaseHTTPRequestHandler.responses
102 responses = BaseHTTPRequestHandler.responses
103 return responses.get(code, ('Error', 'Unknown error'))[0]
103 return responses.get(code, ('Error', 'Unknown error'))[0]
104
104
105 def statusmessage(code, message=None):
105 def statusmessage(code, message=None):
106 return '%d %s' % (code, message or _statusmessage(code))
106 return '%d %s' % (code, message or _statusmessage(code))
107
107
108 def get_stat(spath):
108 def get_stat(spath):
109 """stat changelog if it exists, spath otherwise"""
109 """stat changelog if it exists, spath otherwise"""
110 cl_path = os.path.join(spath, "00changelog.i")
110 cl_path = os.path.join(spath, "00changelog.i")
111 if os.path.exists(cl_path):
111 if os.path.exists(cl_path):
112 return os.stat(cl_path)
112 return os.stat(cl_path)
113 else:
113 else:
114 return os.stat(spath)
114 return os.stat(spath)
115
115
116 def get_mtime(spath):
116 def get_mtime(spath):
117 return get_stat(spath).st_mtime
117 return get_stat(spath).st_mtime
118
118
119 def staticfile(directory, fname, req):
119 def staticfile(directory, fname, req):
120 """return a file inside directory with guessed Content-Type header
120 """return a file inside directory with guessed Content-Type header
121
121
122 fname always uses '/' as directory separator and isn't allowed to
122 fname always uses '/' as directory separator and isn't allowed to
123 contain unusual path components.
123 contain unusual path components.
124 Content-Type is guessed using the mimetypes module.
124 Content-Type is guessed using the mimetypes module.
125 Return an empty string if fname is illegal or file not found.
125 Return an empty string if fname is illegal or file not found.
126
126
127 """
127 """
128 parts = fname.split('/')
128 parts = fname.split('/')
129 for part in parts:
129 for part in parts:
130 if (part in ('', os.curdir, os.pardir) or
130 if (part in ('', os.curdir, os.pardir) or
131 os.sep in part or os.altsep is not None and os.altsep in part):
131 os.sep in part or os.altsep is not None and os.altsep in part):
132 return ""
132 return ""
133 fpath = os.path.join(*parts)
133 fpath = os.path.join(*parts)
134 if isinstance(directory, str):
134 if isinstance(directory, str):
135 directory = [directory]
135 directory = [directory]
136 for d in directory:
136 for d in directory:
137 path = os.path.join(d, fpath)
137 path = os.path.join(d, fpath)
138 if os.path.exists(path):
138 if os.path.exists(path):
139 break
139 break
140 try:
140 try:
141 os.stat(path)
141 os.stat(path)
142 ct = mimetypes.guess_type(path)[0] or "text/plain"
142 ct = mimetypes.guess_type(path)[0] or "text/plain"
143 req.respond(HTTP_OK, ct, length = os.path.getsize(path))
144 fp = open(path, 'rb')
143 fp = open(path, 'rb')
145 data = fp.read()
144 data = fp.read()
146 fp.close()
145 fp.close()
147 return data
146 req.respond(HTTP_OK, ct, body=data)
147 return ""
148 except TypeError:
148 except TypeError:
149 raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal filename')
149 raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal filename')
150 except OSError, err:
150 except OSError, err:
151 if err.errno == errno.ENOENT:
151 if err.errno == errno.ENOENT:
152 raise ErrorResponse(HTTP_NOT_FOUND)
152 raise ErrorResponse(HTTP_NOT_FOUND)
153 else:
153 else:
154 raise ErrorResponse(HTTP_SERVER_ERROR, err.strerror)
154 raise ErrorResponse(HTTP_SERVER_ERROR, err.strerror)
155
155
156 def paritygen(stripecount, offset=0):
156 def paritygen(stripecount, offset=0):
157 """count parity of horizontal stripes for easier reading"""
157 """count parity of horizontal stripes for easier reading"""
158 if stripecount and offset:
158 if stripecount and offset:
159 # account for offset, e.g. due to building the list in reverse
159 # account for offset, e.g. due to building the list in reverse
160 count = (stripecount + offset) % stripecount
160 count = (stripecount + offset) % stripecount
161 parity = (stripecount + offset) / stripecount & 1
161 parity = (stripecount + offset) / stripecount & 1
162 else:
162 else:
163 count = 0
163 count = 0
164 parity = 0
164 parity = 0
165 while True:
165 while True:
166 yield parity
166 yield parity
167 count += 1
167 count += 1
168 if stripecount and count >= stripecount:
168 if stripecount and count >= stripecount:
169 parity = 1 - parity
169 parity = 1 - parity
170 count = 0
170 count = 0
171
171
172 def get_contact(config):
172 def get_contact(config):
173 """Return repo contact information or empty string.
173 """Return repo contact information or empty string.
174
174
175 web.contact is the primary source, but if that is not set, try
175 web.contact is the primary source, but if that is not set, try
176 ui.username or $EMAIL as a fallback to display something useful.
176 ui.username or $EMAIL as a fallback to display something useful.
177 """
177 """
178 return (config("web", "contact") or
178 return (config("web", "contact") or
179 config("ui", "username") or
179 config("ui", "username") or
180 os.environ.get("EMAIL") or "")
180 os.environ.get("EMAIL") or "")
181
181
182 def caching(web, req):
182 def caching(web, req):
183 tag = str(web.mtime)
183 tag = str(web.mtime)
184 if req.env.get('HTTP_IF_NONE_MATCH') == tag:
184 if req.env.get('HTTP_IF_NONE_MATCH') == tag:
185 raise ErrorResponse(HTTP_NOT_MODIFIED)
185 raise ErrorResponse(HTTP_NOT_MODIFIED)
186 req.headers.append(('ETag', tag))
186 req.headers.append(('ETag', tag))
@@ -1,331 +1,332
1 # hgweb/hgweb_mod.py - Web interface for a repository.
1 # hgweb/hgweb_mod.py - Web interface for a repository.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 import os
9 import os
10 from mercurial import ui, hg, hook, error, encoding, templater, util
10 from mercurial import ui, hg, hook, error, encoding, templater, util
11 from common import get_stat, ErrorResponse, permhooks, caching
11 from common import get_stat, ErrorResponse, permhooks, caching
12 from common import HTTP_OK, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST
12 from common import HTTP_OK, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST
13 from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR
13 from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR
14 from request import wsgirequest
14 from request import wsgirequest
15 import webcommands, protocol, webutil
15 import webcommands, protocol, webutil
16
16
17 perms = {
17 perms = {
18 'changegroup': 'pull',
18 'changegroup': 'pull',
19 'changegroupsubset': 'pull',
19 'changegroupsubset': 'pull',
20 'getbundle': 'pull',
20 'getbundle': 'pull',
21 'stream_out': 'pull',
21 'stream_out': 'pull',
22 'listkeys': 'pull',
22 'listkeys': 'pull',
23 'unbundle': 'push',
23 'unbundle': 'push',
24 'pushkey': 'push',
24 'pushkey': 'push',
25 }
25 }
26
26
27 def makebreadcrumb(url):
27 def makebreadcrumb(url):
28 '''Return a 'URL breadcrumb' list
28 '''Return a 'URL breadcrumb' list
29
29
30 A 'URL breadcrumb' is a list of URL-name pairs,
30 A 'URL breadcrumb' is a list of URL-name pairs,
31 corresponding to each of the path items on a URL.
31 corresponding to each of the path items on a URL.
32 This can be used to create path navigation entries.
32 This can be used to create path navigation entries.
33 '''
33 '''
34 if url.endswith('/'):
34 if url.endswith('/'):
35 url = url[:-1]
35 url = url[:-1]
36 relpath = url
36 relpath = url
37 if relpath.startswith('/'):
37 if relpath.startswith('/'):
38 relpath = relpath[1:]
38 relpath = relpath[1:]
39
39
40 breadcrumb = []
40 breadcrumb = []
41 urlel = url
41 urlel = url
42 pathitems = [''] + relpath.split('/')
42 pathitems = [''] + relpath.split('/')
43 for pathel in reversed(pathitems):
43 for pathel in reversed(pathitems):
44 if not pathel or not urlel:
44 if not pathel or not urlel:
45 break
45 break
46 breadcrumb.append({'url': urlel, 'name': pathel})
46 breadcrumb.append({'url': urlel, 'name': pathel})
47 urlel = os.path.dirname(urlel)
47 urlel = os.path.dirname(urlel)
48 return reversed(breadcrumb)
48 return reversed(breadcrumb)
49
49
50
50
51 class hgweb(object):
51 class hgweb(object):
52 def __init__(self, repo, name=None, baseui=None):
52 def __init__(self, repo, name=None, baseui=None):
53 if isinstance(repo, str):
53 if isinstance(repo, str):
54 if baseui:
54 if baseui:
55 u = baseui.copy()
55 u = baseui.copy()
56 else:
56 else:
57 u = ui.ui()
57 u = ui.ui()
58 self.repo = hg.repository(u, repo)
58 self.repo = hg.repository(u, repo)
59 else:
59 else:
60 self.repo = repo
60 self.repo = repo
61
61
62 self.repo.ui.setconfig('ui', 'report_untrusted', 'off')
62 self.repo.ui.setconfig('ui', 'report_untrusted', 'off')
63 self.repo.ui.setconfig('ui', 'nontty', 'true')
63 self.repo.ui.setconfig('ui', 'nontty', 'true')
64 hook.redirect(True)
64 hook.redirect(True)
65 self.mtime = -1
65 self.mtime = -1
66 self.size = -1
66 self.size = -1
67 self.reponame = name
67 self.reponame = name
68 self.archives = 'zip', 'gz', 'bz2'
68 self.archives = 'zip', 'gz', 'bz2'
69 self.stripecount = 1
69 self.stripecount = 1
70 # a repo owner may set web.templates in .hg/hgrc to get any file
70 # a repo owner may set web.templates in .hg/hgrc to get any file
71 # readable by the user running the CGI script
71 # readable by the user running the CGI script
72 self.templatepath = self.config('web', 'templates')
72 self.templatepath = self.config('web', 'templates')
73
73
74 # The CGI scripts are often run by a user different from the repo owner.
74 # The CGI scripts are often run by a user different from the repo owner.
75 # Trust the settings from the .hg/hgrc files by default.
75 # Trust the settings from the .hg/hgrc files by default.
76 def config(self, section, name, default=None, untrusted=True):
76 def config(self, section, name, default=None, untrusted=True):
77 return self.repo.ui.config(section, name, default,
77 return self.repo.ui.config(section, name, default,
78 untrusted=untrusted)
78 untrusted=untrusted)
79
79
80 def configbool(self, section, name, default=False, untrusted=True):
80 def configbool(self, section, name, default=False, untrusted=True):
81 return self.repo.ui.configbool(section, name, default,
81 return self.repo.ui.configbool(section, name, default,
82 untrusted=untrusted)
82 untrusted=untrusted)
83
83
84 def configlist(self, section, name, default=None, untrusted=True):
84 def configlist(self, section, name, default=None, untrusted=True):
85 return self.repo.ui.configlist(section, name, default,
85 return self.repo.ui.configlist(section, name, default,
86 untrusted=untrusted)
86 untrusted=untrusted)
87
87
88 def refresh(self, request=None):
88 def refresh(self, request=None):
89 if request:
89 if request:
90 self.repo.ui.environ = request.env
90 self.repo.ui.environ = request.env
91 st = get_stat(self.repo.spath)
91 st = get_stat(self.repo.spath)
92 # compare changelog size in addition to mtime to catch
92 # compare changelog size in addition to mtime to catch
93 # rollbacks made less than a second ago
93 # rollbacks made less than a second ago
94 if st.st_mtime != self.mtime or st.st_size != self.size:
94 if st.st_mtime != self.mtime or st.st_size != self.size:
95 self.mtime = st.st_mtime
95 self.mtime = st.st_mtime
96 self.size = st.st_size
96 self.size = st.st_size
97 self.repo = hg.repository(self.repo.ui, self.repo.root)
97 self.repo = hg.repository(self.repo.ui, self.repo.root)
98 self.maxchanges = int(self.config("web", "maxchanges", 10))
98 self.maxchanges = int(self.config("web", "maxchanges", 10))
99 self.stripecount = int(self.config("web", "stripes", 1))
99 self.stripecount = int(self.config("web", "stripes", 1))
100 self.maxshortchanges = int(self.config("web", "maxshortchanges",
100 self.maxshortchanges = int(self.config("web", "maxshortchanges",
101 60))
101 60))
102 self.maxfiles = int(self.config("web", "maxfiles", 10))
102 self.maxfiles = int(self.config("web", "maxfiles", 10))
103 self.allowpull = self.configbool("web", "allowpull", True)
103 self.allowpull = self.configbool("web", "allowpull", True)
104 encoding.encoding = self.config("web", "encoding",
104 encoding.encoding = self.config("web", "encoding",
105 encoding.encoding)
105 encoding.encoding)
106
106
107 def run(self):
107 def run(self):
108 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
108 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
109 raise RuntimeError("This function is only intended to be "
109 raise RuntimeError("This function is only intended to be "
110 "called while running as a CGI script.")
110 "called while running as a CGI script.")
111 import mercurial.hgweb.wsgicgi as wsgicgi
111 import mercurial.hgweb.wsgicgi as wsgicgi
112 wsgicgi.launch(self)
112 wsgicgi.launch(self)
113
113
114 def __call__(self, env, respond):
114 def __call__(self, env, respond):
115 req = wsgirequest(env, respond)
115 req = wsgirequest(env, respond)
116 return self.run_wsgi(req)
116 return self.run_wsgi(req)
117
117
118 def run_wsgi(self, req):
118 def run_wsgi(self, req):
119
119
120 self.refresh(req)
120 self.refresh(req)
121
121
122 # work with CGI variables to create coherent structure
122 # work with CGI variables to create coherent structure
123 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
123 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
124
124
125 req.url = req.env['SCRIPT_NAME']
125 req.url = req.env['SCRIPT_NAME']
126 if not req.url.endswith('/'):
126 if not req.url.endswith('/'):
127 req.url += '/'
127 req.url += '/'
128 if 'REPO_NAME' in req.env:
128 if 'REPO_NAME' in req.env:
129 req.url += req.env['REPO_NAME'] + '/'
129 req.url += req.env['REPO_NAME'] + '/'
130
130
131 if 'PATH_INFO' in req.env:
131 if 'PATH_INFO' in req.env:
132 parts = req.env['PATH_INFO'].strip('/').split('/')
132 parts = req.env['PATH_INFO'].strip('/').split('/')
133 repo_parts = req.env.get('REPO_NAME', '').split('/')
133 repo_parts = req.env.get('REPO_NAME', '').split('/')
134 if parts[:len(repo_parts)] == repo_parts:
134 if parts[:len(repo_parts)] == repo_parts:
135 parts = parts[len(repo_parts):]
135 parts = parts[len(repo_parts):]
136 query = '/'.join(parts)
136 query = '/'.join(parts)
137 else:
137 else:
138 query = req.env['QUERY_STRING'].split('&', 1)[0]
138 query = req.env['QUERY_STRING'].split('&', 1)[0]
139 query = query.split(';', 1)[0]
139 query = query.split(';', 1)[0]
140
140
141 # process this if it's a protocol request
141 # process this if it's a protocol request
142 # protocol bits don't need to create any URLs
142 # protocol bits don't need to create any URLs
143 # and the clients always use the old URL structure
143 # and the clients always use the old URL structure
144
144
145 cmd = req.form.get('cmd', [''])[0]
145 cmd = req.form.get('cmd', [''])[0]
146 if protocol.iscmd(cmd):
146 if protocol.iscmd(cmd):
147 try:
147 try:
148 if query:
148 if query:
149 raise ErrorResponse(HTTP_NOT_FOUND)
149 raise ErrorResponse(HTTP_NOT_FOUND)
150 if cmd in perms:
150 if cmd in perms:
151 self.check_perm(req, perms[cmd])
151 self.check_perm(req, perms[cmd])
152 return protocol.call(self.repo, req, cmd)
152 return protocol.call(self.repo, req, cmd)
153 except ErrorResponse, inst:
153 except ErrorResponse, inst:
154 # A client that sends unbundle without 100-continue will
154 # A client that sends unbundle without 100-continue will
155 # break if we respond early.
155 # break if we respond early.
156 if (cmd == 'unbundle' and
156 if (cmd == 'unbundle' and
157 (req.env.get('HTTP_EXPECT',
157 (req.env.get('HTTP_EXPECT',
158 '').lower() != '100-continue') or
158 '').lower() != '100-continue') or
159 req.env.get('X-HgHttp2', '')):
159 req.env.get('X-HgHttp2', '')):
160 req.drain()
160 req.drain()
161 req.respond(inst, protocol.HGTYPE)
161 req.respond(inst, protocol.HGTYPE,
162 return '0\n%s\n' % inst.message
162 body='0\n%s\n' % inst.message)
163 return ''
163
164
164 # translate user-visible url structure to internal structure
165 # translate user-visible url structure to internal structure
165
166
166 args = query.split('/', 2)
167 args = query.split('/', 2)
167 if 'cmd' not in req.form and args and args[0]:
168 if 'cmd' not in req.form and args and args[0]:
168
169
169 cmd = args.pop(0)
170 cmd = args.pop(0)
170 style = cmd.rfind('-')
171 style = cmd.rfind('-')
171 if style != -1:
172 if style != -1:
172 req.form['style'] = [cmd[:style]]
173 req.form['style'] = [cmd[:style]]
173 cmd = cmd[style + 1:]
174 cmd = cmd[style + 1:]
174
175
175 # avoid accepting e.g. style parameter as command
176 # avoid accepting e.g. style parameter as command
176 if util.safehasattr(webcommands, cmd):
177 if util.safehasattr(webcommands, cmd):
177 req.form['cmd'] = [cmd]
178 req.form['cmd'] = [cmd]
178 else:
179 else:
179 cmd = ''
180 cmd = ''
180
181
181 if cmd == 'static':
182 if cmd == 'static':
182 req.form['file'] = ['/'.join(args)]
183 req.form['file'] = ['/'.join(args)]
183 else:
184 else:
184 if args and args[0]:
185 if args and args[0]:
185 node = args.pop(0)
186 node = args.pop(0)
186 req.form['node'] = [node]
187 req.form['node'] = [node]
187 if args:
188 if args:
188 req.form['file'] = args
189 req.form['file'] = args
189
190
190 ua = req.env.get('HTTP_USER_AGENT', '')
191 ua = req.env.get('HTTP_USER_AGENT', '')
191 if cmd == 'rev' and 'mercurial' in ua:
192 if cmd == 'rev' and 'mercurial' in ua:
192 req.form['style'] = ['raw']
193 req.form['style'] = ['raw']
193
194
194 if cmd == 'archive':
195 if cmd == 'archive':
195 fn = req.form['node'][0]
196 fn = req.form['node'][0]
196 for type_, spec in self.archive_specs.iteritems():
197 for type_, spec in self.archive_specs.iteritems():
197 ext = spec[2]
198 ext = spec[2]
198 if fn.endswith(ext):
199 if fn.endswith(ext):
199 req.form['node'] = [fn[:-len(ext)]]
200 req.form['node'] = [fn[:-len(ext)]]
200 req.form['type'] = [type_]
201 req.form['type'] = [type_]
201
202
202 # process the web interface request
203 # process the web interface request
203
204
204 try:
205 try:
205 tmpl = self.templater(req)
206 tmpl = self.templater(req)
206 ctype = tmpl('mimetype', encoding=encoding.encoding)
207 ctype = tmpl('mimetype', encoding=encoding.encoding)
207 ctype = templater.stringify(ctype)
208 ctype = templater.stringify(ctype)
208
209
209 # check read permissions non-static content
210 # check read permissions non-static content
210 if cmd != 'static':
211 if cmd != 'static':
211 self.check_perm(req, None)
212 self.check_perm(req, None)
212
213
213 if cmd == '':
214 if cmd == '':
214 req.form['cmd'] = [tmpl.cache['default']]
215 req.form['cmd'] = [tmpl.cache['default']]
215 cmd = req.form['cmd'][0]
216 cmd = req.form['cmd'][0]
216
217
217 if self.configbool('web', 'cache', True):
218 if self.configbool('web', 'cache', True):
218 caching(self, req) # sets ETag header or raises NOT_MODIFIED
219 caching(self, req) # sets ETag header or raises NOT_MODIFIED
219 if cmd not in webcommands.__all__:
220 if cmd not in webcommands.__all__:
220 msg = 'no such method: %s' % cmd
221 msg = 'no such method: %s' % cmd
221 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
222 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
222 elif cmd == 'file' and 'raw' in req.form.get('style', []):
223 elif cmd == 'file' and 'raw' in req.form.get('style', []):
223 self.ctype = ctype
224 self.ctype = ctype
224 content = webcommands.rawfile(self, req, tmpl)
225 content = webcommands.rawfile(self, req, tmpl)
225 else:
226 else:
226 content = getattr(webcommands, cmd)(self, req, tmpl)
227 content = getattr(webcommands, cmd)(self, req, tmpl)
227 req.respond(HTTP_OK, ctype)
228 req.respond(HTTP_OK, ctype)
228
229
229 return content
230 return content
230
231
231 except error.LookupError, err:
232 except error.LookupError, err:
232 req.respond(HTTP_NOT_FOUND, ctype)
233 req.respond(HTTP_NOT_FOUND, ctype)
233 msg = str(err)
234 msg = str(err)
234 if 'manifest' not in msg:
235 if 'manifest' not in msg:
235 msg = 'revision not found: %s' % err.name
236 msg = 'revision not found: %s' % err.name
236 return tmpl('error', error=msg)
237 return tmpl('error', error=msg)
237 except (error.RepoError, error.RevlogError), inst:
238 except (error.RepoError, error.RevlogError), inst:
238 req.respond(HTTP_SERVER_ERROR, ctype)
239 req.respond(HTTP_SERVER_ERROR, ctype)
239 return tmpl('error', error=str(inst))
240 return tmpl('error', error=str(inst))
240 except ErrorResponse, inst:
241 except ErrorResponse, inst:
241 req.respond(inst, ctype)
242 req.respond(inst, ctype)
242 if inst.code == HTTP_NOT_MODIFIED:
243 if inst.code == HTTP_NOT_MODIFIED:
243 # Not allowed to return a body on a 304
244 # Not allowed to return a body on a 304
244 return ['']
245 return ['']
245 return tmpl('error', error=inst.message)
246 return tmpl('error', error=inst.message)
246
247
247 def templater(self, req):
248 def templater(self, req):
248
249
249 # determine scheme, port and server name
250 # determine scheme, port and server name
250 # this is needed to create absolute urls
251 # this is needed to create absolute urls
251
252
252 proto = req.env.get('wsgi.url_scheme')
253 proto = req.env.get('wsgi.url_scheme')
253 if proto == 'https':
254 if proto == 'https':
254 proto = 'https'
255 proto = 'https'
255 default_port = "443"
256 default_port = "443"
256 else:
257 else:
257 proto = 'http'
258 proto = 'http'
258 default_port = "80"
259 default_port = "80"
259
260
260 port = req.env["SERVER_PORT"]
261 port = req.env["SERVER_PORT"]
261 port = port != default_port and (":" + port) or ""
262 port = port != default_port and (":" + port) or ""
262 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
263 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
263 logourl = self.config("web", "logourl", "http://mercurial.selenic.com/")
264 logourl = self.config("web", "logourl", "http://mercurial.selenic.com/")
264 logoimg = self.config("web", "logoimg", "hglogo.png")
265 logoimg = self.config("web", "logoimg", "hglogo.png")
265 staticurl = self.config("web", "staticurl") or req.url + 'static/'
266 staticurl = self.config("web", "staticurl") or req.url + 'static/'
266 if not staticurl.endswith('/'):
267 if not staticurl.endswith('/'):
267 staticurl += '/'
268 staticurl += '/'
268
269
269 # some functions for the templater
270 # some functions for the templater
270
271
271 def header(**map):
272 def header(**map):
272 yield tmpl('header', encoding=encoding.encoding, **map)
273 yield tmpl('header', encoding=encoding.encoding, **map)
273
274
274 def footer(**map):
275 def footer(**map):
275 yield tmpl("footer", **map)
276 yield tmpl("footer", **map)
276
277
277 def motd(**map):
278 def motd(**map):
278 yield self.config("web", "motd", "")
279 yield self.config("web", "motd", "")
279
280
280 # figure out which style to use
281 # figure out which style to use
281
282
282 vars = {}
283 vars = {}
283 styles = (
284 styles = (
284 req.form.get('style', [None])[0],
285 req.form.get('style', [None])[0],
285 self.config('web', 'style'),
286 self.config('web', 'style'),
286 'paper',
287 'paper',
287 )
288 )
288 style, mapfile = templater.stylemap(styles, self.templatepath)
289 style, mapfile = templater.stylemap(styles, self.templatepath)
289 if style == styles[0]:
290 if style == styles[0]:
290 vars['style'] = style
291 vars['style'] = style
291
292
292 start = req.url[-1] == '?' and '&' or '?'
293 start = req.url[-1] == '?' and '&' or '?'
293 sessionvars = webutil.sessionvars(vars, start)
294 sessionvars = webutil.sessionvars(vars, start)
294
295
295 if not self.reponame:
296 if not self.reponame:
296 self.reponame = (self.config("web", "name")
297 self.reponame = (self.config("web", "name")
297 or req.env.get('REPO_NAME')
298 or req.env.get('REPO_NAME')
298 or req.url.strip('/') or self.repo.root)
299 or req.url.strip('/') or self.repo.root)
299
300
300 # create the templater
301 # create the templater
301
302
302 tmpl = templater.templater(mapfile,
303 tmpl = templater.templater(mapfile,
303 defaults={"url": req.url,
304 defaults={"url": req.url,
304 "logourl": logourl,
305 "logourl": logourl,
305 "logoimg": logoimg,
306 "logoimg": logoimg,
306 "staticurl": staticurl,
307 "staticurl": staticurl,
307 "urlbase": urlbase,
308 "urlbase": urlbase,
308 "repo": self.reponame,
309 "repo": self.reponame,
309 "header": header,
310 "header": header,
310 "footer": footer,
311 "footer": footer,
311 "motd": motd,
312 "motd": motd,
312 "sessionvars": sessionvars,
313 "sessionvars": sessionvars,
313 "pathdef": makebreadcrumb(req.url),
314 "pathdef": makebreadcrumb(req.url),
314 })
315 })
315 return tmpl
316 return tmpl
316
317
317 def archivelist(self, nodeid):
318 def archivelist(self, nodeid):
318 allowed = self.configlist("web", "allow_archive")
319 allowed = self.configlist("web", "allow_archive")
319 for i, spec in self.archive_specs.iteritems():
320 for i, spec in self.archive_specs.iteritems():
320 if i in allowed or self.configbool("web", "allow" + i):
321 if i in allowed or self.configbool("web", "allow" + i):
321 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
322 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
322
323
323 archive_specs = {
324 archive_specs = {
324 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
325 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
325 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
326 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
326 'zip': ('application/zip', 'zip', '.zip', None),
327 'zip': ('application/zip', 'zip', '.zip', None),
327 }
328 }
328
329
329 def check_perm(self, req, op):
330 def check_perm(self, req, op):
330 for hook in permhooks:
331 for hook in permhooks:
331 hook(self, req, op)
332 hook(self, req, op)
@@ -1,98 +1,98
1 #
1 #
2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 import cgi, cStringIO, zlib, urllib
8 import cgi, cStringIO, zlib, urllib
9 from mercurial import util, wireproto
9 from mercurial import util, wireproto
10 from common import HTTP_OK
10 from common import HTTP_OK
11
11
12 HGTYPE = 'application/mercurial-0.1'
12 HGTYPE = 'application/mercurial-0.1'
13 HGERRTYPE = 'application/hg-error'
13 HGERRTYPE = 'application/hg-error'
14
14
15 class webproto(object):
15 class webproto(object):
16 def __init__(self, req, ui):
16 def __init__(self, req, ui):
17 self.req = req
17 self.req = req
18 self.response = ''
18 self.response = ''
19 self.ui = ui
19 self.ui = ui
20 def getargs(self, args):
20 def getargs(self, args):
21 knownargs = self._args()
21 knownargs = self._args()
22 data = {}
22 data = {}
23 keys = args.split()
23 keys = args.split()
24 for k in keys:
24 for k in keys:
25 if k == '*':
25 if k == '*':
26 star = {}
26 star = {}
27 for key in knownargs.keys():
27 for key in knownargs.keys():
28 if key != 'cmd' and key not in keys:
28 if key != 'cmd' and key not in keys:
29 star[key] = knownargs[key][0]
29 star[key] = knownargs[key][0]
30 data['*'] = star
30 data['*'] = star
31 else:
31 else:
32 data[k] = knownargs[k][0]
32 data[k] = knownargs[k][0]
33 return [data[k] for k in keys]
33 return [data[k] for k in keys]
34 def _args(self):
34 def _args(self):
35 args = self.req.form.copy()
35 args = self.req.form.copy()
36 chunks = []
36 chunks = []
37 i = 1
37 i = 1
38 while True:
38 while True:
39 h = self.req.env.get('HTTP_X_HGARG_' + str(i))
39 h = self.req.env.get('HTTP_X_HGARG_' + str(i))
40 if h is None:
40 if h is None:
41 break
41 break
42 chunks += [h]
42 chunks += [h]
43 i += 1
43 i += 1
44 args.update(cgi.parse_qs(''.join(chunks), keep_blank_values=True))
44 args.update(cgi.parse_qs(''.join(chunks), keep_blank_values=True))
45 return args
45 return args
46 def getfile(self, fp):
46 def getfile(self, fp):
47 length = int(self.req.env['CONTENT_LENGTH'])
47 length = int(self.req.env['CONTENT_LENGTH'])
48 for s in util.filechunkiter(self.req, limit=length):
48 for s in util.filechunkiter(self.req, limit=length):
49 fp.write(s)
49 fp.write(s)
50 def redirect(self):
50 def redirect(self):
51 self.oldio = self.ui.fout, self.ui.ferr
51 self.oldio = self.ui.fout, self.ui.ferr
52 self.ui.ferr = self.ui.fout = cStringIO.StringIO()
52 self.ui.ferr = self.ui.fout = cStringIO.StringIO()
53 def restore(self):
53 def restore(self):
54 val = self.ui.fout.getvalue()
54 val = self.ui.fout.getvalue()
55 self.ui.ferr, self.ui.fout = self.oldio
55 self.ui.ferr, self.ui.fout = self.oldio
56 return val
56 return val
57 def groupchunks(self, cg):
57 def groupchunks(self, cg):
58 z = zlib.compressobj()
58 z = zlib.compressobj()
59 while True:
59 while True:
60 chunk = cg.read(4096)
60 chunk = cg.read(4096)
61 if not chunk:
61 if not chunk:
62 break
62 break
63 yield z.compress(chunk)
63 yield z.compress(chunk)
64 yield z.flush()
64 yield z.flush()
65 def _client(self):
65 def _client(self):
66 return 'remote:%s:%s:%s' % (
66 return 'remote:%s:%s:%s' % (
67 self.req.env.get('wsgi.url_scheme') or 'http',
67 self.req.env.get('wsgi.url_scheme') or 'http',
68 urllib.quote(self.req.env.get('REMOTE_HOST', '')),
68 urllib.quote(self.req.env.get('REMOTE_HOST', '')),
69 urllib.quote(self.req.env.get('REMOTE_USER', '')))
69 urllib.quote(self.req.env.get('REMOTE_USER', '')))
70
70
71 def iscmd(cmd):
71 def iscmd(cmd):
72 return cmd in wireproto.commands
72 return cmd in wireproto.commands
73
73
74 def call(repo, req, cmd):
74 def call(repo, req, cmd):
75 p = webproto(req, repo.ui)
75 p = webproto(req, repo.ui)
76 rsp = wireproto.dispatch(repo, p, cmd)
76 rsp = wireproto.dispatch(repo, p, cmd)
77 if isinstance(rsp, str):
77 if isinstance(rsp, str):
78 req.respond(HTTP_OK, HGTYPE, length=len(rsp))
78 req.respond(HTTP_OK, HGTYPE, body=rsp)
79 return [rsp]
79 return []
80 elif isinstance(rsp, wireproto.streamres):
80 elif isinstance(rsp, wireproto.streamres):
81 req.respond(HTTP_OK, HGTYPE)
81 req.respond(HTTP_OK, HGTYPE)
82 return rsp.gen
82 return rsp.gen
83 elif isinstance(rsp, wireproto.pushres):
83 elif isinstance(rsp, wireproto.pushres):
84 val = p.restore()
84 val = p.restore()
85 rsp = '%d\n%s' % (rsp.res, val)
85 rsp = '%d\n%s' % (rsp.res, val)
86 req.respond(HTTP_OK, HGTYPE, length=len(rsp))
86 req.respond(HTTP_OK, HGTYPE, body=rsp)
87 return [rsp]
87 return []
88 elif isinstance(rsp, wireproto.pusherr):
88 elif isinstance(rsp, wireproto.pusherr):
89 # drain the incoming bundle
89 # drain the incoming bundle
90 req.drain()
90 req.drain()
91 p.restore()
91 p.restore()
92 rsp = '0\n%s\n' % rsp.res
92 rsp = '0\n%s\n' % rsp.res
93 req.respond(HTTP_OK, HGTYPE, length=len(rsp))
93 req.respond(HTTP_OK, HGTYPE, body=rsp)
94 return [rsp]
94 return []
95 elif isinstance(rsp, wireproto.ooberror):
95 elif isinstance(rsp, wireproto.ooberror):
96 rsp = rsp.message
96 rsp = rsp.message
97 req.respond(HTTP_OK, HGERRTYPE, length=len(rsp))
97 req.respond(HTTP_OK, HGERRTYPE, body=rsp)
98 return [rsp]
98 return []
@@ -1,131 +1,134
1 # hgweb/request.py - An http request from either CGI or the standalone server.
1 # hgweb/request.py - An http request from either CGI or the standalone server.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 import socket, cgi, errno
9 import socket, cgi, errno
10 from mercurial import util
10 from mercurial import util
11 from common import ErrorResponse, statusmessage, HTTP_NOT_MODIFIED
11 from common import ErrorResponse, statusmessage, HTTP_NOT_MODIFIED
12
12
13 shortcuts = {
13 shortcuts = {
14 'cl': [('cmd', ['changelog']), ('rev', None)],
14 'cl': [('cmd', ['changelog']), ('rev', None)],
15 'sl': [('cmd', ['shortlog']), ('rev', None)],
15 'sl': [('cmd', ['shortlog']), ('rev', None)],
16 'cs': [('cmd', ['changeset']), ('node', None)],
16 'cs': [('cmd', ['changeset']), ('node', None)],
17 'f': [('cmd', ['file']), ('filenode', None)],
17 'f': [('cmd', ['file']), ('filenode', None)],
18 'fl': [('cmd', ['filelog']), ('filenode', None)],
18 'fl': [('cmd', ['filelog']), ('filenode', None)],
19 'fd': [('cmd', ['filediff']), ('node', None)],
19 'fd': [('cmd', ['filediff']), ('node', None)],
20 'fa': [('cmd', ['annotate']), ('filenode', None)],
20 'fa': [('cmd', ['annotate']), ('filenode', None)],
21 'mf': [('cmd', ['manifest']), ('manifest', None)],
21 'mf': [('cmd', ['manifest']), ('manifest', None)],
22 'ca': [('cmd', ['archive']), ('node', None)],
22 'ca': [('cmd', ['archive']), ('node', None)],
23 'tags': [('cmd', ['tags'])],
23 'tags': [('cmd', ['tags'])],
24 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
24 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
25 'static': [('cmd', ['static']), ('file', None)]
25 'static': [('cmd', ['static']), ('file', None)]
26 }
26 }
27
27
28 def normalize(form):
28 def normalize(form):
29 # first expand the shortcuts
29 # first expand the shortcuts
30 for k in shortcuts.iterkeys():
30 for k in shortcuts.iterkeys():
31 if k in form:
31 if k in form:
32 for name, value in shortcuts[k]:
32 for name, value in shortcuts[k]:
33 if value is None:
33 if value is None:
34 value = form[k]
34 value = form[k]
35 form[name] = value
35 form[name] = value
36 del form[k]
36 del form[k]
37 # And strip the values
37 # And strip the values
38 for k, v in form.iteritems():
38 for k, v in form.iteritems():
39 form[k] = [i.strip() for i in v]
39 form[k] = [i.strip() for i in v]
40 return form
40 return form
41
41
42 class wsgirequest(object):
42 class wsgirequest(object):
43 def __init__(self, wsgienv, start_response):
43 def __init__(self, wsgienv, start_response):
44 version = wsgienv['wsgi.version']
44 version = wsgienv['wsgi.version']
45 if (version < (1, 0)) or (version >= (2, 0)):
45 if (version < (1, 0)) or (version >= (2, 0)):
46 raise RuntimeError("Unknown and unsupported WSGI version %d.%d"
46 raise RuntimeError("Unknown and unsupported WSGI version %d.%d"
47 % version)
47 % version)
48 self.inp = wsgienv['wsgi.input']
48 self.inp = wsgienv['wsgi.input']
49 self.err = wsgienv['wsgi.errors']
49 self.err = wsgienv['wsgi.errors']
50 self.threaded = wsgienv['wsgi.multithread']
50 self.threaded = wsgienv['wsgi.multithread']
51 self.multiprocess = wsgienv['wsgi.multiprocess']
51 self.multiprocess = wsgienv['wsgi.multiprocess']
52 self.run_once = wsgienv['wsgi.run_once']
52 self.run_once = wsgienv['wsgi.run_once']
53 self.env = wsgienv
53 self.env = wsgienv
54 self.form = normalize(cgi.parse(self.inp,
54 self.form = normalize(cgi.parse(self.inp,
55 self.env,
55 self.env,
56 keep_blank_values=1))
56 keep_blank_values=1))
57 self._start_response = start_response
57 self._start_response = start_response
58 self.server_write = None
58 self.server_write = None
59 self.headers = []
59 self.headers = []
60
60
61 def __iter__(self):
61 def __iter__(self):
62 return iter([])
62 return iter([])
63
63
64 def read(self, count=-1):
64 def read(self, count=-1):
65 return self.inp.read(count)
65 return self.inp.read(count)
66
66
67 def drain(self):
67 def drain(self):
68 '''need to read all data from request, httplib is half-duplex'''
68 '''need to read all data from request, httplib is half-duplex'''
69 length = int(self.env.get('CONTENT_LENGTH') or 0)
69 length = int(self.env.get('CONTENT_LENGTH') or 0)
70 for s in util.filechunkiter(self.inp, limit=length):
70 for s in util.filechunkiter(self.inp, limit=length):
71 pass
71 pass
72
72
73 def respond(self, status, type, filename=None, length=None):
73 def respond(self, status, type, filename=None, body=None):
74 if self._start_response is not None:
74 if self._start_response is not None:
75 self.headers.append(('Content-Type', type))
75 self.headers.append(('Content-Type', type))
76 if filename:
76 if filename:
77 filename = (filename.split('/')[-1]
77 filename = (filename.split('/')[-1]
78 .replace('\\', '\\\\').replace('"', '\\"'))
78 .replace('\\', '\\\\').replace('"', '\\"'))
79 self.headers.append(('Content-Disposition',
79 self.headers.append(('Content-Disposition',
80 'inline; filename="%s"' % filename))
80 'inline; filename="%s"' % filename))
81 if length is not None:
81 if body is not None:
82 self.headers.append(('Content-Length', str(length)))
82 self.headers.append(('Content-Length', str(len(body))))
83
83
84 for k, v in self.headers:
84 for k, v in self.headers:
85 if not isinstance(v, str):
85 if not isinstance(v, str):
86 raise TypeError('header value must be string: %r' % (v,))
86 raise TypeError('header value must be string: %r' % (v,))
87
87
88 if isinstance(status, ErrorResponse):
88 if isinstance(status, ErrorResponse):
89 self.headers.extend(status.headers)
89 self.headers.extend(status.headers)
90 if status.code == HTTP_NOT_MODIFIED:
90 if status.code == HTTP_NOT_MODIFIED:
91 # RFC 2616 Section 10.3.5: 304 Not Modified has cases where
91 # RFC 2616 Section 10.3.5: 304 Not Modified has cases where
92 # it MUST NOT include any headers other than these and no
92 # it MUST NOT include any headers other than these and no
93 # body
93 # body
94 self.headers = [(k, v) for (k, v) in self.headers if
94 self.headers = [(k, v) for (k, v) in self.headers if
95 k in ('Date', 'ETag', 'Expires',
95 k in ('Date', 'ETag', 'Expires',
96 'Cache-Control', 'Vary')]
96 'Cache-Control', 'Vary')]
97 status = statusmessage(status.code, status.message)
97 status = statusmessage(status.code, status.message)
98 elif status == 200:
98 elif status == 200:
99 status = '200 Script output follows'
99 status = '200 Script output follows'
100 elif isinstance(status, int):
100 elif isinstance(status, int):
101 status = statusmessage(status)
101 status = statusmessage(status)
102
102
103 self.server_write = self._start_response(status, self.headers)
103 self.server_write = self._start_response(status, self.headers)
104 self._start_response = None
104 self._start_response = None
105 self.headers = []
105 self.headers = []
106 if body is not None:
107 self.write(body)
108 self.server_write = None
106
109
107 def write(self, thing):
110 def write(self, thing):
108 if thing:
111 if thing:
109 try:
112 try:
110 self.server_write(thing)
113 self.server_write(thing)
111 except socket.error, inst:
114 except socket.error, inst:
112 if inst[0] != errno.ECONNRESET:
115 if inst[0] != errno.ECONNRESET:
113 raise
116 raise
114
117
115 def writelines(self, lines):
118 def writelines(self, lines):
116 for line in lines:
119 for line in lines:
117 self.write(line)
120 self.write(line)
118
121
119 def flush(self):
122 def flush(self):
120 return None
123 return None
121
124
122 def close(self):
125 def close(self):
123 return None
126 return None
124
127
125 def wsgiapplication(app_maker):
128 def wsgiapplication(app_maker):
126 '''For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
129 '''For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
127 can and should now be used as a WSGI application.'''
130 can and should now be used as a WSGI application.'''
128 application = app_maker()
131 application = app_maker()
129 def run_wsgi(env, respond):
132 def run_wsgi(env, respond):
130 return application(env, respond)
133 return application(env, respond)
131 return run_wsgi
134 return run_wsgi
@@ -1,981 +1,981
1 #
1 #
2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 import os, mimetypes, re, cgi, copy
8 import os, mimetypes, re, cgi, copy
9 import webutil
9 import webutil
10 from mercurial import error, encoding, archival, templater, templatefilters
10 from mercurial import error, encoding, archival, templater, templatefilters
11 from mercurial.node import short, hex, nullid
11 from mercurial.node import short, hex, nullid
12 from mercurial.util import binary
12 from mercurial.util import binary
13 from common import paritygen, staticfile, get_contact, ErrorResponse
13 from common import paritygen, staticfile, get_contact, ErrorResponse
14 from common import HTTP_OK, HTTP_FORBIDDEN, HTTP_NOT_FOUND
14 from common import HTTP_OK, HTTP_FORBIDDEN, HTTP_NOT_FOUND
15 from mercurial import graphmod, patch
15 from mercurial import graphmod, patch
16 from mercurial import help as helpmod
16 from mercurial import help as helpmod
17 from mercurial import scmutil
17 from mercurial import scmutil
18 from mercurial.i18n import _
18 from mercurial.i18n import _
19
19
20 # __all__ is populated with the allowed commands. Be sure to add to it if
20 # __all__ is populated with the allowed commands. Be sure to add to it if
21 # you're adding a new command, or the new command won't work.
21 # you're adding a new command, or the new command won't work.
22
22
23 __all__ = [
23 __all__ = [
24 'log', 'rawfile', 'file', 'changelog', 'shortlog', 'changeset', 'rev',
24 'log', 'rawfile', 'file', 'changelog', 'shortlog', 'changeset', 'rev',
25 'manifest', 'tags', 'bookmarks', 'branches', 'summary', 'filediff', 'diff',
25 'manifest', 'tags', 'bookmarks', 'branches', 'summary', 'filediff', 'diff',
26 'comparison', 'annotate', 'filelog', 'archive', 'static', 'graph', 'help',
26 'comparison', 'annotate', 'filelog', 'archive', 'static', 'graph', 'help',
27 ]
27 ]
28
28
29 def log(web, req, tmpl):
29 def log(web, req, tmpl):
30 if 'file' in req.form and req.form['file'][0]:
30 if 'file' in req.form and req.form['file'][0]:
31 return filelog(web, req, tmpl)
31 return filelog(web, req, tmpl)
32 else:
32 else:
33 return changelog(web, req, tmpl)
33 return changelog(web, req, tmpl)
34
34
35 def rawfile(web, req, tmpl):
35 def rawfile(web, req, tmpl):
36 guessmime = web.configbool('web', 'guessmime', False)
36 guessmime = web.configbool('web', 'guessmime', False)
37
37
38 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
38 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
39 if not path:
39 if not path:
40 content = manifest(web, req, tmpl)
40 content = manifest(web, req, tmpl)
41 req.respond(HTTP_OK, web.ctype)
41 req.respond(HTTP_OK, web.ctype)
42 return content
42 return content
43
43
44 try:
44 try:
45 fctx = webutil.filectx(web.repo, req)
45 fctx = webutil.filectx(web.repo, req)
46 except error.LookupError, inst:
46 except error.LookupError, inst:
47 try:
47 try:
48 content = manifest(web, req, tmpl)
48 content = manifest(web, req, tmpl)
49 req.respond(HTTP_OK, web.ctype)
49 req.respond(HTTP_OK, web.ctype)
50 return content
50 return content
51 except ErrorResponse:
51 except ErrorResponse:
52 raise inst
52 raise inst
53
53
54 path = fctx.path()
54 path = fctx.path()
55 text = fctx.data()
55 text = fctx.data()
56 mt = 'application/binary'
56 mt = 'application/binary'
57 if guessmime:
57 if guessmime:
58 mt = mimetypes.guess_type(path)[0]
58 mt = mimetypes.guess_type(path)[0]
59 if mt is None:
59 if mt is None:
60 mt = binary(text) and 'application/binary' or 'text/plain'
60 mt = binary(text) and 'application/binary' or 'text/plain'
61 if mt.startswith('text/'):
61 if mt.startswith('text/'):
62 mt += '; charset="%s"' % encoding.encoding
62 mt += '; charset="%s"' % encoding.encoding
63
63
64 req.respond(HTTP_OK, mt, path, len(text))
64 req.respond(HTTP_OK, mt, path, body=text)
65 return [text]
65 return []
66
66
67 def _filerevision(web, tmpl, fctx):
67 def _filerevision(web, tmpl, fctx):
68 f = fctx.path()
68 f = fctx.path()
69 text = fctx.data()
69 text = fctx.data()
70 parity = paritygen(web.stripecount)
70 parity = paritygen(web.stripecount)
71
71
72 if binary(text):
72 if binary(text):
73 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
73 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
74 text = '(binary:%s)' % mt
74 text = '(binary:%s)' % mt
75
75
76 def lines():
76 def lines():
77 for lineno, t in enumerate(text.splitlines(True)):
77 for lineno, t in enumerate(text.splitlines(True)):
78 yield {"line": t,
78 yield {"line": t,
79 "lineid": "l%d" % (lineno + 1),
79 "lineid": "l%d" % (lineno + 1),
80 "linenumber": "% 6d" % (lineno + 1),
80 "linenumber": "% 6d" % (lineno + 1),
81 "parity": parity.next()}
81 "parity": parity.next()}
82
82
83 return tmpl("filerevision",
83 return tmpl("filerevision",
84 file=f,
84 file=f,
85 path=webutil.up(f),
85 path=webutil.up(f),
86 text=lines(),
86 text=lines(),
87 rev=fctx.rev(),
87 rev=fctx.rev(),
88 node=fctx.hex(),
88 node=fctx.hex(),
89 author=fctx.user(),
89 author=fctx.user(),
90 date=fctx.date(),
90 date=fctx.date(),
91 desc=fctx.description(),
91 desc=fctx.description(),
92 branch=webutil.nodebranchnodefault(fctx),
92 branch=webutil.nodebranchnodefault(fctx),
93 parent=webutil.parents(fctx),
93 parent=webutil.parents(fctx),
94 child=webutil.children(fctx),
94 child=webutil.children(fctx),
95 rename=webutil.renamelink(fctx),
95 rename=webutil.renamelink(fctx),
96 permissions=fctx.manifest().flags(f))
96 permissions=fctx.manifest().flags(f))
97
97
98 def file(web, req, tmpl):
98 def file(web, req, tmpl):
99 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
99 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
100 if not path:
100 if not path:
101 return manifest(web, req, tmpl)
101 return manifest(web, req, tmpl)
102 try:
102 try:
103 return _filerevision(web, tmpl, webutil.filectx(web.repo, req))
103 return _filerevision(web, tmpl, webutil.filectx(web.repo, req))
104 except error.LookupError, inst:
104 except error.LookupError, inst:
105 try:
105 try:
106 return manifest(web, req, tmpl)
106 return manifest(web, req, tmpl)
107 except ErrorResponse:
107 except ErrorResponse:
108 raise inst
108 raise inst
109
109
110 def _search(web, req, tmpl):
110 def _search(web, req, tmpl):
111
111
112 query = req.form['rev'][0]
112 query = req.form['rev'][0]
113 revcount = web.maxchanges
113 revcount = web.maxchanges
114 if 'revcount' in req.form:
114 if 'revcount' in req.form:
115 revcount = int(req.form.get('revcount', [revcount])[0])
115 revcount = int(req.form.get('revcount', [revcount])[0])
116 revcount = max(revcount, 1)
116 revcount = max(revcount, 1)
117 tmpl.defaults['sessionvars']['revcount'] = revcount
117 tmpl.defaults['sessionvars']['revcount'] = revcount
118
118
119 lessvars = copy.copy(tmpl.defaults['sessionvars'])
119 lessvars = copy.copy(tmpl.defaults['sessionvars'])
120 lessvars['revcount'] = max(revcount / 2, 1)
120 lessvars['revcount'] = max(revcount / 2, 1)
121 lessvars['rev'] = query
121 lessvars['rev'] = query
122 morevars = copy.copy(tmpl.defaults['sessionvars'])
122 morevars = copy.copy(tmpl.defaults['sessionvars'])
123 morevars['revcount'] = revcount * 2
123 morevars['revcount'] = revcount * 2
124 morevars['rev'] = query
124 morevars['rev'] = query
125
125
126 def changelist(**map):
126 def changelist(**map):
127 count = 0
127 count = 0
128 lower = encoding.lower
128 lower = encoding.lower
129 qw = lower(query).split()
129 qw = lower(query).split()
130
130
131 def revgen():
131 def revgen():
132 for i in xrange(len(web.repo) - 1, 0, -100):
132 for i in xrange(len(web.repo) - 1, 0, -100):
133 l = []
133 l = []
134 for j in xrange(max(0, i - 100), i + 1):
134 for j in xrange(max(0, i - 100), i + 1):
135 ctx = web.repo[j]
135 ctx = web.repo[j]
136 l.append(ctx)
136 l.append(ctx)
137 l.reverse()
137 l.reverse()
138 for e in l:
138 for e in l:
139 yield e
139 yield e
140
140
141 for ctx in revgen():
141 for ctx in revgen():
142 miss = 0
142 miss = 0
143 for q in qw:
143 for q in qw:
144 if not (q in lower(ctx.user()) or
144 if not (q in lower(ctx.user()) or
145 q in lower(ctx.description()) or
145 q in lower(ctx.description()) or
146 q in lower(" ".join(ctx.files()))):
146 q in lower(" ".join(ctx.files()))):
147 miss = 1
147 miss = 1
148 break
148 break
149 if miss:
149 if miss:
150 continue
150 continue
151
151
152 count += 1
152 count += 1
153 n = ctx.node()
153 n = ctx.node()
154 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
154 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
155 files = webutil.listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
155 files = webutil.listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
156
156
157 yield tmpl('searchentry',
157 yield tmpl('searchentry',
158 parity=parity.next(),
158 parity=parity.next(),
159 author=ctx.user(),
159 author=ctx.user(),
160 parent=webutil.parents(ctx),
160 parent=webutil.parents(ctx),
161 child=webutil.children(ctx),
161 child=webutil.children(ctx),
162 changelogtag=showtags,
162 changelogtag=showtags,
163 desc=ctx.description(),
163 desc=ctx.description(),
164 date=ctx.date(),
164 date=ctx.date(),
165 files=files,
165 files=files,
166 rev=ctx.rev(),
166 rev=ctx.rev(),
167 node=hex(n),
167 node=hex(n),
168 tags=webutil.nodetagsdict(web.repo, n),
168 tags=webutil.nodetagsdict(web.repo, n),
169 bookmarks=webutil.nodebookmarksdict(web.repo, n),
169 bookmarks=webutil.nodebookmarksdict(web.repo, n),
170 inbranch=webutil.nodeinbranch(web.repo, ctx),
170 inbranch=webutil.nodeinbranch(web.repo, ctx),
171 branches=webutil.nodebranchdict(web.repo, ctx))
171 branches=webutil.nodebranchdict(web.repo, ctx))
172
172
173 if count >= revcount:
173 if count >= revcount:
174 break
174 break
175
175
176 tip = web.repo['tip']
176 tip = web.repo['tip']
177 parity = paritygen(web.stripecount)
177 parity = paritygen(web.stripecount)
178
178
179 return tmpl('search', query=query, node=tip.hex(),
179 return tmpl('search', query=query, node=tip.hex(),
180 entries=changelist, archives=web.archivelist("tip"),
180 entries=changelist, archives=web.archivelist("tip"),
181 morevars=morevars, lessvars=lessvars)
181 morevars=morevars, lessvars=lessvars)
182
182
183 def changelog(web, req, tmpl, shortlog=False):
183 def changelog(web, req, tmpl, shortlog=False):
184
184
185 if 'node' in req.form:
185 if 'node' in req.form:
186 ctx = webutil.changectx(web.repo, req)
186 ctx = webutil.changectx(web.repo, req)
187 else:
187 else:
188 if 'rev' in req.form:
188 if 'rev' in req.form:
189 hi = req.form['rev'][0]
189 hi = req.form['rev'][0]
190 else:
190 else:
191 hi = len(web.repo) - 1
191 hi = len(web.repo) - 1
192 try:
192 try:
193 ctx = web.repo[hi]
193 ctx = web.repo[hi]
194 except error.RepoError:
194 except error.RepoError:
195 return _search(web, req, tmpl) # XXX redirect to 404 page?
195 return _search(web, req, tmpl) # XXX redirect to 404 page?
196
196
197 def changelist(limit=0, **map):
197 def changelist(limit=0, **map):
198 l = [] # build a list in forward order for efficiency
198 l = [] # build a list in forward order for efficiency
199 for i in xrange(start, end):
199 for i in xrange(start, end):
200 ctx = web.repo[i]
200 ctx = web.repo[i]
201 n = ctx.node()
201 n = ctx.node()
202 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
202 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
203 files = webutil.listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
203 files = webutil.listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
204
204
205 l.append({"parity": parity.next(),
205 l.append({"parity": parity.next(),
206 "author": ctx.user(),
206 "author": ctx.user(),
207 "parent": webutil.parents(ctx, i - 1),
207 "parent": webutil.parents(ctx, i - 1),
208 "child": webutil.children(ctx, i + 1),
208 "child": webutil.children(ctx, i + 1),
209 "changelogtag": showtags,
209 "changelogtag": showtags,
210 "desc": ctx.description(),
210 "desc": ctx.description(),
211 "date": ctx.date(),
211 "date": ctx.date(),
212 "files": files,
212 "files": files,
213 "rev": i,
213 "rev": i,
214 "node": hex(n),
214 "node": hex(n),
215 "tags": webutil.nodetagsdict(web.repo, n),
215 "tags": webutil.nodetagsdict(web.repo, n),
216 "bookmarks": webutil.nodebookmarksdict(web.repo, n),
216 "bookmarks": webutil.nodebookmarksdict(web.repo, n),
217 "inbranch": webutil.nodeinbranch(web.repo, ctx),
217 "inbranch": webutil.nodeinbranch(web.repo, ctx),
218 "branches": webutil.nodebranchdict(web.repo, ctx)
218 "branches": webutil.nodebranchdict(web.repo, ctx)
219 })
219 })
220 if limit > 0:
220 if limit > 0:
221 l = l[-limit:]
221 l = l[-limit:]
222
222
223 for e in reversed(l):
223 for e in reversed(l):
224 yield e
224 yield e
225
225
226 revcount = shortlog and web.maxshortchanges or web.maxchanges
226 revcount = shortlog and web.maxshortchanges or web.maxchanges
227 if 'revcount' in req.form:
227 if 'revcount' in req.form:
228 revcount = int(req.form.get('revcount', [revcount])[0])
228 revcount = int(req.form.get('revcount', [revcount])[0])
229 revcount = max(revcount, 1)
229 revcount = max(revcount, 1)
230 tmpl.defaults['sessionvars']['revcount'] = revcount
230 tmpl.defaults['sessionvars']['revcount'] = revcount
231
231
232 lessvars = copy.copy(tmpl.defaults['sessionvars'])
232 lessvars = copy.copy(tmpl.defaults['sessionvars'])
233 lessvars['revcount'] = max(revcount / 2, 1)
233 lessvars['revcount'] = max(revcount / 2, 1)
234 morevars = copy.copy(tmpl.defaults['sessionvars'])
234 morevars = copy.copy(tmpl.defaults['sessionvars'])
235 morevars['revcount'] = revcount * 2
235 morevars['revcount'] = revcount * 2
236
236
237 count = len(web.repo)
237 count = len(web.repo)
238 pos = ctx.rev()
238 pos = ctx.rev()
239 start = max(0, pos - revcount + 1)
239 start = max(0, pos - revcount + 1)
240 end = min(count, start + revcount)
240 end = min(count, start + revcount)
241 pos = end - 1
241 pos = end - 1
242 parity = paritygen(web.stripecount, offset=start - end)
242 parity = paritygen(web.stripecount, offset=start - end)
243
243
244 changenav = webutil.revnavgen(pos, revcount, count, web.repo.changectx)
244 changenav = webutil.revnavgen(pos, revcount, count, web.repo.changectx)
245
245
246 return tmpl(shortlog and 'shortlog' or 'changelog', changenav=changenav,
246 return tmpl(shortlog and 'shortlog' or 'changelog', changenav=changenav,
247 node=ctx.hex(), rev=pos, changesets=count,
247 node=ctx.hex(), rev=pos, changesets=count,
248 entries=lambda **x: changelist(limit=0,**x),
248 entries=lambda **x: changelist(limit=0,**x),
249 latestentry=lambda **x: changelist(limit=1,**x),
249 latestentry=lambda **x: changelist(limit=1,**x),
250 archives=web.archivelist("tip"), revcount=revcount,
250 archives=web.archivelist("tip"), revcount=revcount,
251 morevars=morevars, lessvars=lessvars)
251 morevars=morevars, lessvars=lessvars)
252
252
253 def shortlog(web, req, tmpl):
253 def shortlog(web, req, tmpl):
254 return changelog(web, req, tmpl, shortlog = True)
254 return changelog(web, req, tmpl, shortlog = True)
255
255
256 def changeset(web, req, tmpl):
256 def changeset(web, req, tmpl):
257 ctx = webutil.changectx(web.repo, req)
257 ctx = webutil.changectx(web.repo, req)
258 basectx = webutil.basechangectx(web.repo, req)
258 basectx = webutil.basechangectx(web.repo, req)
259 if basectx is None:
259 if basectx is None:
260 basectx = ctx.p1()
260 basectx = ctx.p1()
261 showtags = webutil.showtag(web.repo, tmpl, 'changesettag', ctx.node())
261 showtags = webutil.showtag(web.repo, tmpl, 'changesettag', ctx.node())
262 showbookmarks = webutil.showbookmark(web.repo, tmpl, 'changesetbookmark',
262 showbookmarks = webutil.showbookmark(web.repo, tmpl, 'changesetbookmark',
263 ctx.node())
263 ctx.node())
264 showbranch = webutil.nodebranchnodefault(ctx)
264 showbranch = webutil.nodebranchnodefault(ctx)
265
265
266 files = []
266 files = []
267 parity = paritygen(web.stripecount)
267 parity = paritygen(web.stripecount)
268 for blockno, f in enumerate(ctx.files()):
268 for blockno, f in enumerate(ctx.files()):
269 template = f in ctx and 'filenodelink' or 'filenolink'
269 template = f in ctx and 'filenodelink' or 'filenolink'
270 files.append(tmpl(template,
270 files.append(tmpl(template,
271 node=ctx.hex(), file=f, blockno=blockno + 1,
271 node=ctx.hex(), file=f, blockno=blockno + 1,
272 parity=parity.next()))
272 parity=parity.next()))
273
273
274 style = web.config('web', 'style', 'paper')
274 style = web.config('web', 'style', 'paper')
275 if 'style' in req.form:
275 if 'style' in req.form:
276 style = req.form['style'][0]
276 style = req.form['style'][0]
277
277
278 parity = paritygen(web.stripecount)
278 parity = paritygen(web.stripecount)
279 diffs = webutil.diffs(web.repo, tmpl, ctx, basectx, None, parity, style)
279 diffs = webutil.diffs(web.repo, tmpl, ctx, basectx, None, parity, style)
280
280
281 parity = paritygen(web.stripecount)
281 parity = paritygen(web.stripecount)
282 diffstatgen = webutil.diffstatgen(ctx, basectx)
282 diffstatgen = webutil.diffstatgen(ctx, basectx)
283 diffstat = webutil.diffstat(tmpl, ctx, diffstatgen, parity)
283 diffstat = webutil.diffstat(tmpl, ctx, diffstatgen, parity)
284
284
285 return tmpl('changeset',
285 return tmpl('changeset',
286 diff=diffs,
286 diff=diffs,
287 rev=ctx.rev(),
287 rev=ctx.rev(),
288 node=ctx.hex(),
288 node=ctx.hex(),
289 parent=webutil.parents(ctx),
289 parent=webutil.parents(ctx),
290 child=webutil.children(ctx),
290 child=webutil.children(ctx),
291 currentbaseline=basectx.hex(),
291 currentbaseline=basectx.hex(),
292 changesettag=showtags,
292 changesettag=showtags,
293 changesetbookmark=showbookmarks,
293 changesetbookmark=showbookmarks,
294 changesetbranch=showbranch,
294 changesetbranch=showbranch,
295 author=ctx.user(),
295 author=ctx.user(),
296 desc=ctx.description(),
296 desc=ctx.description(),
297 date=ctx.date(),
297 date=ctx.date(),
298 files=files,
298 files=files,
299 diffsummary=lambda **x: webutil.diffsummary(diffstatgen),
299 diffsummary=lambda **x: webutil.diffsummary(diffstatgen),
300 diffstat=diffstat,
300 diffstat=diffstat,
301 archives=web.archivelist(ctx.hex()),
301 archives=web.archivelist(ctx.hex()),
302 tags=webutil.nodetagsdict(web.repo, ctx.node()),
302 tags=webutil.nodetagsdict(web.repo, ctx.node()),
303 bookmarks=webutil.nodebookmarksdict(web.repo, ctx.node()),
303 bookmarks=webutil.nodebookmarksdict(web.repo, ctx.node()),
304 branch=webutil.nodebranchnodefault(ctx),
304 branch=webutil.nodebranchnodefault(ctx),
305 inbranch=webutil.nodeinbranch(web.repo, ctx),
305 inbranch=webutil.nodeinbranch(web.repo, ctx),
306 branches=webutil.nodebranchdict(web.repo, ctx))
306 branches=webutil.nodebranchdict(web.repo, ctx))
307
307
308 rev = changeset
308 rev = changeset
309
309
310 def decodepath(path):
310 def decodepath(path):
311 """Hook for mapping a path in the repository to a path in the
311 """Hook for mapping a path in the repository to a path in the
312 working copy.
312 working copy.
313
313
314 Extensions (e.g., largefiles) can override this to remap files in
314 Extensions (e.g., largefiles) can override this to remap files in
315 the virtual file system presented by the manifest command below."""
315 the virtual file system presented by the manifest command below."""
316 return path
316 return path
317
317
318 def manifest(web, req, tmpl):
318 def manifest(web, req, tmpl):
319 ctx = webutil.changectx(web.repo, req)
319 ctx = webutil.changectx(web.repo, req)
320 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
320 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
321 mf = ctx.manifest()
321 mf = ctx.manifest()
322 node = ctx.node()
322 node = ctx.node()
323
323
324 files = {}
324 files = {}
325 dirs = {}
325 dirs = {}
326 parity = paritygen(web.stripecount)
326 parity = paritygen(web.stripecount)
327
327
328 if path and path[-1] != "/":
328 if path and path[-1] != "/":
329 path += "/"
329 path += "/"
330 l = len(path)
330 l = len(path)
331 abspath = "/" + path
331 abspath = "/" + path
332
332
333 for full, n in mf.iteritems():
333 for full, n in mf.iteritems():
334 # the virtual path (working copy path) used for the full
334 # the virtual path (working copy path) used for the full
335 # (repository) path
335 # (repository) path
336 f = decodepath(full)
336 f = decodepath(full)
337
337
338 if f[:l] != path:
338 if f[:l] != path:
339 continue
339 continue
340 remain = f[l:]
340 remain = f[l:]
341 elements = remain.split('/')
341 elements = remain.split('/')
342 if len(elements) == 1:
342 if len(elements) == 1:
343 files[remain] = full
343 files[remain] = full
344 else:
344 else:
345 h = dirs # need to retain ref to dirs (root)
345 h = dirs # need to retain ref to dirs (root)
346 for elem in elements[0:-1]:
346 for elem in elements[0:-1]:
347 if elem not in h:
347 if elem not in h:
348 h[elem] = {}
348 h[elem] = {}
349 h = h[elem]
349 h = h[elem]
350 if len(h) > 1:
350 if len(h) > 1:
351 break
351 break
352 h[None] = None # denotes files present
352 h[None] = None # denotes files present
353
353
354 if mf and not files and not dirs:
354 if mf and not files and not dirs:
355 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
355 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
356
356
357 def filelist(**map):
357 def filelist(**map):
358 for f in sorted(files):
358 for f in sorted(files):
359 full = files[f]
359 full = files[f]
360
360
361 fctx = ctx.filectx(full)
361 fctx = ctx.filectx(full)
362 yield {"file": full,
362 yield {"file": full,
363 "parity": parity.next(),
363 "parity": parity.next(),
364 "basename": f,
364 "basename": f,
365 "date": fctx.date(),
365 "date": fctx.date(),
366 "size": fctx.size(),
366 "size": fctx.size(),
367 "permissions": mf.flags(full)}
367 "permissions": mf.flags(full)}
368
368
369 def dirlist(**map):
369 def dirlist(**map):
370 for d in sorted(dirs):
370 for d in sorted(dirs):
371
371
372 emptydirs = []
372 emptydirs = []
373 h = dirs[d]
373 h = dirs[d]
374 while isinstance(h, dict) and len(h) == 1:
374 while isinstance(h, dict) and len(h) == 1:
375 k, v = h.items()[0]
375 k, v = h.items()[0]
376 if v:
376 if v:
377 emptydirs.append(k)
377 emptydirs.append(k)
378 h = v
378 h = v
379
379
380 path = "%s%s" % (abspath, d)
380 path = "%s%s" % (abspath, d)
381 yield {"parity": parity.next(),
381 yield {"parity": parity.next(),
382 "path": path,
382 "path": path,
383 "emptydirs": "/".join(emptydirs),
383 "emptydirs": "/".join(emptydirs),
384 "basename": d}
384 "basename": d}
385
385
386 return tmpl("manifest",
386 return tmpl("manifest",
387 rev=ctx.rev(),
387 rev=ctx.rev(),
388 node=hex(node),
388 node=hex(node),
389 path=abspath,
389 path=abspath,
390 up=webutil.up(abspath),
390 up=webutil.up(abspath),
391 upparity=parity.next(),
391 upparity=parity.next(),
392 fentries=filelist,
392 fentries=filelist,
393 dentries=dirlist,
393 dentries=dirlist,
394 archives=web.archivelist(hex(node)),
394 archives=web.archivelist(hex(node)),
395 tags=webutil.nodetagsdict(web.repo, node),
395 tags=webutil.nodetagsdict(web.repo, node),
396 bookmarks=webutil.nodebookmarksdict(web.repo, node),
396 bookmarks=webutil.nodebookmarksdict(web.repo, node),
397 inbranch=webutil.nodeinbranch(web.repo, ctx),
397 inbranch=webutil.nodeinbranch(web.repo, ctx),
398 branches=webutil.nodebranchdict(web.repo, ctx))
398 branches=webutil.nodebranchdict(web.repo, ctx))
399
399
400 def tags(web, req, tmpl):
400 def tags(web, req, tmpl):
401 i = list(reversed(web.repo.tagslist()))
401 i = list(reversed(web.repo.tagslist()))
402 parity = paritygen(web.stripecount)
402 parity = paritygen(web.stripecount)
403
403
404 def entries(notip=False, limit=0, **map):
404 def entries(notip=False, limit=0, **map):
405 count = 0
405 count = 0
406 for k, n in i:
406 for k, n in i:
407 if notip and k == "tip":
407 if notip and k == "tip":
408 continue
408 continue
409 if limit > 0 and count >= limit:
409 if limit > 0 and count >= limit:
410 continue
410 continue
411 count = count + 1
411 count = count + 1
412 yield {"parity": parity.next(),
412 yield {"parity": parity.next(),
413 "tag": k,
413 "tag": k,
414 "date": web.repo[n].date(),
414 "date": web.repo[n].date(),
415 "node": hex(n)}
415 "node": hex(n)}
416
416
417 return tmpl("tags",
417 return tmpl("tags",
418 node=hex(web.repo.changelog.tip()),
418 node=hex(web.repo.changelog.tip()),
419 entries=lambda **x: entries(False, 0, **x),
419 entries=lambda **x: entries(False, 0, **x),
420 entriesnotip=lambda **x: entries(True, 0, **x),
420 entriesnotip=lambda **x: entries(True, 0, **x),
421 latestentry=lambda **x: entries(True, 1, **x))
421 latestentry=lambda **x: entries(True, 1, **x))
422
422
423 def bookmarks(web, req, tmpl):
423 def bookmarks(web, req, tmpl):
424 i = web.repo._bookmarks.items()
424 i = web.repo._bookmarks.items()
425 parity = paritygen(web.stripecount)
425 parity = paritygen(web.stripecount)
426
426
427 def entries(limit=0, **map):
427 def entries(limit=0, **map):
428 count = 0
428 count = 0
429 for k, n in sorted(i):
429 for k, n in sorted(i):
430 if limit > 0 and count >= limit:
430 if limit > 0 and count >= limit:
431 continue
431 continue
432 count = count + 1
432 count = count + 1
433 yield {"parity": parity.next(),
433 yield {"parity": parity.next(),
434 "bookmark": k,
434 "bookmark": k,
435 "date": web.repo[n].date(),
435 "date": web.repo[n].date(),
436 "node": hex(n)}
436 "node": hex(n)}
437
437
438 return tmpl("bookmarks",
438 return tmpl("bookmarks",
439 node=hex(web.repo.changelog.tip()),
439 node=hex(web.repo.changelog.tip()),
440 entries=lambda **x: entries(0, **x),
440 entries=lambda **x: entries(0, **x),
441 latestentry=lambda **x: entries(1, **x))
441 latestentry=lambda **x: entries(1, **x))
442
442
443 def branches(web, req, tmpl):
443 def branches(web, req, tmpl):
444 tips = []
444 tips = []
445 heads = web.repo.heads()
445 heads = web.repo.heads()
446 parity = paritygen(web.stripecount)
446 parity = paritygen(web.stripecount)
447 sortkey = lambda ctx: (not ctx.closesbranch(), ctx.rev())
447 sortkey = lambda ctx: (not ctx.closesbranch(), ctx.rev())
448
448
449 def entries(limit, **map):
449 def entries(limit, **map):
450 count = 0
450 count = 0
451 if not tips:
451 if not tips:
452 for t, n in web.repo.branchtags().iteritems():
452 for t, n in web.repo.branchtags().iteritems():
453 tips.append(web.repo[n])
453 tips.append(web.repo[n])
454 for ctx in sorted(tips, key=sortkey, reverse=True):
454 for ctx in sorted(tips, key=sortkey, reverse=True):
455 if limit > 0 and count >= limit:
455 if limit > 0 and count >= limit:
456 return
456 return
457 count += 1
457 count += 1
458 if not web.repo.branchheads(ctx.branch()):
458 if not web.repo.branchheads(ctx.branch()):
459 status = 'closed'
459 status = 'closed'
460 elif ctx.node() not in heads:
460 elif ctx.node() not in heads:
461 status = 'inactive'
461 status = 'inactive'
462 else:
462 else:
463 status = 'open'
463 status = 'open'
464 yield {'parity': parity.next(),
464 yield {'parity': parity.next(),
465 'branch': ctx.branch(),
465 'branch': ctx.branch(),
466 'status': status,
466 'status': status,
467 'node': ctx.hex(),
467 'node': ctx.hex(),
468 'date': ctx.date()}
468 'date': ctx.date()}
469
469
470 return tmpl('branches', node=hex(web.repo.changelog.tip()),
470 return tmpl('branches', node=hex(web.repo.changelog.tip()),
471 entries=lambda **x: entries(0, **x),
471 entries=lambda **x: entries(0, **x),
472 latestentry=lambda **x: entries(1, **x))
472 latestentry=lambda **x: entries(1, **x))
473
473
474 def summary(web, req, tmpl):
474 def summary(web, req, tmpl):
475 i = reversed(web.repo.tagslist())
475 i = reversed(web.repo.tagslist())
476
476
477 def tagentries(**map):
477 def tagentries(**map):
478 parity = paritygen(web.stripecount)
478 parity = paritygen(web.stripecount)
479 count = 0
479 count = 0
480 for k, n in i:
480 for k, n in i:
481 if k == "tip": # skip tip
481 if k == "tip": # skip tip
482 continue
482 continue
483
483
484 count += 1
484 count += 1
485 if count > 10: # limit to 10 tags
485 if count > 10: # limit to 10 tags
486 break
486 break
487
487
488 yield tmpl("tagentry",
488 yield tmpl("tagentry",
489 parity=parity.next(),
489 parity=parity.next(),
490 tag=k,
490 tag=k,
491 node=hex(n),
491 node=hex(n),
492 date=web.repo[n].date())
492 date=web.repo[n].date())
493
493
494 def bookmarks(**map):
494 def bookmarks(**map):
495 parity = paritygen(web.stripecount)
495 parity = paritygen(web.stripecount)
496 b = web.repo._bookmarks.items()
496 b = web.repo._bookmarks.items()
497 for k, n in sorted(b)[:10]: # limit to 10 bookmarks
497 for k, n in sorted(b)[:10]: # limit to 10 bookmarks
498 yield {'parity': parity.next(),
498 yield {'parity': parity.next(),
499 'bookmark': k,
499 'bookmark': k,
500 'date': web.repo[n].date(),
500 'date': web.repo[n].date(),
501 'node': hex(n)}
501 'node': hex(n)}
502
502
503 def branches(**map):
503 def branches(**map):
504 parity = paritygen(web.stripecount)
504 parity = paritygen(web.stripecount)
505
505
506 b = web.repo.branchtags()
506 b = web.repo.branchtags()
507 l = [(-web.repo.changelog.rev(n), n, t) for t, n in b.iteritems()]
507 l = [(-web.repo.changelog.rev(n), n, t) for t, n in b.iteritems()]
508 for r, n, t in sorted(l):
508 for r, n, t in sorted(l):
509 yield {'parity': parity.next(),
509 yield {'parity': parity.next(),
510 'branch': t,
510 'branch': t,
511 'node': hex(n),
511 'node': hex(n),
512 'date': web.repo[n].date()}
512 'date': web.repo[n].date()}
513
513
514 def changelist(**map):
514 def changelist(**map):
515 parity = paritygen(web.stripecount, offset=start - end)
515 parity = paritygen(web.stripecount, offset=start - end)
516 l = [] # build a list in forward order for efficiency
516 l = [] # build a list in forward order for efficiency
517 for i in xrange(start, end):
517 for i in xrange(start, end):
518 ctx = web.repo[i]
518 ctx = web.repo[i]
519 n = ctx.node()
519 n = ctx.node()
520 hn = hex(n)
520 hn = hex(n)
521
521
522 l.append(tmpl(
522 l.append(tmpl(
523 'shortlogentry',
523 'shortlogentry',
524 parity=parity.next(),
524 parity=parity.next(),
525 author=ctx.user(),
525 author=ctx.user(),
526 desc=ctx.description(),
526 desc=ctx.description(),
527 date=ctx.date(),
527 date=ctx.date(),
528 rev=i,
528 rev=i,
529 node=hn,
529 node=hn,
530 tags=webutil.nodetagsdict(web.repo, n),
530 tags=webutil.nodetagsdict(web.repo, n),
531 bookmarks=webutil.nodebookmarksdict(web.repo, n),
531 bookmarks=webutil.nodebookmarksdict(web.repo, n),
532 inbranch=webutil.nodeinbranch(web.repo, ctx),
532 inbranch=webutil.nodeinbranch(web.repo, ctx),
533 branches=webutil.nodebranchdict(web.repo, ctx)))
533 branches=webutil.nodebranchdict(web.repo, ctx)))
534
534
535 l.reverse()
535 l.reverse()
536 yield l
536 yield l
537
537
538 tip = web.repo['tip']
538 tip = web.repo['tip']
539 count = len(web.repo)
539 count = len(web.repo)
540 start = max(0, count - web.maxchanges)
540 start = max(0, count - web.maxchanges)
541 end = min(count, start + web.maxchanges)
541 end = min(count, start + web.maxchanges)
542
542
543 return tmpl("summary",
543 return tmpl("summary",
544 desc=web.config("web", "description", "unknown"),
544 desc=web.config("web", "description", "unknown"),
545 owner=get_contact(web.config) or "unknown",
545 owner=get_contact(web.config) or "unknown",
546 lastchange=tip.date(),
546 lastchange=tip.date(),
547 tags=tagentries,
547 tags=tagentries,
548 bookmarks=bookmarks,
548 bookmarks=bookmarks,
549 branches=branches,
549 branches=branches,
550 shortlog=changelist,
550 shortlog=changelist,
551 node=tip.hex(),
551 node=tip.hex(),
552 archives=web.archivelist("tip"))
552 archives=web.archivelist("tip"))
553
553
554 def filediff(web, req, tmpl):
554 def filediff(web, req, tmpl):
555 fctx, ctx = None, None
555 fctx, ctx = None, None
556 try:
556 try:
557 fctx = webutil.filectx(web.repo, req)
557 fctx = webutil.filectx(web.repo, req)
558 except LookupError:
558 except LookupError:
559 ctx = webutil.changectx(web.repo, req)
559 ctx = webutil.changectx(web.repo, req)
560 path = webutil.cleanpath(web.repo, req.form['file'][0])
560 path = webutil.cleanpath(web.repo, req.form['file'][0])
561 if path not in ctx.files():
561 if path not in ctx.files():
562 raise
562 raise
563
563
564 if fctx is not None:
564 if fctx is not None:
565 n = fctx.node()
565 n = fctx.node()
566 path = fctx.path()
566 path = fctx.path()
567 ctx = fctx.changectx()
567 ctx = fctx.changectx()
568 else:
568 else:
569 n = ctx.node()
569 n = ctx.node()
570 # path already defined in except clause
570 # path already defined in except clause
571
571
572 parity = paritygen(web.stripecount)
572 parity = paritygen(web.stripecount)
573 style = web.config('web', 'style', 'paper')
573 style = web.config('web', 'style', 'paper')
574 if 'style' in req.form:
574 if 'style' in req.form:
575 style = req.form['style'][0]
575 style = req.form['style'][0]
576
576
577 diffs = webutil.diffs(web.repo, tmpl, ctx, None, [path], parity, style)
577 diffs = webutil.diffs(web.repo, tmpl, ctx, None, [path], parity, style)
578 rename = fctx and webutil.renamelink(fctx) or []
578 rename = fctx and webutil.renamelink(fctx) or []
579 ctx = fctx and fctx or ctx
579 ctx = fctx and fctx or ctx
580 return tmpl("filediff",
580 return tmpl("filediff",
581 file=path,
581 file=path,
582 node=hex(n),
582 node=hex(n),
583 rev=ctx.rev(),
583 rev=ctx.rev(),
584 date=ctx.date(),
584 date=ctx.date(),
585 desc=ctx.description(),
585 desc=ctx.description(),
586 author=ctx.user(),
586 author=ctx.user(),
587 rename=rename,
587 rename=rename,
588 branch=webutil.nodebranchnodefault(ctx),
588 branch=webutil.nodebranchnodefault(ctx),
589 parent=webutil.parents(ctx),
589 parent=webutil.parents(ctx),
590 child=webutil.children(ctx),
590 child=webutil.children(ctx),
591 diff=diffs)
591 diff=diffs)
592
592
593 diff = filediff
593 diff = filediff
594
594
595 def comparison(web, req, tmpl):
595 def comparison(web, req, tmpl):
596 ctx = webutil.changectx(web.repo, req)
596 ctx = webutil.changectx(web.repo, req)
597 if 'file' not in req.form:
597 if 'file' not in req.form:
598 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
598 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
599 path = webutil.cleanpath(web.repo, req.form['file'][0])
599 path = webutil.cleanpath(web.repo, req.form['file'][0])
600 rename = path in ctx and webutil.renamelink(ctx[path]) or []
600 rename = path in ctx and webutil.renamelink(ctx[path]) or []
601
601
602 parsecontext = lambda v: v == 'full' and -1 or int(v)
602 parsecontext = lambda v: v == 'full' and -1 or int(v)
603 if 'context' in req.form:
603 if 'context' in req.form:
604 context = parsecontext(req.form['context'][0])
604 context = parsecontext(req.form['context'][0])
605 else:
605 else:
606 context = parsecontext(web.config('web', 'comparisoncontext', '5'))
606 context = parsecontext(web.config('web', 'comparisoncontext', '5'))
607
607
608 def filelines(f):
608 def filelines(f):
609 if binary(f.data()):
609 if binary(f.data()):
610 mt = mimetypes.guess_type(f.path())[0]
610 mt = mimetypes.guess_type(f.path())[0]
611 if not mt:
611 if not mt:
612 mt = 'application/octet-stream'
612 mt = 'application/octet-stream'
613 return [_('(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
613 return [_('(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
614 return f.data().splitlines()
614 return f.data().splitlines()
615
615
616 if path in ctx:
616 if path in ctx:
617 fctx = ctx[path]
617 fctx = ctx[path]
618 rightrev = fctx.filerev()
618 rightrev = fctx.filerev()
619 rightnode = fctx.filenode()
619 rightnode = fctx.filenode()
620 rightlines = filelines(fctx)
620 rightlines = filelines(fctx)
621 parents = fctx.parents()
621 parents = fctx.parents()
622 if not parents:
622 if not parents:
623 leftrev = -1
623 leftrev = -1
624 leftnode = nullid
624 leftnode = nullid
625 leftlines = ()
625 leftlines = ()
626 else:
626 else:
627 pfctx = parents[0]
627 pfctx = parents[0]
628 leftrev = pfctx.filerev()
628 leftrev = pfctx.filerev()
629 leftnode = pfctx.filenode()
629 leftnode = pfctx.filenode()
630 leftlines = filelines(pfctx)
630 leftlines = filelines(pfctx)
631 else:
631 else:
632 rightrev = -1
632 rightrev = -1
633 rightnode = nullid
633 rightnode = nullid
634 rightlines = ()
634 rightlines = ()
635 fctx = ctx.parents()[0][path]
635 fctx = ctx.parents()[0][path]
636 leftrev = fctx.filerev()
636 leftrev = fctx.filerev()
637 leftnode = fctx.filenode()
637 leftnode = fctx.filenode()
638 leftlines = filelines(fctx)
638 leftlines = filelines(fctx)
639
639
640 comparison = webutil.compare(tmpl, context, leftlines, rightlines)
640 comparison = webutil.compare(tmpl, context, leftlines, rightlines)
641 return tmpl('filecomparison',
641 return tmpl('filecomparison',
642 file=path,
642 file=path,
643 node=hex(ctx.node()),
643 node=hex(ctx.node()),
644 rev=ctx.rev(),
644 rev=ctx.rev(),
645 date=ctx.date(),
645 date=ctx.date(),
646 desc=ctx.description(),
646 desc=ctx.description(),
647 author=ctx.user(),
647 author=ctx.user(),
648 rename=rename,
648 rename=rename,
649 branch=webutil.nodebranchnodefault(ctx),
649 branch=webutil.nodebranchnodefault(ctx),
650 parent=webutil.parents(fctx),
650 parent=webutil.parents(fctx),
651 child=webutil.children(fctx),
651 child=webutil.children(fctx),
652 leftrev=leftrev,
652 leftrev=leftrev,
653 leftnode=hex(leftnode),
653 leftnode=hex(leftnode),
654 rightrev=rightrev,
654 rightrev=rightrev,
655 rightnode=hex(rightnode),
655 rightnode=hex(rightnode),
656 comparison=comparison)
656 comparison=comparison)
657
657
658 def annotate(web, req, tmpl):
658 def annotate(web, req, tmpl):
659 fctx = webutil.filectx(web.repo, req)
659 fctx = webutil.filectx(web.repo, req)
660 f = fctx.path()
660 f = fctx.path()
661 parity = paritygen(web.stripecount)
661 parity = paritygen(web.stripecount)
662 diffopts = patch.diffopts(web.repo.ui, untrusted=True, section='annotate')
662 diffopts = patch.diffopts(web.repo.ui, untrusted=True, section='annotate')
663
663
664 def annotate(**map):
664 def annotate(**map):
665 last = None
665 last = None
666 if binary(fctx.data()):
666 if binary(fctx.data()):
667 mt = (mimetypes.guess_type(fctx.path())[0]
667 mt = (mimetypes.guess_type(fctx.path())[0]
668 or 'application/octet-stream')
668 or 'application/octet-stream')
669 lines = enumerate([((fctx.filectx(fctx.filerev()), 1),
669 lines = enumerate([((fctx.filectx(fctx.filerev()), 1),
670 '(binary:%s)' % mt)])
670 '(binary:%s)' % mt)])
671 else:
671 else:
672 lines = enumerate(fctx.annotate(follow=True, linenumber=True,
672 lines = enumerate(fctx.annotate(follow=True, linenumber=True,
673 diffopts=diffopts))
673 diffopts=diffopts))
674 for lineno, ((f, targetline), l) in lines:
674 for lineno, ((f, targetline), l) in lines:
675 fnode = f.filenode()
675 fnode = f.filenode()
676
676
677 if last != fnode:
677 if last != fnode:
678 last = fnode
678 last = fnode
679
679
680 yield {"parity": parity.next(),
680 yield {"parity": parity.next(),
681 "node": f.hex(),
681 "node": f.hex(),
682 "rev": f.rev(),
682 "rev": f.rev(),
683 "author": f.user(),
683 "author": f.user(),
684 "desc": f.description(),
684 "desc": f.description(),
685 "file": f.path(),
685 "file": f.path(),
686 "targetline": targetline,
686 "targetline": targetline,
687 "line": l,
687 "line": l,
688 "lineid": "l%d" % (lineno + 1),
688 "lineid": "l%d" % (lineno + 1),
689 "linenumber": "% 6d" % (lineno + 1),
689 "linenumber": "% 6d" % (lineno + 1),
690 "revdate": f.date()}
690 "revdate": f.date()}
691
691
692 return tmpl("fileannotate",
692 return tmpl("fileannotate",
693 file=f,
693 file=f,
694 annotate=annotate,
694 annotate=annotate,
695 path=webutil.up(f),
695 path=webutil.up(f),
696 rev=fctx.rev(),
696 rev=fctx.rev(),
697 node=fctx.hex(),
697 node=fctx.hex(),
698 author=fctx.user(),
698 author=fctx.user(),
699 date=fctx.date(),
699 date=fctx.date(),
700 desc=fctx.description(),
700 desc=fctx.description(),
701 rename=webutil.renamelink(fctx),
701 rename=webutil.renamelink(fctx),
702 branch=webutil.nodebranchnodefault(fctx),
702 branch=webutil.nodebranchnodefault(fctx),
703 parent=webutil.parents(fctx),
703 parent=webutil.parents(fctx),
704 child=webutil.children(fctx),
704 child=webutil.children(fctx),
705 permissions=fctx.manifest().flags(f))
705 permissions=fctx.manifest().flags(f))
706
706
707 def filelog(web, req, tmpl):
707 def filelog(web, req, tmpl):
708
708
709 try:
709 try:
710 fctx = webutil.filectx(web.repo, req)
710 fctx = webutil.filectx(web.repo, req)
711 f = fctx.path()
711 f = fctx.path()
712 fl = fctx.filelog()
712 fl = fctx.filelog()
713 except error.LookupError:
713 except error.LookupError:
714 f = webutil.cleanpath(web.repo, req.form['file'][0])
714 f = webutil.cleanpath(web.repo, req.form['file'][0])
715 fl = web.repo.file(f)
715 fl = web.repo.file(f)
716 numrevs = len(fl)
716 numrevs = len(fl)
717 if not numrevs: # file doesn't exist at all
717 if not numrevs: # file doesn't exist at all
718 raise
718 raise
719 rev = webutil.changectx(web.repo, req).rev()
719 rev = webutil.changectx(web.repo, req).rev()
720 first = fl.linkrev(0)
720 first = fl.linkrev(0)
721 if rev < first: # current rev is from before file existed
721 if rev < first: # current rev is from before file existed
722 raise
722 raise
723 frev = numrevs - 1
723 frev = numrevs - 1
724 while fl.linkrev(frev) > rev:
724 while fl.linkrev(frev) > rev:
725 frev -= 1
725 frev -= 1
726 fctx = web.repo.filectx(f, fl.linkrev(frev))
726 fctx = web.repo.filectx(f, fl.linkrev(frev))
727
727
728 revcount = web.maxshortchanges
728 revcount = web.maxshortchanges
729 if 'revcount' in req.form:
729 if 'revcount' in req.form:
730 revcount = int(req.form.get('revcount', [revcount])[0])
730 revcount = int(req.form.get('revcount', [revcount])[0])
731 revcount = max(revcount, 1)
731 revcount = max(revcount, 1)
732 tmpl.defaults['sessionvars']['revcount'] = revcount
732 tmpl.defaults['sessionvars']['revcount'] = revcount
733
733
734 lessvars = copy.copy(tmpl.defaults['sessionvars'])
734 lessvars = copy.copy(tmpl.defaults['sessionvars'])
735 lessvars['revcount'] = max(revcount / 2, 1)
735 lessvars['revcount'] = max(revcount / 2, 1)
736 morevars = copy.copy(tmpl.defaults['sessionvars'])
736 morevars = copy.copy(tmpl.defaults['sessionvars'])
737 morevars['revcount'] = revcount * 2
737 morevars['revcount'] = revcount * 2
738
738
739 count = fctx.filerev() + 1
739 count = fctx.filerev() + 1
740 start = max(0, fctx.filerev() - revcount + 1) # first rev on this page
740 start = max(0, fctx.filerev() - revcount + 1) # first rev on this page
741 end = min(count, start + revcount) # last rev on this page
741 end = min(count, start + revcount) # last rev on this page
742 parity = paritygen(web.stripecount, offset=start - end)
742 parity = paritygen(web.stripecount, offset=start - end)
743
743
744 def entries(limit=0, **map):
744 def entries(limit=0, **map):
745 l = []
745 l = []
746
746
747 repo = web.repo
747 repo = web.repo
748 for i in xrange(start, end):
748 for i in xrange(start, end):
749 iterfctx = fctx.filectx(i)
749 iterfctx = fctx.filectx(i)
750
750
751 l.append({"parity": parity.next(),
751 l.append({"parity": parity.next(),
752 "filerev": i,
752 "filerev": i,
753 "file": f,
753 "file": f,
754 "node": iterfctx.hex(),
754 "node": iterfctx.hex(),
755 "author": iterfctx.user(),
755 "author": iterfctx.user(),
756 "date": iterfctx.date(),
756 "date": iterfctx.date(),
757 "rename": webutil.renamelink(iterfctx),
757 "rename": webutil.renamelink(iterfctx),
758 "parent": webutil.parents(iterfctx),
758 "parent": webutil.parents(iterfctx),
759 "child": webutil.children(iterfctx),
759 "child": webutil.children(iterfctx),
760 "desc": iterfctx.description(),
760 "desc": iterfctx.description(),
761 "tags": webutil.nodetagsdict(repo, iterfctx.node()),
761 "tags": webutil.nodetagsdict(repo, iterfctx.node()),
762 "bookmarks": webutil.nodebookmarksdict(
762 "bookmarks": webutil.nodebookmarksdict(
763 repo, iterfctx.node()),
763 repo, iterfctx.node()),
764 "branch": webutil.nodebranchnodefault(iterfctx),
764 "branch": webutil.nodebranchnodefault(iterfctx),
765 "inbranch": webutil.nodeinbranch(repo, iterfctx),
765 "inbranch": webutil.nodeinbranch(repo, iterfctx),
766 "branches": webutil.nodebranchdict(repo, iterfctx)})
766 "branches": webutil.nodebranchdict(repo, iterfctx)})
767
767
768 if limit > 0:
768 if limit > 0:
769 l = l[-limit:]
769 l = l[-limit:]
770
770
771 for e in reversed(l):
771 for e in reversed(l):
772 yield e
772 yield e
773
773
774 nodefunc = lambda x: fctx.filectx(fileid=x)
774 nodefunc = lambda x: fctx.filectx(fileid=x)
775 nav = webutil.revnavgen(end - 1, revcount, count, nodefunc)
775 nav = webutil.revnavgen(end - 1, revcount, count, nodefunc)
776 return tmpl("filelog", file=f, node=fctx.hex(), nav=nav,
776 return tmpl("filelog", file=f, node=fctx.hex(), nav=nav,
777 entries=lambda **x: entries(limit=0, **x),
777 entries=lambda **x: entries(limit=0, **x),
778 latestentry=lambda **x: entries(limit=1, **x),
778 latestentry=lambda **x: entries(limit=1, **x),
779 revcount=revcount, morevars=morevars, lessvars=lessvars)
779 revcount=revcount, morevars=morevars, lessvars=lessvars)
780
780
781 def archive(web, req, tmpl):
781 def archive(web, req, tmpl):
782 type_ = req.form.get('type', [None])[0]
782 type_ = req.form.get('type', [None])[0]
783 allowed = web.configlist("web", "allow_archive")
783 allowed = web.configlist("web", "allow_archive")
784 key = req.form['node'][0]
784 key = req.form['node'][0]
785
785
786 if type_ not in web.archives:
786 if type_ not in web.archives:
787 msg = 'Unsupported archive type: %s' % type_
787 msg = 'Unsupported archive type: %s' % type_
788 raise ErrorResponse(HTTP_NOT_FOUND, msg)
788 raise ErrorResponse(HTTP_NOT_FOUND, msg)
789
789
790 if not ((type_ in allowed or
790 if not ((type_ in allowed or
791 web.configbool("web", "allow" + type_, False))):
791 web.configbool("web", "allow" + type_, False))):
792 msg = 'Archive type not allowed: %s' % type_
792 msg = 'Archive type not allowed: %s' % type_
793 raise ErrorResponse(HTTP_FORBIDDEN, msg)
793 raise ErrorResponse(HTTP_FORBIDDEN, msg)
794
794
795 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
795 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
796 cnode = web.repo.lookup(key)
796 cnode = web.repo.lookup(key)
797 arch_version = key
797 arch_version = key
798 if cnode == key or key == 'tip':
798 if cnode == key or key == 'tip':
799 arch_version = short(cnode)
799 arch_version = short(cnode)
800 name = "%s-%s" % (reponame, arch_version)
800 name = "%s-%s" % (reponame, arch_version)
801 mimetype, artype, extension, encoding = web.archive_specs[type_]
801 mimetype, artype, extension, encoding = web.archive_specs[type_]
802 headers = [
802 headers = [
803 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
803 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
804 ]
804 ]
805 if encoding:
805 if encoding:
806 headers.append(('Content-Encoding', encoding))
806 headers.append(('Content-Encoding', encoding))
807 req.headers.extend(headers)
807 req.headers.extend(headers)
808 req.respond(HTTP_OK, mimetype)
808 req.respond(HTTP_OK, mimetype)
809
809
810 ctx = webutil.changectx(web.repo, req)
810 ctx = webutil.changectx(web.repo, req)
811 archival.archive(web.repo, req, cnode, artype, prefix=name,
811 archival.archive(web.repo, req, cnode, artype, prefix=name,
812 matchfn=scmutil.match(ctx, []),
812 matchfn=scmutil.match(ctx, []),
813 subrepos=web.configbool("web", "archivesubrepos"))
813 subrepos=web.configbool("web", "archivesubrepos"))
814 return []
814 return []
815
815
816
816
817 def static(web, req, tmpl):
817 def static(web, req, tmpl):
818 fname = req.form['file'][0]
818 fname = req.form['file'][0]
819 # a repo owner may set web.static in .hg/hgrc to get any file
819 # a repo owner may set web.static in .hg/hgrc to get any file
820 # readable by the user running the CGI script
820 # readable by the user running the CGI script
821 static = web.config("web", "static", None, untrusted=False)
821 static = web.config("web", "static", None, untrusted=False)
822 if not static:
822 if not static:
823 tp = web.templatepath or templater.templatepath()
823 tp = web.templatepath or templater.templatepath()
824 if isinstance(tp, str):
824 if isinstance(tp, str):
825 tp = [tp]
825 tp = [tp]
826 static = [os.path.join(p, 'static') for p in tp]
826 static = [os.path.join(p, 'static') for p in tp]
827 return [staticfile(static, fname, req)]
827 return [staticfile(static, fname, req)]
828
828
829 def graph(web, req, tmpl):
829 def graph(web, req, tmpl):
830
830
831 ctx = webutil.changectx(web.repo, req)
831 ctx = webutil.changectx(web.repo, req)
832 rev = ctx.rev()
832 rev = ctx.rev()
833
833
834 bg_height = 39
834 bg_height = 39
835 revcount = web.maxshortchanges
835 revcount = web.maxshortchanges
836 if 'revcount' in req.form:
836 if 'revcount' in req.form:
837 revcount = int(req.form.get('revcount', [revcount])[0])
837 revcount = int(req.form.get('revcount', [revcount])[0])
838 revcount = max(revcount, 1)
838 revcount = max(revcount, 1)
839 tmpl.defaults['sessionvars']['revcount'] = revcount
839 tmpl.defaults['sessionvars']['revcount'] = revcount
840
840
841 lessvars = copy.copy(tmpl.defaults['sessionvars'])
841 lessvars = copy.copy(tmpl.defaults['sessionvars'])
842 lessvars['revcount'] = max(revcount / 2, 1)
842 lessvars['revcount'] = max(revcount / 2, 1)
843 morevars = copy.copy(tmpl.defaults['sessionvars'])
843 morevars = copy.copy(tmpl.defaults['sessionvars'])
844 morevars['revcount'] = revcount * 2
844 morevars['revcount'] = revcount * 2
845
845
846 count = len(web.repo)
846 count = len(web.repo)
847 pos = rev
847 pos = rev
848 start = max(0, pos - revcount + 1)
848 start = max(0, pos - revcount + 1)
849 end = min(count, start + revcount)
849 end = min(count, start + revcount)
850 pos = end - 1
850 pos = end - 1
851
851
852 uprev = min(max(0, count - 1), rev + revcount)
852 uprev = min(max(0, count - 1), rev + revcount)
853 downrev = max(0, rev - revcount)
853 downrev = max(0, rev - revcount)
854 changenav = webutil.revnavgen(pos, revcount, count, web.repo.changectx)
854 changenav = webutil.revnavgen(pos, revcount, count, web.repo.changectx)
855
855
856 dag = graphmod.dagwalker(web.repo, range(start, end)[::-1])
856 dag = graphmod.dagwalker(web.repo, range(start, end)[::-1])
857 tree = list(graphmod.colored(dag, web.repo))
857 tree = list(graphmod.colored(dag, web.repo))
858
858
859 def getcolumns(tree):
859 def getcolumns(tree):
860 cols = 0
860 cols = 0
861 for (id, type, ctx, vtx, edges) in tree:
861 for (id, type, ctx, vtx, edges) in tree:
862 if type != graphmod.CHANGESET:
862 if type != graphmod.CHANGESET:
863 continue
863 continue
864 cols = max(cols, max([edge[0] for edge in edges] or [0]),
864 cols = max(cols, max([edge[0] for edge in edges] or [0]),
865 max([edge[1] for edge in edges] or [0]))
865 max([edge[1] for edge in edges] or [0]))
866 return cols
866 return cols
867
867
868 def graphdata(usetuples, **map):
868 def graphdata(usetuples, **map):
869 data = []
869 data = []
870
870
871 row = 0
871 row = 0
872 for (id, type, ctx, vtx, edges) in tree:
872 for (id, type, ctx, vtx, edges) in tree:
873 if type != graphmod.CHANGESET:
873 if type != graphmod.CHANGESET:
874 continue
874 continue
875 node = str(ctx)
875 node = str(ctx)
876 age = templatefilters.age(ctx.date())
876 age = templatefilters.age(ctx.date())
877 desc = templatefilters.firstline(ctx.description())
877 desc = templatefilters.firstline(ctx.description())
878 desc = cgi.escape(templatefilters.nonempty(desc))
878 desc = cgi.escape(templatefilters.nonempty(desc))
879 user = cgi.escape(templatefilters.person(ctx.user()))
879 user = cgi.escape(templatefilters.person(ctx.user()))
880 branch = ctx.branch()
880 branch = ctx.branch()
881 try:
881 try:
882 branchnode = web.repo.branchtip(branch)
882 branchnode = web.repo.branchtip(branch)
883 except error.RepoLookupError:
883 except error.RepoLookupError:
884 branchnode = None
884 branchnode = None
885 branch = branch, branchnode == ctx.node()
885 branch = branch, branchnode == ctx.node()
886
886
887 if usetuples:
887 if usetuples:
888 data.append((node, vtx, edges, desc, user, age, branch,
888 data.append((node, vtx, edges, desc, user, age, branch,
889 ctx.tags(), ctx.bookmarks()))
889 ctx.tags(), ctx.bookmarks()))
890 else:
890 else:
891 edgedata = [dict(col=edge[0], nextcol=edge[1],
891 edgedata = [dict(col=edge[0], nextcol=edge[1],
892 color=(edge[2] - 1) % 6 + 1,
892 color=(edge[2] - 1) % 6 + 1,
893 width=edge[3], bcolor=edge[4])
893 width=edge[3], bcolor=edge[4])
894 for edge in edges]
894 for edge in edges]
895
895
896 data.append(
896 data.append(
897 dict(node=node,
897 dict(node=node,
898 col=vtx[0],
898 col=vtx[0],
899 color=(vtx[1] - 1) % 6 + 1,
899 color=(vtx[1] - 1) % 6 + 1,
900 edges=edgedata,
900 edges=edgedata,
901 row=row,
901 row=row,
902 nextrow=row + 1,
902 nextrow=row + 1,
903 desc=desc,
903 desc=desc,
904 user=user,
904 user=user,
905 age=age,
905 age=age,
906 bookmarks=webutil.nodebookmarksdict(
906 bookmarks=webutil.nodebookmarksdict(
907 web.repo, ctx.node()),
907 web.repo, ctx.node()),
908 branches=webutil.nodebranchdict(web.repo, ctx),
908 branches=webutil.nodebranchdict(web.repo, ctx),
909 inbranch=webutil.nodeinbranch(web.repo, ctx),
909 inbranch=webutil.nodeinbranch(web.repo, ctx),
910 tags=webutil.nodetagsdict(web.repo, ctx.node())))
910 tags=webutil.nodetagsdict(web.repo, ctx.node())))
911
911
912 row += 1
912 row += 1
913
913
914 return data
914 return data
915
915
916 cols = getcolumns(tree)
916 cols = getcolumns(tree)
917 rows = len(tree)
917 rows = len(tree)
918 canvasheight = (rows + 1) * bg_height - 27
918 canvasheight = (rows + 1) * bg_height - 27
919
919
920 return tmpl('graph', rev=rev, revcount=revcount, uprev=uprev,
920 return tmpl('graph', rev=rev, revcount=revcount, uprev=uprev,
921 lessvars=lessvars, morevars=morevars, downrev=downrev,
921 lessvars=lessvars, morevars=morevars, downrev=downrev,
922 cols=cols, rows=rows,
922 cols=cols, rows=rows,
923 canvaswidth=(cols + 1) * bg_height,
923 canvaswidth=(cols + 1) * bg_height,
924 truecanvasheight=rows * bg_height,
924 truecanvasheight=rows * bg_height,
925 canvasheight=canvasheight, bg_height=bg_height,
925 canvasheight=canvasheight, bg_height=bg_height,
926 jsdata=lambda **x: graphdata(True, **x),
926 jsdata=lambda **x: graphdata(True, **x),
927 nodes=lambda **x: graphdata(False, **x),
927 nodes=lambda **x: graphdata(False, **x),
928 node=ctx.hex(), changenav=changenav)
928 node=ctx.hex(), changenav=changenav)
929
929
930 def _getdoc(e):
930 def _getdoc(e):
931 doc = e[0].__doc__
931 doc = e[0].__doc__
932 if doc:
932 if doc:
933 doc = _(doc).split('\n')[0]
933 doc = _(doc).split('\n')[0]
934 else:
934 else:
935 doc = _('(no help text available)')
935 doc = _('(no help text available)')
936 return doc
936 return doc
937
937
938 def help(web, req, tmpl):
938 def help(web, req, tmpl):
939 from mercurial import commands # avoid cycle
939 from mercurial import commands # avoid cycle
940
940
941 topicname = req.form.get('node', [None])[0]
941 topicname = req.form.get('node', [None])[0]
942 if not topicname:
942 if not topicname:
943 def topics(**map):
943 def topics(**map):
944 for entries, summary, _ in helpmod.helptable:
944 for entries, summary, _ in helpmod.helptable:
945 yield {'topic': entries[0], 'summary': summary}
945 yield {'topic': entries[0], 'summary': summary}
946
946
947 early, other = [], []
947 early, other = [], []
948 primary = lambda s: s.split('|')[0]
948 primary = lambda s: s.split('|')[0]
949 for c, e in commands.table.iteritems():
949 for c, e in commands.table.iteritems():
950 doc = _getdoc(e)
950 doc = _getdoc(e)
951 if 'DEPRECATED' in doc or c.startswith('debug'):
951 if 'DEPRECATED' in doc or c.startswith('debug'):
952 continue
952 continue
953 cmd = primary(c)
953 cmd = primary(c)
954 if cmd.startswith('^'):
954 if cmd.startswith('^'):
955 early.append((cmd[1:], doc))
955 early.append((cmd[1:], doc))
956 else:
956 else:
957 other.append((cmd, doc))
957 other.append((cmd, doc))
958
958
959 early.sort()
959 early.sort()
960 other.sort()
960 other.sort()
961
961
962 def earlycommands(**map):
962 def earlycommands(**map):
963 for c, doc in early:
963 for c, doc in early:
964 yield {'topic': c, 'summary': doc}
964 yield {'topic': c, 'summary': doc}
965
965
966 def othercommands(**map):
966 def othercommands(**map):
967 for c, doc in other:
967 for c, doc in other:
968 yield {'topic': c, 'summary': doc}
968 yield {'topic': c, 'summary': doc}
969
969
970 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
970 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
971 othercommands=othercommands, title='Index')
971 othercommands=othercommands, title='Index')
972
972
973 u = webutil.wsgiui()
973 u = webutil.wsgiui()
974 u.pushbuffer()
974 u.pushbuffer()
975 u.verbose = True
975 u.verbose = True
976 try:
976 try:
977 commands.help_(u, topicname)
977 commands.help_(u, topicname)
978 except error.UnknownCommand:
978 except error.UnknownCommand:
979 raise ErrorResponse(HTTP_NOT_FOUND)
979 raise ErrorResponse(HTTP_NOT_FOUND)
980 doc = u.popbuffer()
980 doc = u.popbuffer()
981 return tmpl('help', topic=topicname, doc=doc)
981 return tmpl('help', topic=topicname, doc=doc)
General Comments 0
You need to be logged in to leave comments. Login now