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