##// END OF EJS Templates
hgweb: use absolute_import
Yuya Nishihara -
r27046:37fcfe52 default
parent child Browse files
Show More
@@ -1,31 +1,37 b''
1 1 # hgweb/__init__.py - web interface to a mercurial repository
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005 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 from __future__ import absolute_import
10
9 11 import os
10 import hgweb_mod, hgwebdir_mod
12
13 from . import (
14 hgweb_mod,
15 hgwebdir_mod,
16 )
11 17
12 18 def hgweb(config, name=None, baseui=None):
13 19 '''create an hgweb wsgi object
14 20
15 21 config can be one of:
16 22 - repo object (single repo view)
17 23 - path to repo (single repo view)
18 24 - path to config file (multi-repo view)
19 25 - dict of virtual:real pairs (multi-repo view)
20 26 - list of virtual:real tuples (multi-repo view)
21 27 '''
22 28
23 29 if ((isinstance(config, str) and not os.path.isdir(config)) or
24 30 isinstance(config, dict) or isinstance(config, list)):
25 31 # create a multi-dir interface
26 32 return hgwebdir_mod.hgwebdir(config, baseui=baseui)
27 33 return hgweb_mod.hgweb(config, name=name, baseui=baseui)
28 34
29 35 def hgwebdir(config, baseui=None):
30 36 return hgwebdir_mod.hgwebdir(config, baseui=baseui)
31 37
@@ -1,189 +1,193 b''
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 from __future__ import absolute_import
10
9 11 import BaseHTTPServer
10 import errno, mimetypes, os
12 import errno
13 import mimetypes
14 import os
11 15
12 16 HTTP_OK = 200
13 17 HTTP_NOT_MODIFIED = 304
14 18 HTTP_BAD_REQUEST = 400
15 19 HTTP_UNAUTHORIZED = 401
16 20 HTTP_FORBIDDEN = 403
17 21 HTTP_NOT_FOUND = 404
18 22 HTTP_METHOD_NOT_ALLOWED = 405
19 23 HTTP_SERVER_ERROR = 500
20 24
21 25
22 26 def ismember(ui, username, userlist):
23 27 """Check if username is a member of userlist.
24 28
25 29 If userlist has a single '*' member, all users are considered members.
26 30 Can be overridden by extensions to provide more complex authorization
27 31 schemes.
28 32 """
29 33 return userlist == ['*'] or username in userlist
30 34
31 35 def checkauthz(hgweb, req, op):
32 36 '''Check permission for operation based on request data (including
33 37 authentication info). Return if op allowed, else raise an ErrorResponse
34 38 exception.'''
35 39
36 40 user = req.env.get('REMOTE_USER')
37 41
38 42 deny_read = hgweb.configlist('web', 'deny_read')
39 43 if deny_read and (not user or ismember(hgweb.repo.ui, user, deny_read)):
40 44 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
41 45
42 46 allow_read = hgweb.configlist('web', 'allow_read')
43 47 if allow_read and (not ismember(hgweb.repo.ui, user, allow_read)):
44 48 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
45 49
46 50 if op == 'pull' and not hgweb.allowpull:
47 51 raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized')
48 52 elif op == 'pull' or op is None: # op is None for interface requests
49 53 return
50 54
51 55 # enforce that you can only push using POST requests
52 56 if req.env['REQUEST_METHOD'] != 'POST':
53 57 msg = 'push requires POST request'
54 58 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
55 59
56 60 # require ssl by default for pushing, auth info cannot be sniffed
57 61 # and replayed
58 62 scheme = req.env.get('wsgi.url_scheme')
59 63 if hgweb.configbool('web', 'push_ssl', True) and scheme != 'https':
60 64 raise ErrorResponse(HTTP_FORBIDDEN, 'ssl required')
61 65
62 66 deny = hgweb.configlist('web', 'deny_push')
63 67 if deny and (not user or ismember(hgweb.repo.ui, user, deny)):
64 68 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
65 69
66 70 allow = hgweb.configlist('web', 'allow_push')
67 71 if not (allow and ismember(hgweb.repo.ui, user, allow)):
68 72 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
69 73
70 74 # Hooks for hgweb permission checks; extensions can add hooks here.
71 75 # Each hook is invoked like this: hook(hgweb, request, operation),
72 76 # where operation is either read, pull or push. Hooks should either
73 77 # raise an ErrorResponse exception, or just return.
74 78 #
75 79 # It is possible to do both authentication and authorization through
76 80 # this.
77 81 permhooks = [checkauthz]
78 82
79 83
80 84 class ErrorResponse(Exception):
81 85 def __init__(self, code, message=None, headers=[]):
82 86 if message is None:
83 87 message = _statusmessage(code)
84 88 Exception.__init__(self, message)
85 89 self.code = code
86 90 self.headers = headers
87 91
88 92 class continuereader(object):
89 93 def __init__(self, f, write):
90 94 self.f = f
91 95 self._write = write
92 96 self.continued = False
93 97
94 98 def read(self, amt=-1):
95 99 if not self.continued:
96 100 self.continued = True
97 101 self._write('HTTP/1.1 100 Continue\r\n\r\n')
98 102 return self.f.read(amt)
99 103
100 104 def __getattr__(self, attr):
101 105 if attr in ('close', 'readline', 'readlines', '__iter__'):
102 106 return getattr(self.f, attr)
103 107 raise AttributeError
104 108
105 109 def _statusmessage(code):
106 110 responses = BaseHTTPServer.BaseHTTPRequestHandler.responses
107 111 return responses.get(code, ('Error', 'Unknown error'))[0]
108 112
109 113 def statusmessage(code, message=None):
110 114 return '%d %s' % (code, message or _statusmessage(code))
111 115
112 116 def get_stat(spath, fn):
113 117 """stat fn if it exists, spath otherwise"""
114 118 cl_path = os.path.join(spath, fn)
115 119 if os.path.exists(cl_path):
116 120 return os.stat(cl_path)
117 121 else:
118 122 return os.stat(spath)
119 123
120 124 def get_mtime(spath):
121 125 return get_stat(spath, "00changelog.i").st_mtime
122 126
123 127 def staticfile(directory, fname, req):
124 128 """return a file inside directory with guessed Content-Type header
125 129
126 130 fname always uses '/' as directory separator and isn't allowed to
127 131 contain unusual path components.
128 132 Content-Type is guessed using the mimetypes module.
129 133 Return an empty string if fname is illegal or file not found.
130 134
131 135 """
132 136 parts = fname.split('/')
133 137 for part in parts:
134 138 if (part in ('', os.curdir, os.pardir) or
135 139 os.sep in part or os.altsep is not None and os.altsep in part):
136 140 return
137 141 fpath = os.path.join(*parts)
138 142 if isinstance(directory, str):
139 143 directory = [directory]
140 144 for d in directory:
141 145 path = os.path.join(d, fpath)
142 146 if os.path.exists(path):
143 147 break
144 148 try:
145 149 os.stat(path)
146 150 ct = mimetypes.guess_type(path)[0] or "text/plain"
147 151 fp = open(path, 'rb')
148 152 data = fp.read()
149 153 fp.close()
150 154 req.respond(HTTP_OK, ct, body=data)
151 155 except TypeError:
152 156 raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal filename')
153 157 except OSError as err:
154 158 if err.errno == errno.ENOENT:
155 159 raise ErrorResponse(HTTP_NOT_FOUND)
156 160 else:
157 161 raise ErrorResponse(HTTP_SERVER_ERROR, err.strerror)
158 162
159 163 def paritygen(stripecount, offset=0):
160 164 """count parity of horizontal stripes for easier reading"""
161 165 if stripecount and offset:
162 166 # account for offset, e.g. due to building the list in reverse
163 167 count = (stripecount + offset) % stripecount
164 168 parity = (stripecount + offset) / stripecount & 1
165 169 else:
166 170 count = 0
167 171 parity = 0
168 172 while True:
169 173 yield parity
170 174 count += 1
171 175 if stripecount and count >= stripecount:
172 176 parity = 1 - parity
173 177 count = 0
174 178
175 179 def get_contact(config):
176 180 """Return repo contact information or empty string.
177 181
178 182 web.contact is the primary source, but if that is not set, try
179 183 ui.username or $EMAIL as a fallback to display something useful.
180 184 """
181 185 return (config("web", "contact") or
182 186 config("ui", "username") or
183 187 os.environ.get("EMAIL") or "")
184 188
185 189 def caching(web, req):
186 190 tag = str(web.mtime)
187 191 if req.env.get('HTTP_IF_NONE_MATCH') == tag:
188 192 raise ErrorResponse(HTTP_NOT_MODIFIED)
189 193 req.headers.append(('ETag', tag))
@@ -1,441 +1,466 b''
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 from __future__ import absolute_import
10
9 11 import contextlib
10 12 import os
11 from mercurial import hg, hook, error, encoding, templater, util, repoview
12 from mercurial import ui as uimod
13 from mercurial import templatefilters
14 from common import ErrorResponse, permhooks, caching
15 from common import HTTP_OK, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST
16 from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR
17 from request import wsgirequest
18 import webcommands, protocol, webutil, wsgicgi
13
14 from .common import (
15 ErrorResponse,
16 HTTP_BAD_REQUEST,
17 HTTP_NOT_FOUND,
18 HTTP_NOT_MODIFIED,
19 HTTP_OK,
20 HTTP_SERVER_ERROR,
21 caching,
22 permhooks,
23 )
24 from .request import wsgirequest
25
26 from .. import (
27 encoding,
28 error,
29 hg,
30 hook,
31 repoview,
32 templatefilters,
33 templater,
34 ui as uimod,
35 util,
36 )
37
38 from . import (
39 protocol,
40 webcommands,
41 webutil,
42 wsgicgi,
43 )
19 44
20 45 perms = {
21 46 'changegroup': 'pull',
22 47 'changegroupsubset': 'pull',
23 48 'getbundle': 'pull',
24 49 'stream_out': 'pull',
25 50 'listkeys': 'pull',
26 51 'unbundle': 'push',
27 52 'pushkey': 'push',
28 53 }
29 54
30 55 def makebreadcrumb(url, prefix=''):
31 56 '''Return a 'URL breadcrumb' list
32 57
33 58 A 'URL breadcrumb' is a list of URL-name pairs,
34 59 corresponding to each of the path items on a URL.
35 60 This can be used to create path navigation entries.
36 61 '''
37 62 if url.endswith('/'):
38 63 url = url[:-1]
39 64 if prefix:
40 65 url = '/' + prefix + url
41 66 relpath = url
42 67 if relpath.startswith('/'):
43 68 relpath = relpath[1:]
44 69
45 70 breadcrumb = []
46 71 urlel = url
47 72 pathitems = [''] + relpath.split('/')
48 73 for pathel in reversed(pathitems):
49 74 if not pathel or not urlel:
50 75 break
51 76 breadcrumb.append({'url': urlel, 'name': pathel})
52 77 urlel = os.path.dirname(urlel)
53 78 return reversed(breadcrumb)
54 79
55 80 class requestcontext(object):
56 81 """Holds state/context for an individual request.
57 82
58 83 Servers can be multi-threaded. Holding state on the WSGI application
59 84 is prone to race conditions. Instances of this class exist to hold
60 85 mutable and race-free state for requests.
61 86 """
62 87 def __init__(self, app, repo):
63 88 self.repo = repo
64 89 self.reponame = app.reponame
65 90
66 91 self.archives = ('zip', 'gz', 'bz2')
67 92
68 93 self.maxchanges = self.configint('web', 'maxchanges', 10)
69 94 self.stripecount = self.configint('web', 'stripes', 1)
70 95 self.maxshortchanges = self.configint('web', 'maxshortchanges', 60)
71 96 self.maxfiles = self.configint('web', 'maxfiles', 10)
72 97 self.allowpull = self.configbool('web', 'allowpull', True)
73 98
74 99 # we use untrusted=False to prevent a repo owner from using
75 100 # web.templates in .hg/hgrc to get access to any file readable
76 101 # by the user running the CGI script
77 102 self.templatepath = self.config('web', 'templates', untrusted=False)
78 103
79 104 # This object is more expensive to build than simple config values.
80 105 # It is shared across requests. The app will replace the object
81 106 # if it is updated. Since this is a reference and nothing should
82 107 # modify the underlying object, it should be constant for the lifetime
83 108 # of the request.
84 109 self.websubtable = app.websubtable
85 110
86 111 # Trust the settings from the .hg/hgrc files by default.
87 112 def config(self, section, name, default=None, untrusted=True):
88 113 return self.repo.ui.config(section, name, default,
89 114 untrusted=untrusted)
90 115
91 116 def configbool(self, section, name, default=False, untrusted=True):
92 117 return self.repo.ui.configbool(section, name, default,
93 118 untrusted=untrusted)
94 119
95 120 def configint(self, section, name, default=None, untrusted=True):
96 121 return self.repo.ui.configint(section, name, default,
97 122 untrusted=untrusted)
98 123
99 124 def configlist(self, section, name, default=None, untrusted=True):
100 125 return self.repo.ui.configlist(section, name, default,
101 126 untrusted=untrusted)
102 127
103 128 archivespecs = {
104 129 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
105 130 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
106 131 'zip': ('application/zip', 'zip', '.zip', None),
107 132 }
108 133
109 134 def archivelist(self, nodeid):
110 135 allowed = self.configlist('web', 'allow_archive')
111 136 for typ, spec in self.archivespecs.iteritems():
112 137 if typ in allowed or self.configbool('web', 'allow%s' % typ):
113 138 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
114 139
115 140 def templater(self, req):
116 141 # determine scheme, port and server name
117 142 # this is needed to create absolute urls
118 143
119 144 proto = req.env.get('wsgi.url_scheme')
120 145 if proto == 'https':
121 146 proto = 'https'
122 147 default_port = '443'
123 148 else:
124 149 proto = 'http'
125 150 default_port = '80'
126 151
127 152 port = req.env['SERVER_PORT']
128 153 port = port != default_port and (':' + port) or ''
129 154 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
130 155 logourl = self.config('web', 'logourl', 'https://mercurial-scm.org/')
131 156 logoimg = self.config('web', 'logoimg', 'hglogo.png')
132 157 staticurl = self.config('web', 'staticurl') or req.url + 'static/'
133 158 if not staticurl.endswith('/'):
134 159 staticurl += '/'
135 160
136 161 # some functions for the templater
137 162
138 163 def motd(**map):
139 164 yield self.config('web', 'motd', '')
140 165
141 166 # figure out which style to use
142 167
143 168 vars = {}
144 169 styles = (
145 170 req.form.get('style', [None])[0],
146 171 self.config('web', 'style'),
147 172 'paper',
148 173 )
149 174 style, mapfile = templater.stylemap(styles, self.templatepath)
150 175 if style == styles[0]:
151 176 vars['style'] = style
152 177
153 178 start = req.url[-1] == '?' and '&' or '?'
154 179 sessionvars = webutil.sessionvars(vars, start)
155 180
156 181 if not self.reponame:
157 182 self.reponame = (self.config('web', 'name')
158 183 or req.env.get('REPO_NAME')
159 184 or req.url.strip('/') or self.repo.root)
160 185
161 186 def websubfilter(text):
162 187 return templatefilters.websub(text, self.websubtable)
163 188
164 189 # create the templater
165 190
166 191 tmpl = templater.templater(mapfile,
167 192 filters={'websub': websubfilter},
168 193 defaults={'url': req.url,
169 194 'logourl': logourl,
170 195 'logoimg': logoimg,
171 196 'staticurl': staticurl,
172 197 'urlbase': urlbase,
173 198 'repo': self.reponame,
174 199 'encoding': encoding.encoding,
175 200 'motd': motd,
176 201 'sessionvars': sessionvars,
177 202 'pathdef': makebreadcrumb(req.url),
178 203 'style': style,
179 204 })
180 205 return tmpl
181 206
182 207
183 208 class hgweb(object):
184 209 """HTTP server for individual repositories.
185 210
186 211 Instances of this class serve HTTP responses for a particular
187 212 repository.
188 213
189 214 Instances are typically used as WSGI applications.
190 215
191 216 Some servers are multi-threaded. On these servers, there may
192 217 be multiple active threads inside __call__.
193 218 """
194 219 def __init__(self, repo, name=None, baseui=None):
195 220 if isinstance(repo, str):
196 221 if baseui:
197 222 u = baseui.copy()
198 223 else:
199 224 u = uimod.ui()
200 225 r = hg.repository(u, repo)
201 226 else:
202 227 # we trust caller to give us a private copy
203 228 r = repo
204 229
205 230 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
206 231 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
207 232 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
208 233 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
209 234 # resolve file patterns relative to repo root
210 235 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
211 236 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
212 237 # displaying bundling progress bar while serving feel wrong and may
213 238 # break some wsgi implementation.
214 239 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
215 240 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
216 241 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
217 242 self._lastrepo = self._repos[0]
218 243 hook.redirect(True)
219 244 self.reponame = name
220 245
221 246 def _webifyrepo(self, repo):
222 247 repo = getwebview(repo)
223 248 self.websubtable = webutil.getwebsubs(repo)
224 249 return repo
225 250
226 251 @contextlib.contextmanager
227 252 def _obtainrepo(self):
228 253 """Obtain a repo unique to the caller.
229 254
230 255 Internally we maintain a stack of cachedlocalrepo instances
231 256 to be handed out. If one is available, we pop it and return it,
232 257 ensuring it is up to date in the process. If one is not available,
233 258 we clone the most recently used repo instance and return it.
234 259
235 260 It is currently possible for the stack to grow without bounds
236 261 if the server allows infinite threads. However, servers should
237 262 have a thread limit, thus establishing our limit.
238 263 """
239 264 if self._repos:
240 265 cached = self._repos.pop()
241 266 r, created = cached.fetch()
242 267 else:
243 268 cached = self._lastrepo.copy()
244 269 r, created = cached.fetch()
245 270 if created:
246 271 r = self._webifyrepo(r)
247 272
248 273 self._lastrepo = cached
249 274 self.mtime = cached.mtime
250 275 try:
251 276 yield r
252 277 finally:
253 278 self._repos.append(cached)
254 279
255 280 def run(self):
256 281 """Start a server from CGI environment.
257 282
258 283 Modern servers should be using WSGI and should avoid this
259 284 method, if possible.
260 285 """
261 286 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
262 287 raise RuntimeError("This function is only intended to be "
263 288 "called while running as a CGI script.")
264 289 wsgicgi.launch(self)
265 290
266 291 def __call__(self, env, respond):
267 292 """Run the WSGI application.
268 293
269 294 This may be called by multiple threads.
270 295 """
271 296 req = wsgirequest(env, respond)
272 297 return self.run_wsgi(req)
273 298
274 299 def run_wsgi(self, req):
275 300 """Internal method to run the WSGI application.
276 301
277 302 This is typically only called by Mercurial. External consumers
278 303 should be using instances of this class as the WSGI application.
279 304 """
280 305 with self._obtainrepo() as repo:
281 306 for r in self._runwsgi(req, repo):
282 307 yield r
283 308
284 309 def _runwsgi(self, req, repo):
285 310 rctx = requestcontext(self, repo)
286 311
287 312 # This state is global across all threads.
288 313 encoding.encoding = rctx.config('web', 'encoding', encoding.encoding)
289 314 rctx.repo.ui.environ = req.env
290 315
291 316 # work with CGI variables to create coherent structure
292 317 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
293 318
294 319 req.url = req.env['SCRIPT_NAME']
295 320 if not req.url.endswith('/'):
296 321 req.url += '/'
297 322 if 'REPO_NAME' in req.env:
298 323 req.url += req.env['REPO_NAME'] + '/'
299 324
300 325 if 'PATH_INFO' in req.env:
301 326 parts = req.env['PATH_INFO'].strip('/').split('/')
302 327 repo_parts = req.env.get('REPO_NAME', '').split('/')
303 328 if parts[:len(repo_parts)] == repo_parts:
304 329 parts = parts[len(repo_parts):]
305 330 query = '/'.join(parts)
306 331 else:
307 332 query = req.env['QUERY_STRING'].partition('&')[0]
308 333 query = query.partition(';')[0]
309 334
310 335 # process this if it's a protocol request
311 336 # protocol bits don't need to create any URLs
312 337 # and the clients always use the old URL structure
313 338
314 339 cmd = req.form.get('cmd', [''])[0]
315 340 if protocol.iscmd(cmd):
316 341 try:
317 342 if query:
318 343 raise ErrorResponse(HTTP_NOT_FOUND)
319 344 if cmd in perms:
320 345 self.check_perm(rctx, req, perms[cmd])
321 346 return protocol.call(rctx.repo, req, cmd)
322 347 except ErrorResponse as inst:
323 348 # A client that sends unbundle without 100-continue will
324 349 # break if we respond early.
325 350 if (cmd == 'unbundle' and
326 351 (req.env.get('HTTP_EXPECT',
327 352 '').lower() != '100-continue') or
328 353 req.env.get('X-HgHttp2', '')):
329 354 req.drain()
330 355 else:
331 356 req.headers.append(('Connection', 'Close'))
332 357 req.respond(inst, protocol.HGTYPE,
333 358 body='0\n%s\n' % inst)
334 359 return ''
335 360
336 361 # translate user-visible url structure to internal structure
337 362
338 363 args = query.split('/', 2)
339 364 if 'cmd' not in req.form and args and args[0]:
340 365
341 366 cmd = args.pop(0)
342 367 style = cmd.rfind('-')
343 368 if style != -1:
344 369 req.form['style'] = [cmd[:style]]
345 370 cmd = cmd[style + 1:]
346 371
347 372 # avoid accepting e.g. style parameter as command
348 373 if util.safehasattr(webcommands, cmd):
349 374 req.form['cmd'] = [cmd]
350 375
351 376 if cmd == 'static':
352 377 req.form['file'] = ['/'.join(args)]
353 378 else:
354 379 if args and args[0]:
355 380 node = args.pop(0).replace('%2F', '/')
356 381 req.form['node'] = [node]
357 382 if args:
358 383 req.form['file'] = args
359 384
360 385 ua = req.env.get('HTTP_USER_AGENT', '')
361 386 if cmd == 'rev' and 'mercurial' in ua:
362 387 req.form['style'] = ['raw']
363 388
364 389 if cmd == 'archive':
365 390 fn = req.form['node'][0]
366 391 for type_, spec in rctx.archivespecs.iteritems():
367 392 ext = spec[2]
368 393 if fn.endswith(ext):
369 394 req.form['node'] = [fn[:-len(ext)]]
370 395 req.form['type'] = [type_]
371 396
372 397 # process the web interface request
373 398
374 399 try:
375 400 tmpl = rctx.templater(req)
376 401 ctype = tmpl('mimetype', encoding=encoding.encoding)
377 402 ctype = templater.stringify(ctype)
378 403
379 404 # check read permissions non-static content
380 405 if cmd != 'static':
381 406 self.check_perm(rctx, req, None)
382 407
383 408 if cmd == '':
384 409 req.form['cmd'] = [tmpl.cache['default']]
385 410 cmd = req.form['cmd'][0]
386 411
387 412 if rctx.configbool('web', 'cache', True):
388 413 caching(self, req) # sets ETag header or raises NOT_MODIFIED
389 414 if cmd not in webcommands.__all__:
390 415 msg = 'no such method: %s' % cmd
391 416 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
392 417 elif cmd == 'file' and 'raw' in req.form.get('style', []):
393 418 rctx.ctype = ctype
394 419 content = webcommands.rawfile(rctx, req, tmpl)
395 420 else:
396 421 content = getattr(webcommands, cmd)(rctx, req, tmpl)
397 422 req.respond(HTTP_OK, ctype)
398 423
399 424 return content
400 425
401 426 except (error.LookupError, error.RepoLookupError) as err:
402 427 req.respond(HTTP_NOT_FOUND, ctype)
403 428 msg = str(err)
404 429 if (util.safehasattr(err, 'name') and
405 430 not isinstance(err, error.ManifestLookupError)):
406 431 msg = 'revision not found: %s' % err.name
407 432 return tmpl('error', error=msg)
408 433 except (error.RepoError, error.RevlogError) as inst:
409 434 req.respond(HTTP_SERVER_ERROR, ctype)
410 435 return tmpl('error', error=str(inst))
411 436 except ErrorResponse as inst:
412 437 req.respond(inst, ctype)
413 438 if inst.code == HTTP_NOT_MODIFIED:
414 439 # Not allowed to return a body on a 304
415 440 return ['']
416 441 return tmpl('error', error=str(inst))
417 442
418 443 def check_perm(self, rctx, req, op):
419 444 for permhook in permhooks:
420 445 permhook(rctx, req, op)
421 446
422 447 def getwebview(repo):
423 448 """The 'web.view' config controls changeset filter to hgweb. Possible
424 449 values are ``served``, ``visible`` and ``all``. Default is ``served``.
425 450 The ``served`` filter only shows changesets that can be pulled from the
426 451 hgweb instance. The``visible`` filter includes secret changesets but
427 452 still excludes "hidden" one.
428 453
429 454 See the repoview module for details.
430 455
431 456 The option has been around undocumented since Mercurial 2.5, but no
432 457 user ever asked about it. So we better keep it undocumented for now."""
433 458 viewconfig = repo.ui.config('web', 'view', 'served',
434 459 untrusted=True)
435 460 if viewconfig == 'all':
436 461 return repo.unfiltered()
437 462 elif viewconfig in repoview.filtertable:
438 463 return repo.filtered(viewconfig)
439 464 else:
440 465 return repo.filtered('served')
441 466
@@ -1,485 +1,511 b''
1 1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
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 import os, re, time
10 from mercurial.i18n import _
11 from mercurial import hg, scmutil, util, templater
12 from mercurial import ui as uimod
13 from mercurial import error, encoding
14 from common import ErrorResponse, get_mtime, staticfile, paritygen, ismember, \
15 get_contact, HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
16 import hgweb_mod
17 from request import wsgirequest
18 import webutil, wsgicgi
9 from __future__ import absolute_import
10
11 import os
12 import re
13 import time
14
15 from ..i18n import _
16
17 from .common import (
18 ErrorResponse,
19 HTTP_NOT_FOUND,
20 HTTP_OK,
21 HTTP_SERVER_ERROR,
22 get_contact,
23 get_mtime,
24 ismember,
25 paritygen,
26 staticfile,
27 )
28 from .request import wsgirequest
29
30 from .. import (
31 encoding,
32 error,
33 hg,
34 scmutil,
35 templater,
36 ui as uimod,
37 util,
38 )
39
40 from . import (
41 hgweb_mod,
42 webutil,
43 wsgicgi,
44 )
19 45
20 46 def cleannames(items):
21 47 return [(util.pconvert(name).strip('/'), path) for name, path in items]
22 48
23 49 def findrepos(paths):
24 50 repos = []
25 51 for prefix, root in cleannames(paths):
26 52 roothead, roottail = os.path.split(root)
27 53 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
28 54 # /bar/ be served as as foo/N .
29 55 # '*' will not search inside dirs with .hg (except .hg/patches),
30 56 # '**' will search inside dirs with .hg (and thus also find subrepos).
31 57 try:
32 58 recurse = {'*': False, '**': True}[roottail]
33 59 except KeyError:
34 60 repos.append((prefix, root))
35 61 continue
36 62 roothead = os.path.normpath(os.path.abspath(roothead))
37 63 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
38 64 repos.extend(urlrepos(prefix, roothead, paths))
39 65 return repos
40 66
41 67 def urlrepos(prefix, roothead, paths):
42 68 """yield url paths and filesystem paths from a list of repo paths
43 69
44 70 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
45 71 >>> conv(urlrepos('hg', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
46 72 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
47 73 >>> conv(urlrepos('', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
48 74 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
49 75 """
50 76 for path in paths:
51 77 path = os.path.normpath(path)
52 78 yield (prefix + '/' +
53 79 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
54 80
55 81 def geturlcgivars(baseurl, port):
56 82 """
57 83 Extract CGI variables from baseurl
58 84
59 85 >>> geturlcgivars("http://host.org/base", "80")
60 86 ('host.org', '80', '/base')
61 87 >>> geturlcgivars("http://host.org:8000/base", "80")
62 88 ('host.org', '8000', '/base')
63 89 >>> geturlcgivars('/base', 8000)
64 90 ('', '8000', '/base')
65 91 >>> geturlcgivars("base", '8000')
66 92 ('', '8000', '/base')
67 93 >>> geturlcgivars("http://host", '8000')
68 94 ('host', '8000', '/')
69 95 >>> geturlcgivars("http://host/", '8000')
70 96 ('host', '8000', '/')
71 97 """
72 98 u = util.url(baseurl)
73 99 name = u.host or ''
74 100 if u.port:
75 101 port = u.port
76 102 path = u.path or ""
77 103 if not path.startswith('/'):
78 104 path = '/' + path
79 105
80 106 return name, str(port), path
81 107
82 108 class hgwebdir(object):
83 109 """HTTP server for multiple repositories.
84 110
85 111 Given a configuration, different repositories will be served depending
86 112 on the request path.
87 113
88 114 Instances are typically used as WSGI applications.
89 115 """
90 116 def __init__(self, conf, baseui=None):
91 117 self.conf = conf
92 118 self.baseui = baseui
93 119 self.ui = None
94 120 self.lastrefresh = 0
95 121 self.motd = None
96 122 self.refresh()
97 123
98 124 def refresh(self):
99 125 refreshinterval = 20
100 126 if self.ui:
101 127 refreshinterval = self.ui.configint('web', 'refreshinterval',
102 128 refreshinterval)
103 129
104 130 # refreshinterval <= 0 means to always refresh.
105 131 if (refreshinterval > 0 and
106 132 self.lastrefresh + refreshinterval > time.time()):
107 133 return
108 134
109 135 if self.baseui:
110 136 u = self.baseui.copy()
111 137 else:
112 138 u = uimod.ui()
113 139 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
114 140 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
115 141 # displaying bundling progress bar while serving feels wrong and may
116 142 # break some wsgi implementations.
117 143 u.setconfig('progress', 'disable', 'true', 'hgweb')
118 144
119 145 if not isinstance(self.conf, (dict, list, tuple)):
120 146 map = {'paths': 'hgweb-paths'}
121 147 if not os.path.exists(self.conf):
122 148 raise error.Abort(_('config file %s not found!') % self.conf)
123 149 u.readconfig(self.conf, remap=map, trust=True)
124 150 paths = []
125 151 for name, ignored in u.configitems('hgweb-paths'):
126 152 for path in u.configlist('hgweb-paths', name):
127 153 paths.append((name, path))
128 154 elif isinstance(self.conf, (list, tuple)):
129 155 paths = self.conf
130 156 elif isinstance(self.conf, dict):
131 157 paths = self.conf.items()
132 158
133 159 repos = findrepos(paths)
134 160 for prefix, root in u.configitems('collections'):
135 161 prefix = util.pconvert(prefix)
136 162 for path in scmutil.walkrepos(root, followsym=True):
137 163 repo = os.path.normpath(path)
138 164 name = util.pconvert(repo)
139 165 if name.startswith(prefix):
140 166 name = name[len(prefix):]
141 167 repos.append((name.lstrip('/'), repo))
142 168
143 169 self.repos = repos
144 170 self.ui = u
145 171 encoding.encoding = self.ui.config('web', 'encoding',
146 172 encoding.encoding)
147 173 self.style = self.ui.config('web', 'style', 'paper')
148 174 self.templatepath = self.ui.config('web', 'templates', None)
149 175 self.stripecount = self.ui.config('web', 'stripes', 1)
150 176 if self.stripecount:
151 177 self.stripecount = int(self.stripecount)
152 178 self._baseurl = self.ui.config('web', 'baseurl')
153 179 prefix = self.ui.config('web', 'prefix', '')
154 180 if prefix.startswith('/'):
155 181 prefix = prefix[1:]
156 182 if prefix.endswith('/'):
157 183 prefix = prefix[:-1]
158 184 self.prefix = prefix
159 185 self.lastrefresh = time.time()
160 186
161 187 def run(self):
162 188 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
163 189 raise RuntimeError("This function is only intended to be "
164 190 "called while running as a CGI script.")
165 191 wsgicgi.launch(self)
166 192
167 193 def __call__(self, env, respond):
168 194 req = wsgirequest(env, respond)
169 195 return self.run_wsgi(req)
170 196
171 197 def read_allowed(self, ui, req):
172 198 """Check allow_read and deny_read config options of a repo's ui object
173 199 to determine user permissions. By default, with neither option set (or
174 200 both empty), allow all users to read the repo. There are two ways a
175 201 user can be denied read access: (1) deny_read is not empty, and the
176 202 user is unauthenticated or deny_read contains user (or *), and (2)
177 203 allow_read is not empty and the user is not in allow_read. Return True
178 204 if user is allowed to read the repo, else return False."""
179 205
180 206 user = req.env.get('REMOTE_USER')
181 207
182 208 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
183 209 if deny_read and (not user or ismember(ui, user, deny_read)):
184 210 return False
185 211
186 212 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
187 213 # by default, allow reading if no allow_read option has been set
188 214 if (not allow_read) or ismember(ui, user, allow_read):
189 215 return True
190 216
191 217 return False
192 218
193 219 def run_wsgi(self, req):
194 220 try:
195 221 self.refresh()
196 222
197 223 virtual = req.env.get("PATH_INFO", "").strip('/')
198 224 tmpl = self.templater(req)
199 225 ctype = tmpl('mimetype', encoding=encoding.encoding)
200 226 ctype = templater.stringify(ctype)
201 227
202 228 # a static file
203 229 if virtual.startswith('static/') or 'static' in req.form:
204 230 if virtual.startswith('static/'):
205 231 fname = virtual[7:]
206 232 else:
207 233 fname = req.form['static'][0]
208 234 static = self.ui.config("web", "static", None,
209 235 untrusted=False)
210 236 if not static:
211 237 tp = self.templatepath or templater.templatepaths()
212 238 if isinstance(tp, str):
213 239 tp = [tp]
214 240 static = [os.path.join(p, 'static') for p in tp]
215 241 staticfile(static, fname, req)
216 242 return []
217 243
218 244 # top-level index
219 245 elif not virtual:
220 246 req.respond(HTTP_OK, ctype)
221 247 return self.makeindex(req, tmpl)
222 248
223 249 # nested indexes and hgwebs
224 250
225 251 repos = dict(self.repos)
226 252 virtualrepo = virtual
227 253 while virtualrepo:
228 254 real = repos.get(virtualrepo)
229 255 if real:
230 256 req.env['REPO_NAME'] = virtualrepo
231 257 try:
232 258 # ensure caller gets private copy of ui
233 259 repo = hg.repository(self.ui.copy(), real)
234 260 return hgweb_mod.hgweb(repo).run_wsgi(req)
235 261 except IOError as inst:
236 262 msg = inst.strerror
237 263 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
238 264 except error.RepoError as inst:
239 265 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
240 266
241 267 up = virtualrepo.rfind('/')
242 268 if up < 0:
243 269 break
244 270 virtualrepo = virtualrepo[:up]
245 271
246 272 # browse subdirectories
247 273 subdir = virtual + '/'
248 274 if [r for r in repos if r.startswith(subdir)]:
249 275 req.respond(HTTP_OK, ctype)
250 276 return self.makeindex(req, tmpl, subdir)
251 277
252 278 # prefixes not found
253 279 req.respond(HTTP_NOT_FOUND, ctype)
254 280 return tmpl("notfound", repo=virtual)
255 281
256 282 except ErrorResponse as err:
257 283 req.respond(err, ctype)
258 284 return tmpl('error', error=err.message or '')
259 285 finally:
260 286 tmpl = None
261 287
262 288 def makeindex(self, req, tmpl, subdir=""):
263 289
264 290 def archivelist(ui, nodeid, url):
265 291 allowed = ui.configlist("web", "allow_archive", untrusted=True)
266 292 archives = []
267 293 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
268 294 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
269 295 untrusted=True):
270 296 archives.append({"type" : i[0], "extension": i[1],
271 297 "node": nodeid, "url": url})
272 298 return archives
273 299
274 300 def rawentries(subdir="", **map):
275 301
276 302 descend = self.ui.configbool('web', 'descend', True)
277 303 collapse = self.ui.configbool('web', 'collapse', False)
278 304 seenrepos = set()
279 305 seendirs = set()
280 306 for name, path in self.repos:
281 307
282 308 if not name.startswith(subdir):
283 309 continue
284 310 name = name[len(subdir):]
285 311 directory = False
286 312
287 313 if '/' in name:
288 314 if not descend:
289 315 continue
290 316
291 317 nameparts = name.split('/')
292 318 rootname = nameparts[0]
293 319
294 320 if not collapse:
295 321 pass
296 322 elif rootname in seendirs:
297 323 continue
298 324 elif rootname in seenrepos:
299 325 pass
300 326 else:
301 327 directory = True
302 328 name = rootname
303 329
304 330 # redefine the path to refer to the directory
305 331 discarded = '/'.join(nameparts[1:])
306 332
307 333 # remove name parts plus accompanying slash
308 334 path = path[:-len(discarded) - 1]
309 335
310 336 try:
311 337 r = hg.repository(self.ui, path)
312 338 directory = False
313 339 except (IOError, error.RepoError):
314 340 pass
315 341
316 342 parts = [name]
317 343 if 'PATH_INFO' in req.env:
318 344 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
319 345 if req.env['SCRIPT_NAME']:
320 346 parts.insert(0, req.env['SCRIPT_NAME'])
321 347 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
322 348
323 349 # show either a directory entry or a repository
324 350 if directory:
325 351 # get the directory's time information
326 352 try:
327 353 d = (get_mtime(path), util.makedate()[1])
328 354 except OSError:
329 355 continue
330 356
331 357 # add '/' to the name to make it obvious that
332 358 # the entry is a directory, not a regular repository
333 359 row = {'contact': "",
334 360 'contact_sort': "",
335 361 'name': name + '/',
336 362 'name_sort': name,
337 363 'url': url,
338 364 'description': "",
339 365 'description_sort': "",
340 366 'lastchange': d,
341 367 'lastchange_sort': d[1]-d[0],
342 368 'archives': [],
343 369 'isdirectory': True}
344 370
345 371 seendirs.add(name)
346 372 yield row
347 373 continue
348 374
349 375 u = self.ui.copy()
350 376 try:
351 377 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
352 378 except Exception as e:
353 379 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
354 380 continue
355 381 def get(section, name, default=None):
356 382 return u.config(section, name, default, untrusted=True)
357 383
358 384 if u.configbool("web", "hidden", untrusted=True):
359 385 continue
360 386
361 387 if not self.read_allowed(u, req):
362 388 continue
363 389
364 390 # update time with local timezone
365 391 try:
366 392 r = hg.repository(self.ui, path)
367 393 except IOError:
368 394 u.warn(_('error accessing repository at %s\n') % path)
369 395 continue
370 396 except error.RepoError:
371 397 u.warn(_('error accessing repository at %s\n') % path)
372 398 continue
373 399 try:
374 400 d = (get_mtime(r.spath), util.makedate()[1])
375 401 except OSError:
376 402 continue
377 403
378 404 contact = get_contact(get)
379 405 description = get("web", "description", "")
380 406 seenrepos.add(name)
381 407 name = get("web", "name", name)
382 408 row = {'contact': contact or "unknown",
383 409 'contact_sort': contact.upper() or "unknown",
384 410 'name': name,
385 411 'name_sort': name,
386 412 'url': url,
387 413 'description': description or "unknown",
388 414 'description_sort': description.upper() or "unknown",
389 415 'lastchange': d,
390 416 'lastchange_sort': d[1]-d[0],
391 417 'archives': archivelist(u, "tip", url),
392 418 'isdirectory': None,
393 419 }
394 420
395 421 yield row
396 422
397 423 sortdefault = None, False
398 424 def entries(sortcolumn="", descending=False, subdir="", **map):
399 425 rows = rawentries(subdir=subdir, **map)
400 426
401 427 if sortcolumn and sortdefault != (sortcolumn, descending):
402 428 sortkey = '%s_sort' % sortcolumn
403 429 rows = sorted(rows, key=lambda x: x[sortkey],
404 430 reverse=descending)
405 431 for row, parity in zip(rows, paritygen(self.stripecount)):
406 432 row['parity'] = parity
407 433 yield row
408 434
409 435 self.refresh()
410 436 sortable = ["name", "description", "contact", "lastchange"]
411 437 sortcolumn, descending = sortdefault
412 438 if 'sort' in req.form:
413 439 sortcolumn = req.form['sort'][0]
414 440 descending = sortcolumn.startswith('-')
415 441 if descending:
416 442 sortcolumn = sortcolumn[1:]
417 443 if sortcolumn not in sortable:
418 444 sortcolumn = ""
419 445
420 446 sort = [("sort_%s" % column,
421 447 "%s%s" % ((not descending and column == sortcolumn)
422 448 and "-" or "", column))
423 449 for column in sortable]
424 450
425 451 self.refresh()
426 452 self.updatereqenv(req.env)
427 453
428 454 return tmpl("index", entries=entries, subdir=subdir,
429 455 pathdef=hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
430 456 sortcolumn=sortcolumn, descending=descending,
431 457 **dict(sort))
432 458
433 459 def templater(self, req):
434 460
435 461 def motd(**map):
436 462 if self.motd is not None:
437 463 yield self.motd
438 464 else:
439 465 yield config('web', 'motd', '')
440 466
441 467 def config(section, name, default=None, untrusted=True):
442 468 return self.ui.config(section, name, default, untrusted)
443 469
444 470 self.updatereqenv(req.env)
445 471
446 472 url = req.env.get('SCRIPT_NAME', '')
447 473 if not url.endswith('/'):
448 474 url += '/'
449 475
450 476 vars = {}
451 477 styles = (
452 478 req.form.get('style', [None])[0],
453 479 config('web', 'style'),
454 480 'paper'
455 481 )
456 482 style, mapfile = templater.stylemap(styles, self.templatepath)
457 483 if style == styles[0]:
458 484 vars['style'] = style
459 485
460 486 start = url[-1] == '?' and '&' or '?'
461 487 sessionvars = webutil.sessionvars(vars, start)
462 488 logourl = config('web', 'logourl', 'https://mercurial-scm.org/')
463 489 logoimg = config('web', 'logoimg', 'hglogo.png')
464 490 staticurl = config('web', 'staticurl') or url + 'static/'
465 491 if not staticurl.endswith('/'):
466 492 staticurl += '/'
467 493
468 494 tmpl = templater.templater(mapfile,
469 495 defaults={"encoding": encoding.encoding,
470 496 "motd": motd,
471 497 "url": url,
472 498 "logourl": logourl,
473 499 "logoimg": logoimg,
474 500 "staticurl": staticurl,
475 501 "sessionvars": sessionvars,
476 502 "style": style,
477 503 })
478 504 return tmpl
479 505
480 506 def updatereqenv(self, env):
481 507 if self._baseurl is not None:
482 508 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
483 509 env['SERVER_NAME'] = name
484 510 env['SERVER_PORT'] = port
485 511 env['SCRIPT_NAME'] = path
@@ -1,98 +1,110 b''
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 import cgi, cStringIO, zlib, urllib
9 from mercurial import util, wireproto
10 from common import HTTP_OK
8 from __future__ import absolute_import
9
10 import cStringIO
11 import cgi
12 import urllib
13 import zlib
14
15 from .common import (
16 HTTP_OK,
17 )
18
19 from .. import (
20 util,
21 wireproto,
22 )
11 23
12 24 HGTYPE = 'application/mercurial-0.1'
13 25 HGERRTYPE = 'application/hg-error'
14 26
15 27 class webproto(wireproto.abstractserverproto):
16 28 def __init__(self, req, ui):
17 29 self.req = req
18 30 self.response = ''
19 31 self.ui = ui
20 32 def getargs(self, args):
21 33 knownargs = self._args()
22 34 data = {}
23 35 keys = args.split()
24 36 for k in keys:
25 37 if k == '*':
26 38 star = {}
27 39 for key in knownargs.keys():
28 40 if key != 'cmd' and key not in keys:
29 41 star[key] = knownargs[key][0]
30 42 data['*'] = star
31 43 else:
32 44 data[k] = knownargs[k][0]
33 45 return [data[k] for k in keys]
34 46 def _args(self):
35 47 args = self.req.form.copy()
36 48 chunks = []
37 49 i = 1
38 50 while True:
39 51 h = self.req.env.get('HTTP_X_HGARG_' + str(i))
40 52 if h is None:
41 53 break
42 54 chunks += [h]
43 55 i += 1
44 56 args.update(cgi.parse_qs(''.join(chunks), keep_blank_values=True))
45 57 return args
46 58 def getfile(self, fp):
47 59 length = int(self.req.env['CONTENT_LENGTH'])
48 60 for s in util.filechunkiter(self.req, limit=length):
49 61 fp.write(s)
50 62 def redirect(self):
51 63 self.oldio = self.ui.fout, self.ui.ferr
52 64 self.ui.ferr = self.ui.fout = cStringIO.StringIO()
53 65 def restore(self):
54 66 val = self.ui.fout.getvalue()
55 67 self.ui.ferr, self.ui.fout = self.oldio
56 68 return val
57 69 def groupchunks(self, cg):
58 70 z = zlib.compressobj()
59 71 while True:
60 72 chunk = cg.read(4096)
61 73 if not chunk:
62 74 break
63 75 yield z.compress(chunk)
64 76 yield z.flush()
65 77 def _client(self):
66 78 return 'remote:%s:%s:%s' % (
67 79 self.req.env.get('wsgi.url_scheme') or 'http',
68 80 urllib.quote(self.req.env.get('REMOTE_HOST', '')),
69 81 urllib.quote(self.req.env.get('REMOTE_USER', '')))
70 82
71 83 def iscmd(cmd):
72 84 return cmd in wireproto.commands
73 85
74 86 def call(repo, req, cmd):
75 87 p = webproto(req, repo.ui)
76 88 rsp = wireproto.dispatch(repo, p, cmd)
77 89 if isinstance(rsp, str):
78 90 req.respond(HTTP_OK, HGTYPE, body=rsp)
79 91 return []
80 92 elif isinstance(rsp, wireproto.streamres):
81 93 req.respond(HTTP_OK, HGTYPE)
82 94 return rsp.gen
83 95 elif isinstance(rsp, wireproto.pushres):
84 96 val = p.restore()
85 97 rsp = '%d\n%s' % (rsp.res, val)
86 98 req.respond(HTTP_OK, HGTYPE, body=rsp)
87 99 return []
88 100 elif isinstance(rsp, wireproto.pusherr):
89 101 # drain the incoming bundle
90 102 req.drain()
91 103 p.restore()
92 104 rsp = '0\n%s\n' % rsp.res
93 105 req.respond(HTTP_OK, HGTYPE, body=rsp)
94 106 return []
95 107 elif isinstance(rsp, wireproto.ooberror):
96 108 rsp = rsp.message
97 109 req.respond(HTTP_OK, HGERRTYPE, body=rsp)
98 110 return []
@@ -1,140 +1,152 b''
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 import socket, cgi, errno
10 from mercurial import util
11 from common import ErrorResponse, statusmessage, HTTP_NOT_MODIFIED
9 from __future__ import absolute_import
10
11 import cgi
12 import errno
13 import socket
14
15 from .common import (
16 ErrorResponse,
17 HTTP_NOT_MODIFIED,
18 statusmessage,
19 )
20
21 from .. import (
22 util,
23 )
12 24
13 25 shortcuts = {
14 26 'cl': [('cmd', ['changelog']), ('rev', None)],
15 27 'sl': [('cmd', ['shortlog']), ('rev', None)],
16 28 'cs': [('cmd', ['changeset']), ('node', None)],
17 29 'f': [('cmd', ['file']), ('filenode', None)],
18 30 'fl': [('cmd', ['filelog']), ('filenode', None)],
19 31 'fd': [('cmd', ['filediff']), ('node', None)],
20 32 'fa': [('cmd', ['annotate']), ('filenode', None)],
21 33 'mf': [('cmd', ['manifest']), ('manifest', None)],
22 34 'ca': [('cmd', ['archive']), ('node', None)],
23 35 'tags': [('cmd', ['tags'])],
24 36 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
25 37 'static': [('cmd', ['static']), ('file', None)]
26 38 }
27 39
28 40 def normalize(form):
29 41 # first expand the shortcuts
30 42 for k in shortcuts.iterkeys():
31 43 if k in form:
32 44 for name, value in shortcuts[k]:
33 45 if value is None:
34 46 value = form[k]
35 47 form[name] = value
36 48 del form[k]
37 49 # And strip the values
38 50 for k, v in form.iteritems():
39 51 form[k] = [i.strip() for i in v]
40 52 return form
41 53
42 54 class wsgirequest(object):
43 55 """Higher-level API for a WSGI request.
44 56
45 57 WSGI applications are invoked with 2 arguments. They are used to
46 58 instantiate instances of this class, which provides higher-level APIs
47 59 for obtaining request parameters, writing HTTP output, etc.
48 60 """
49 61 def __init__(self, wsgienv, start_response):
50 62 version = wsgienv['wsgi.version']
51 63 if (version < (1, 0)) or (version >= (2, 0)):
52 64 raise RuntimeError("Unknown and unsupported WSGI version %d.%d"
53 65 % version)
54 66 self.inp = wsgienv['wsgi.input']
55 67 self.err = wsgienv['wsgi.errors']
56 68 self.threaded = wsgienv['wsgi.multithread']
57 69 self.multiprocess = wsgienv['wsgi.multiprocess']
58 70 self.run_once = wsgienv['wsgi.run_once']
59 71 self.env = wsgienv
60 72 self.form = normalize(cgi.parse(self.inp,
61 73 self.env,
62 74 keep_blank_values=1))
63 75 self._start_response = start_response
64 76 self.server_write = None
65 77 self.headers = []
66 78
67 79 def __iter__(self):
68 80 return iter([])
69 81
70 82 def read(self, count=-1):
71 83 return self.inp.read(count)
72 84
73 85 def drain(self):
74 86 '''need to read all data from request, httplib is half-duplex'''
75 87 length = int(self.env.get('CONTENT_LENGTH') or 0)
76 88 for s in util.filechunkiter(self.inp, limit=length):
77 89 pass
78 90
79 91 def respond(self, status, type, filename=None, body=None):
80 92 if self._start_response is not None:
81 93 self.headers.append(('Content-Type', type))
82 94 if filename:
83 95 filename = (filename.rpartition('/')[-1]
84 96 .replace('\\', '\\\\').replace('"', '\\"'))
85 97 self.headers.append(('Content-Disposition',
86 98 'inline; filename="%s"' % filename))
87 99 if body is not None:
88 100 self.headers.append(('Content-Length', str(len(body))))
89 101
90 102 for k, v in self.headers:
91 103 if not isinstance(v, str):
92 104 raise TypeError('header value must be string: %r' % (v,))
93 105
94 106 if isinstance(status, ErrorResponse):
95 107 self.headers.extend(status.headers)
96 108 if status.code == HTTP_NOT_MODIFIED:
97 109 # RFC 2616 Section 10.3.5: 304 Not Modified has cases where
98 110 # it MUST NOT include any headers other than these and no
99 111 # body
100 112 self.headers = [(k, v) for (k, v) in self.headers if
101 113 k in ('Date', 'ETag', 'Expires',
102 114 'Cache-Control', 'Vary')]
103 115 status = statusmessage(status.code, str(status))
104 116 elif status == 200:
105 117 status = '200 Script output follows'
106 118 elif isinstance(status, int):
107 119 status = statusmessage(status)
108 120
109 121 self.server_write = self._start_response(status, self.headers)
110 122 self._start_response = None
111 123 self.headers = []
112 124 if body is not None:
113 125 self.write(body)
114 126 self.server_write = None
115 127
116 128 def write(self, thing):
117 129 if thing:
118 130 try:
119 131 self.server_write(thing)
120 132 except socket.error as inst:
121 133 if inst[0] != errno.ECONNRESET:
122 134 raise
123 135
124 136 def writelines(self, lines):
125 137 for line in lines:
126 138 self.write(line)
127 139
128 140 def flush(self):
129 141 return None
130 142
131 143 def close(self):
132 144 return None
133 145
134 146 def wsgiapplication(app_maker):
135 147 '''For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
136 148 can and should now be used as a WSGI application.'''
137 149 application = app_maker()
138 150 def run_wsgi(env, respond):
139 151 return application(env, respond)
140 152 return run_wsgi
@@ -1,305 +1,322 b''
1 1 # hgweb/server.py - The standalone hg web server.
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 import os, sys, errno, urllib, BaseHTTPServer, socket, SocketServer, traceback
10 from mercurial import util, error
11 from mercurial.hgweb import common
12 from mercurial.i18n import _
9 from __future__ import absolute_import
10
11 import BaseHTTPServer
12 import SocketServer
13 import errno
14 import os
15 import socket
16 import sys
17 import traceback
18 import urllib
19
20 from ..i18n import _
21
22 from .. import (
23 error,
24 util,
25 )
26
27 from . import (
28 common,
29 )
13 30
14 31 def _splitURI(uri):
15 32 """Return path and query that has been split from uri
16 33
17 34 Just like CGI environment, the path is unquoted, the query is
18 35 not.
19 36 """
20 37 if '?' in uri:
21 38 path, query = uri.split('?', 1)
22 39 else:
23 40 path, query = uri, ''
24 41 return urllib.unquote(path), query
25 42
26 43 class _error_logger(object):
27 44 def __init__(self, handler):
28 45 self.handler = handler
29 46 def flush(self):
30 47 pass
31 48 def write(self, str):
32 49 self.writelines(str.split('\n'))
33 50 def writelines(self, seq):
34 51 for msg in seq:
35 52 self.handler.log_error("HG error: %s", msg)
36 53
37 54 class _httprequesthandler(BaseHTTPServer.BaseHTTPRequestHandler):
38 55
39 56 url_scheme = 'http'
40 57
41 58 @staticmethod
42 59 def preparehttpserver(httpserver, ssl_cert):
43 60 """Prepare .socket of new HTTPServer instance"""
44 61 pass
45 62
46 63 def __init__(self, *args, **kargs):
47 64 self.protocol_version = 'HTTP/1.1'
48 65 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, *args, **kargs)
49 66
50 67 def _log_any(self, fp, format, *args):
51 68 fp.write("%s - - [%s] %s\n" % (self.client_address[0],
52 69 self.log_date_time_string(),
53 70 format % args))
54 71 fp.flush()
55 72
56 73 def log_error(self, format, *args):
57 74 self._log_any(self.server.errorlog, format, *args)
58 75
59 76 def log_message(self, format, *args):
60 77 self._log_any(self.server.accesslog, format, *args)
61 78
62 79 def log_request(self, code='-', size='-'):
63 80 xheaders = []
64 81 if util.safehasattr(self, 'headers'):
65 82 xheaders = [h for h in self.headers.items()
66 83 if h[0].startswith('x-')]
67 84 self.log_message('"%s" %s %s%s',
68 85 self.requestline, str(code), str(size),
69 86 ''.join([' %s:%s' % h for h in sorted(xheaders)]))
70 87
71 88 def do_write(self):
72 89 try:
73 90 self.do_hgweb()
74 91 except socket.error as inst:
75 92 if inst[0] != errno.EPIPE:
76 93 raise
77 94
78 95 def do_POST(self):
79 96 try:
80 97 self.do_write()
81 98 except Exception:
82 99 self._start_response("500 Internal Server Error", [])
83 100 self._write("Internal Server Error")
84 101 self._done()
85 102 tb = "".join(traceback.format_exception(*sys.exc_info()))
86 103 self.log_error("Exception happened during processing "
87 104 "request '%s':\n%s", self.path, tb)
88 105
89 106 def do_GET(self):
90 107 self.do_POST()
91 108
92 109 def do_hgweb(self):
93 110 path, query = _splitURI(self.path)
94 111
95 112 env = {}
96 113 env['GATEWAY_INTERFACE'] = 'CGI/1.1'
97 114 env['REQUEST_METHOD'] = self.command
98 115 env['SERVER_NAME'] = self.server.server_name
99 116 env['SERVER_PORT'] = str(self.server.server_port)
100 117 env['REQUEST_URI'] = self.path
101 118 env['SCRIPT_NAME'] = self.server.prefix
102 119 env['PATH_INFO'] = path[len(self.server.prefix):]
103 120 env['REMOTE_HOST'] = self.client_address[0]
104 121 env['REMOTE_ADDR'] = self.client_address[0]
105 122 if query:
106 123 env['QUERY_STRING'] = query
107 124
108 125 if self.headers.typeheader is None:
109 126 env['CONTENT_TYPE'] = self.headers.type
110 127 else:
111 128 env['CONTENT_TYPE'] = self.headers.typeheader
112 129 length = self.headers.getheader('content-length')
113 130 if length:
114 131 env['CONTENT_LENGTH'] = length
115 132 for header in [h for h in self.headers.keys()
116 133 if h not in ('content-type', 'content-length')]:
117 134 hkey = 'HTTP_' + header.replace('-', '_').upper()
118 135 hval = self.headers.getheader(header)
119 136 hval = hval.replace('\n', '').strip()
120 137 if hval:
121 138 env[hkey] = hval
122 139 env['SERVER_PROTOCOL'] = self.request_version
123 140 env['wsgi.version'] = (1, 0)
124 141 env['wsgi.url_scheme'] = self.url_scheme
125 142 if env.get('HTTP_EXPECT', '').lower() == '100-continue':
126 143 self.rfile = common.continuereader(self.rfile, self.wfile.write)
127 144
128 145 env['wsgi.input'] = self.rfile
129 146 env['wsgi.errors'] = _error_logger(self)
130 147 env['wsgi.multithread'] = isinstance(self.server,
131 148 SocketServer.ThreadingMixIn)
132 149 env['wsgi.multiprocess'] = isinstance(self.server,
133 150 SocketServer.ForkingMixIn)
134 151 env['wsgi.run_once'] = 0
135 152
136 153 self.saved_status = None
137 154 self.saved_headers = []
138 155 self.sent_headers = False
139 156 self.length = None
140 157 self._chunked = None
141 158 for chunk in self.server.application(env, self._start_response):
142 159 self._write(chunk)
143 160 if not self.sent_headers:
144 161 self.send_headers()
145 162 self._done()
146 163
147 164 def send_headers(self):
148 165 if not self.saved_status:
149 166 raise AssertionError("Sending headers before "
150 167 "start_response() called")
151 168 saved_status = self.saved_status.split(None, 1)
152 169 saved_status[0] = int(saved_status[0])
153 170 self.send_response(*saved_status)
154 171 self.length = None
155 172 self._chunked = False
156 173 for h in self.saved_headers:
157 174 self.send_header(*h)
158 175 if h[0].lower() == 'content-length':
159 176 self.length = int(h[1])
160 177 if (self.length is None and
161 178 saved_status[0] != common.HTTP_NOT_MODIFIED):
162 179 self._chunked = (not self.close_connection and
163 180 self.request_version == "HTTP/1.1")
164 181 if self._chunked:
165 182 self.send_header('Transfer-Encoding', 'chunked')
166 183 else:
167 184 self.send_header('Connection', 'close')
168 185 self.end_headers()
169 186 self.sent_headers = True
170 187
171 188 def _start_response(self, http_status, headers, exc_info=None):
172 189 code, msg = http_status.split(None, 1)
173 190 code = int(code)
174 191 self.saved_status = http_status
175 192 bad_headers = ('connection', 'transfer-encoding')
176 193 self.saved_headers = [h for h in headers
177 194 if h[0].lower() not in bad_headers]
178 195 return self._write
179 196
180 197 def _write(self, data):
181 198 if not self.saved_status:
182 199 raise AssertionError("data written before start_response() called")
183 200 elif not self.sent_headers:
184 201 self.send_headers()
185 202 if self.length is not None:
186 203 if len(data) > self.length:
187 204 raise AssertionError("Content-length header sent, but more "
188 205 "bytes than specified are being written.")
189 206 self.length = self.length - len(data)
190 207 elif self._chunked and data:
191 208 data = '%x\r\n%s\r\n' % (len(data), data)
192 209 self.wfile.write(data)
193 210 self.wfile.flush()
194 211
195 212 def _done(self):
196 213 if self._chunked:
197 214 self.wfile.write('0\r\n\r\n')
198 215 self.wfile.flush()
199 216
200 217 class _httprequesthandlerssl(_httprequesthandler):
201 218 """HTTPS handler based on Python's ssl module"""
202 219
203 220 url_scheme = 'https'
204 221
205 222 @staticmethod
206 223 def preparehttpserver(httpserver, ssl_cert):
207 224 try:
208 225 import ssl
209 226 ssl.wrap_socket
210 227 except ImportError:
211 228 raise error.Abort(_("SSL support is unavailable"))
212 229 httpserver.socket = ssl.wrap_socket(
213 230 httpserver.socket, server_side=True,
214 231 certfile=ssl_cert, ssl_version=ssl.PROTOCOL_TLSv1)
215 232
216 233 def setup(self):
217 234 self.connection = self.request
218 235 self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
219 236 self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
220 237
221 238 try:
222 from threading import activeCount
223 activeCount() # silence pyflakes
239 import threading
240 threading.activeCount() # silence pyflakes and bypass demandimport
224 241 _mixin = SocketServer.ThreadingMixIn
225 242 except ImportError:
226 243 if util.safehasattr(os, "fork"):
227 244 _mixin = SocketServer.ForkingMixIn
228 245 else:
229 246 class _mixin(object):
230 247 pass
231 248
232 249 def openlog(opt, default):
233 250 if opt and opt != '-':
234 251 return open(opt, 'a')
235 252 return default
236 253
237 254 class MercurialHTTPServer(object, _mixin, BaseHTTPServer.HTTPServer):
238 255
239 256 # SO_REUSEADDR has broken semantics on windows
240 257 if os.name == 'nt':
241 258 allow_reuse_address = 0
242 259
243 260 def __init__(self, ui, app, addr, handler, **kwargs):
244 261 BaseHTTPServer.HTTPServer.__init__(self, addr, handler, **kwargs)
245 262 self.daemon_threads = True
246 263 self.application = app
247 264
248 265 handler.preparehttpserver(self, ui.config('web', 'certificate'))
249 266
250 267 prefix = ui.config('web', 'prefix', '')
251 268 if prefix:
252 269 prefix = '/' + prefix.strip('/')
253 270 self.prefix = prefix
254 271
255 272 alog = openlog(ui.config('web', 'accesslog', '-'), sys.stdout)
256 273 elog = openlog(ui.config('web', 'errorlog', '-'), sys.stderr)
257 274 self.accesslog = alog
258 275 self.errorlog = elog
259 276
260 277 self.addr, self.port = self.socket.getsockname()[0:2]
261 278 self.fqaddr = socket.getfqdn(addr[0])
262 279
263 280 class IPv6HTTPServer(MercurialHTTPServer):
264 281 address_family = getattr(socket, 'AF_INET6', None)
265 282 def __init__(self, *args, **kwargs):
266 283 if self.address_family is None:
267 284 raise error.RepoError(_('IPv6 is not available on this system'))
268 285 super(IPv6HTTPServer, self).__init__(*args, **kwargs)
269 286
270 287 def create_server(ui, app):
271 288
272 289 if ui.config('web', 'certificate'):
273 290 handler = _httprequesthandlerssl
274 291 else:
275 292 handler = _httprequesthandler
276 293
277 294 if ui.configbool('web', 'ipv6'):
278 295 cls = IPv6HTTPServer
279 296 else:
280 297 cls = MercurialHTTPServer
281 298
282 299 # ugly hack due to python issue5853 (for threaded use)
283 300 try:
284 301 import mimetypes
285 302 mimetypes.init()
286 303 except UnicodeDecodeError:
287 304 # Python 2.x's mimetypes module attempts to decode strings
288 305 # from Windows' ANSI APIs as ascii (fail), then re-encode them
289 306 # as ascii (clown fail), because the default Python Unicode
290 307 # codec is hardcoded as ascii.
291 308
292 309 sys.argv # unwrap demand-loader so that reload() works
293 310 reload(sys) # resurrect sys.setdefaultencoding()
294 311 oldenc = sys.getdefaultencoding()
295 312 sys.setdefaultencoding("latin1") # or any full 8-bit encoding
296 313 mimetypes.init()
297 314 sys.setdefaultencoding(oldenc)
298 315
299 316 address = ui.config('web', 'address', '')
300 317 port = util.getport(ui.config('web', 'port', 8000))
301 318 try:
302 319 return cls(ui, app, (address, port), handler)
303 320 except socket.error as inst:
304 321 raise error.Abort(_("cannot start server at '%s:%d': %s")
305 322 % (address, port, inst.args[1]))
@@ -1,1315 +1,1340 b''
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 import os, mimetypes, re, cgi, copy
9 import webutil
10 from mercurial import error, encoding, archival, templater, templatefilters
11 from mercurial.node import short, hex
12 from mercurial import util
13 from common import paritygen, staticfile, get_contact, ErrorResponse
14 from common import HTTP_OK, HTTP_FORBIDDEN, HTTP_NOT_FOUND
15 from mercurial import graphmod, patch
16 from mercurial import scmutil
17 from mercurial.i18n import _
18 from mercurial import revset
8 from __future__ import absolute_import
9
10 import cgi
11 import copy
12 import mimetypes
13 import os
14 import re
15
16 from ..i18n import _
17 from ..node import hex, short
18
19 from .common import (
20 ErrorResponse,
21 HTTP_FORBIDDEN,
22 HTTP_NOT_FOUND,
23 HTTP_OK,
24 get_contact,
25 paritygen,
26 staticfile,
27 )
28
29 from .. import (
30 archival,
31 encoding,
32 error,
33 graphmod,
34 patch,
35 revset,
36 scmutil,
37 templatefilters,
38 templater,
39 util,
40 )
41
42 from . import (
43 webutil,
44 )
19 45
20 46 __all__ = []
21 47 commands = {}
22 48
23 49 class webcommand(object):
24 50 """Decorator used to register a web command handler.
25 51
26 52 The decorator takes as its positional arguments the name/path the
27 53 command should be accessible under.
28 54
29 55 Usage:
30 56
31 57 @webcommand('mycommand')
32 58 def mycommand(web, req, tmpl):
33 59 pass
34 60 """
35 61
36 62 def __init__(self, name):
37 63 self.name = name
38 64
39 65 def __call__(self, func):
40 66 __all__.append(self.name)
41 67 commands[self.name] = func
42 68 return func
43 69
44 70 @webcommand('log')
45 71 def log(web, req, tmpl):
46 72 """
47 73 /log[/{revision}[/{path}]]
48 74 --------------------------
49 75
50 76 Show repository or file history.
51 77
52 78 For URLs of the form ``/log/{revision}``, a list of changesets starting at
53 79 the specified changeset identifier is shown. If ``{revision}`` is not
54 80 defined, the default is ``tip``. This form is equivalent to the
55 81 ``changelog`` handler.
56 82
57 83 For URLs of the form ``/log/{revision}/{file}``, the history for a specific
58 84 file will be shown. This form is equivalent to the ``filelog`` handler.
59 85 """
60 86
61 87 if 'file' in req.form and req.form['file'][0]:
62 88 return filelog(web, req, tmpl)
63 89 else:
64 90 return changelog(web, req, tmpl)
65 91
66 92 @webcommand('rawfile')
67 93 def rawfile(web, req, tmpl):
68 94 guessmime = web.configbool('web', 'guessmime', False)
69 95
70 96 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
71 97 if not path:
72 98 content = manifest(web, req, tmpl)
73 99 req.respond(HTTP_OK, web.ctype)
74 100 return content
75 101
76 102 try:
77 103 fctx = webutil.filectx(web.repo, req)
78 104 except error.LookupError as inst:
79 105 try:
80 106 content = manifest(web, req, tmpl)
81 107 req.respond(HTTP_OK, web.ctype)
82 108 return content
83 109 except ErrorResponse:
84 110 raise inst
85 111
86 112 path = fctx.path()
87 113 text = fctx.data()
88 114 mt = 'application/binary'
89 115 if guessmime:
90 116 mt = mimetypes.guess_type(path)[0]
91 117 if mt is None:
92 118 if util.binary(text):
93 119 mt = 'application/binary'
94 120 else:
95 121 mt = 'text/plain'
96 122 if mt.startswith('text/'):
97 123 mt += '; charset="%s"' % encoding.encoding
98 124
99 125 req.respond(HTTP_OK, mt, path, body=text)
100 126 return []
101 127
102 128 def _filerevision(web, req, tmpl, fctx):
103 129 f = fctx.path()
104 130 text = fctx.data()
105 131 parity = paritygen(web.stripecount)
106 132
107 133 if util.binary(text):
108 134 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
109 135 text = '(binary:%s)' % mt
110 136
111 137 def lines():
112 138 for lineno, t in enumerate(text.splitlines(True)):
113 139 yield {"line": t,
114 140 "lineid": "l%d" % (lineno + 1),
115 141 "linenumber": "% 6d" % (lineno + 1),
116 142 "parity": parity.next()}
117 143
118 144 return tmpl("filerevision",
119 145 file=f,
120 146 path=webutil.up(f),
121 147 text=lines(),
122 148 rev=fctx.rev(),
123 149 symrev=webutil.symrevorshortnode(req, fctx),
124 150 node=fctx.hex(),
125 151 author=fctx.user(),
126 152 date=fctx.date(),
127 153 desc=fctx.description(),
128 154 extra=fctx.extra(),
129 155 branch=webutil.nodebranchnodefault(fctx),
130 156 parent=webutil.parents(fctx),
131 157 child=webutil.children(fctx),
132 158 rename=webutil.renamelink(fctx),
133 159 tags=webutil.nodetagsdict(web.repo, fctx.node()),
134 160 bookmarks=webutil.nodebookmarksdict(web.repo, fctx.node()),
135 161 permissions=fctx.manifest().flags(f))
136 162
137 163 @webcommand('file')
138 164 def file(web, req, tmpl):
139 165 """
140 166 /file/{revision}[/{path}]
141 167 -------------------------
142 168
143 169 Show information about a directory or file in the repository.
144 170
145 171 Info about the ``path`` given as a URL parameter will be rendered.
146 172
147 173 If ``path`` is a directory, information about the entries in that
148 174 directory will be rendered. This form is equivalent to the ``manifest``
149 175 handler.
150 176
151 177 If ``path`` is a file, information about that file will be shown via
152 178 the ``filerevision`` template.
153 179
154 180 If ``path`` is not defined, information about the root directory will
155 181 be rendered.
156 182 """
157 183 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
158 184 if not path:
159 185 return manifest(web, req, tmpl)
160 186 try:
161 187 return _filerevision(web, req, tmpl, webutil.filectx(web.repo, req))
162 188 except error.LookupError as inst:
163 189 try:
164 190 return manifest(web, req, tmpl)
165 191 except ErrorResponse:
166 192 raise inst
167 193
168 194 def _search(web, req, tmpl):
169 195 MODE_REVISION = 'rev'
170 196 MODE_KEYWORD = 'keyword'
171 197 MODE_REVSET = 'revset'
172 198
173 199 def revsearch(ctx):
174 200 yield ctx
175 201
176 202 def keywordsearch(query):
177 203 lower = encoding.lower
178 204 qw = lower(query).split()
179 205
180 206 def revgen():
181 207 cl = web.repo.changelog
182 208 for i in xrange(len(web.repo) - 1, 0, -100):
183 209 l = []
184 210 for j in cl.revs(max(0, i - 99), i):
185 211 ctx = web.repo[j]
186 212 l.append(ctx)
187 213 l.reverse()
188 214 for e in l:
189 215 yield e
190 216
191 217 for ctx in revgen():
192 218 miss = 0
193 219 for q in qw:
194 220 if not (q in lower(ctx.user()) or
195 221 q in lower(ctx.description()) or
196 222 q in lower(" ".join(ctx.files()))):
197 223 miss = 1
198 224 break
199 225 if miss:
200 226 continue
201 227
202 228 yield ctx
203 229
204 230 def revsetsearch(revs):
205 231 for r in revs:
206 232 yield web.repo[r]
207 233
208 234 searchfuncs = {
209 235 MODE_REVISION: (revsearch, 'exact revision search'),
210 236 MODE_KEYWORD: (keywordsearch, 'literal keyword search'),
211 237 MODE_REVSET: (revsetsearch, 'revset expression search'),
212 238 }
213 239
214 240 def getsearchmode(query):
215 241 try:
216 242 ctx = web.repo[query]
217 243 except (error.RepoError, error.LookupError):
218 244 # query is not an exact revision pointer, need to
219 245 # decide if it's a revset expression or keywords
220 246 pass
221 247 else:
222 248 return MODE_REVISION, ctx
223 249
224 250 revdef = 'reverse(%s)' % query
225 251 try:
226 252 tree = revset.parse(revdef)
227 253 except error.ParseError:
228 254 # can't parse to a revset tree
229 255 return MODE_KEYWORD, query
230 256
231 257 if revset.depth(tree) <= 2:
232 258 # no revset syntax used
233 259 return MODE_KEYWORD, query
234 260
235 261 if any((token, (value or '')[:3]) == ('string', 're:')
236 262 for token, value, pos in revset.tokenize(revdef)):
237 263 return MODE_KEYWORD, query
238 264
239 265 funcsused = revset.funcsused(tree)
240 266 if not funcsused.issubset(revset.safesymbols):
241 267 return MODE_KEYWORD, query
242 268
243 269 mfunc = revset.match(web.repo.ui, revdef)
244 270 try:
245 271 revs = mfunc(web.repo)
246 272 return MODE_REVSET, revs
247 273 # ParseError: wrongly placed tokens, wrongs arguments, etc
248 274 # RepoLookupError: no such revision, e.g. in 'revision:'
249 275 # Abort: bookmark/tag not exists
250 276 # LookupError: ambiguous identifier, e.g. in '(bc)' on a large repo
251 277 except (error.ParseError, error.RepoLookupError, error.Abort,
252 278 LookupError):
253 279 return MODE_KEYWORD, query
254 280
255 281 def changelist(**map):
256 282 count = 0
257 283
258 284 for ctx in searchfunc[0](funcarg):
259 285 count += 1
260 286 n = ctx.node()
261 287 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
262 288 files = webutil.listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
263 289
264 290 yield tmpl('searchentry',
265 291 parity=parity.next(),
266 292 author=ctx.user(),
267 293 parent=lambda **x: webutil.parents(ctx),
268 294 child=lambda **x: webutil.children(ctx),
269 295 changelogtag=showtags,
270 296 desc=ctx.description(),
271 297 extra=ctx.extra(),
272 298 date=ctx.date(),
273 299 files=files,
274 300 rev=ctx.rev(),
275 301 node=hex(n),
276 302 tags=webutil.nodetagsdict(web.repo, n),
277 303 bookmarks=webutil.nodebookmarksdict(web.repo, n),
278 304 inbranch=webutil.nodeinbranch(web.repo, ctx),
279 305 branches=webutil.nodebranchdict(web.repo, ctx))
280 306
281 307 if count >= revcount:
282 308 break
283 309
284 310 query = req.form['rev'][0]
285 311 revcount = web.maxchanges
286 312 if 'revcount' in req.form:
287 313 try:
288 314 revcount = int(req.form.get('revcount', [revcount])[0])
289 315 revcount = max(revcount, 1)
290 316 tmpl.defaults['sessionvars']['revcount'] = revcount
291 317 except ValueError:
292 318 pass
293 319
294 320 lessvars = copy.copy(tmpl.defaults['sessionvars'])
295 321 lessvars['revcount'] = max(revcount / 2, 1)
296 322 lessvars['rev'] = query
297 323 morevars = copy.copy(tmpl.defaults['sessionvars'])
298 324 morevars['revcount'] = revcount * 2
299 325 morevars['rev'] = query
300 326
301 327 mode, funcarg = getsearchmode(query)
302 328
303 329 if 'forcekw' in req.form:
304 330 showforcekw = ''
305 331 showunforcekw = searchfuncs[mode][1]
306 332 mode = MODE_KEYWORD
307 333 funcarg = query
308 334 else:
309 335 if mode != MODE_KEYWORD:
310 336 showforcekw = searchfuncs[MODE_KEYWORD][1]
311 337 else:
312 338 showforcekw = ''
313 339 showunforcekw = ''
314 340
315 341 searchfunc = searchfuncs[mode]
316 342
317 343 tip = web.repo['tip']
318 344 parity = paritygen(web.stripecount)
319 345
320 346 return tmpl('search', query=query, node=tip.hex(), symrev='tip',
321 347 entries=changelist, archives=web.archivelist("tip"),
322 348 morevars=morevars, lessvars=lessvars,
323 349 modedesc=searchfunc[1],
324 350 showforcekw=showforcekw, showunforcekw=showunforcekw)
325 351
326 352 @webcommand('changelog')
327 353 def changelog(web, req, tmpl, shortlog=False):
328 354 """
329 355 /changelog[/{revision}]
330 356 -----------------------
331 357
332 358 Show information about multiple changesets.
333 359
334 360 If the optional ``revision`` URL argument is absent, information about
335 361 all changesets starting at ``tip`` will be rendered. If the ``revision``
336 362 argument is present, changesets will be shown starting from the specified
337 363 revision.
338 364
339 365 If ``revision`` is absent, the ``rev`` query string argument may be
340 366 defined. This will perform a search for changesets.
341 367
342 368 The argument for ``rev`` can be a single revision, a revision set,
343 369 or a literal keyword to search for in changeset data (equivalent to
344 370 :hg:`log -k`).
345 371
346 372 The ``revcount`` query string argument defines the maximum numbers of
347 373 changesets to render.
348 374
349 375 For non-searches, the ``changelog`` template will be rendered.
350 376 """
351 377
352 378 query = ''
353 379 if 'node' in req.form:
354 380 ctx = webutil.changectx(web.repo, req)
355 381 symrev = webutil.symrevorshortnode(req, ctx)
356 382 elif 'rev' in req.form:
357 383 return _search(web, req, tmpl)
358 384 else:
359 385 ctx = web.repo['tip']
360 386 symrev = 'tip'
361 387
362 388 def changelist():
363 389 revs = []
364 390 if pos != -1:
365 391 revs = web.repo.changelog.revs(pos, 0)
366 392 curcount = 0
367 393 for rev in revs:
368 394 curcount += 1
369 395 if curcount > revcount + 1:
370 396 break
371 397
372 398 entry = webutil.changelistentry(web, web.repo[rev], tmpl)
373 399 entry['parity'] = parity.next()
374 400 yield entry
375 401
376 402 if shortlog:
377 403 revcount = web.maxshortchanges
378 404 else:
379 405 revcount = web.maxchanges
380 406
381 407 if 'revcount' in req.form:
382 408 try:
383 409 revcount = int(req.form.get('revcount', [revcount])[0])
384 410 revcount = max(revcount, 1)
385 411 tmpl.defaults['sessionvars']['revcount'] = revcount
386 412 except ValueError:
387 413 pass
388 414
389 415 lessvars = copy.copy(tmpl.defaults['sessionvars'])
390 416 lessvars['revcount'] = max(revcount / 2, 1)
391 417 morevars = copy.copy(tmpl.defaults['sessionvars'])
392 418 morevars['revcount'] = revcount * 2
393 419
394 420 count = len(web.repo)
395 421 pos = ctx.rev()
396 422 parity = paritygen(web.stripecount)
397 423
398 424 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
399 425
400 426 entries = list(changelist())
401 427 latestentry = entries[:1]
402 428 if len(entries) > revcount:
403 429 nextentry = entries[-1:]
404 430 entries = entries[:-1]
405 431 else:
406 432 nextentry = []
407 433
408 434 return tmpl(shortlog and 'shortlog' or 'changelog', changenav=changenav,
409 435 node=ctx.hex(), rev=pos, symrev=symrev, changesets=count,
410 436 entries=entries,
411 437 latestentry=latestentry, nextentry=nextentry,
412 438 archives=web.archivelist("tip"), revcount=revcount,
413 439 morevars=morevars, lessvars=lessvars, query=query)
414 440
415 441 @webcommand('shortlog')
416 442 def shortlog(web, req, tmpl):
417 443 """
418 444 /shortlog
419 445 ---------
420 446
421 447 Show basic information about a set of changesets.
422 448
423 449 This accepts the same parameters as the ``changelog`` handler. The only
424 450 difference is the ``shortlog`` template will be rendered instead of the
425 451 ``changelog`` template.
426 452 """
427 453 return changelog(web, req, tmpl, shortlog=True)
428 454
429 455 @webcommand('changeset')
430 456 def changeset(web, req, tmpl):
431 457 """
432 458 /changeset[/{revision}]
433 459 -----------------------
434 460
435 461 Show information about a single changeset.
436 462
437 463 A URL path argument is the changeset identifier to show. See ``hg help
438 464 revisions`` for possible values. If not defined, the ``tip`` changeset
439 465 will be shown.
440 466
441 467 The ``changeset`` template is rendered. Contents of the ``changesettag``,
442 468 ``changesetbookmark``, ``filenodelink``, ``filenolink``, and the many
443 469 templates related to diffs may all be used to produce the output.
444 470 """
445 471 ctx = webutil.changectx(web.repo, req)
446 472
447 473 return tmpl('changeset', **webutil.changesetentry(web, req, tmpl, ctx))
448 474
449 475 rev = webcommand('rev')(changeset)
450 476
451 477 def decodepath(path):
452 478 """Hook for mapping a path in the repository to a path in the
453 479 working copy.
454 480
455 481 Extensions (e.g., largefiles) can override this to remap files in
456 482 the virtual file system presented by the manifest command below."""
457 483 return path
458 484
459 485 @webcommand('manifest')
460 486 def manifest(web, req, tmpl):
461 487 """
462 488 /manifest[/{revision}[/{path}]]
463 489 -------------------------------
464 490
465 491 Show information about a directory.
466 492
467 493 If the URL path arguments are omitted, information about the root
468 494 directory for the ``tip`` changeset will be shown.
469 495
470 496 Because this handler can only show information for directories, it
471 497 is recommended to use the ``file`` handler instead, as it can handle both
472 498 directories and files.
473 499
474 500 The ``manifest`` template will be rendered for this handler.
475 501 """
476 502 if 'node' in req.form:
477 503 ctx = webutil.changectx(web.repo, req)
478 504 symrev = webutil.symrevorshortnode(req, ctx)
479 505 else:
480 506 ctx = web.repo['tip']
481 507 symrev = 'tip'
482 508 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
483 509 mf = ctx.manifest()
484 510 node = ctx.node()
485 511
486 512 files = {}
487 513 dirs = {}
488 514 parity = paritygen(web.stripecount)
489 515
490 516 if path and path[-1] != "/":
491 517 path += "/"
492 518 l = len(path)
493 519 abspath = "/" + path
494 520
495 521 for full, n in mf.iteritems():
496 522 # the virtual path (working copy path) used for the full
497 523 # (repository) path
498 524 f = decodepath(full)
499 525
500 526 if f[:l] != path:
501 527 continue
502 528 remain = f[l:]
503 529 elements = remain.split('/')
504 530 if len(elements) == 1:
505 531 files[remain] = full
506 532 else:
507 533 h = dirs # need to retain ref to dirs (root)
508 534 for elem in elements[0:-1]:
509 535 if elem not in h:
510 536 h[elem] = {}
511 537 h = h[elem]
512 538 if len(h) > 1:
513 539 break
514 540 h[None] = None # denotes files present
515 541
516 542 if mf and not files and not dirs:
517 543 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
518 544
519 545 def filelist(**map):
520 546 for f in sorted(files):
521 547 full = files[f]
522 548
523 549 fctx = ctx.filectx(full)
524 550 yield {"file": full,
525 551 "parity": parity.next(),
526 552 "basename": f,
527 553 "date": fctx.date(),
528 554 "size": fctx.size(),
529 555 "permissions": mf.flags(full)}
530 556
531 557 def dirlist(**map):
532 558 for d in sorted(dirs):
533 559
534 560 emptydirs = []
535 561 h = dirs[d]
536 562 while isinstance(h, dict) and len(h) == 1:
537 563 k, v = h.items()[0]
538 564 if v:
539 565 emptydirs.append(k)
540 566 h = v
541 567
542 568 path = "%s%s" % (abspath, d)
543 569 yield {"parity": parity.next(),
544 570 "path": path,
545 571 "emptydirs": "/".join(emptydirs),
546 572 "basename": d}
547 573
548 574 return tmpl("manifest",
549 575 rev=ctx.rev(),
550 576 symrev=symrev,
551 577 node=hex(node),
552 578 path=abspath,
553 579 up=webutil.up(abspath),
554 580 upparity=parity.next(),
555 581 fentries=filelist,
556 582 dentries=dirlist,
557 583 archives=web.archivelist(hex(node)),
558 584 tags=webutil.nodetagsdict(web.repo, node),
559 585 bookmarks=webutil.nodebookmarksdict(web.repo, node),
560 586 branch=webutil.nodebranchnodefault(ctx),
561 587 inbranch=webutil.nodeinbranch(web.repo, ctx),
562 588 branches=webutil.nodebranchdict(web.repo, ctx))
563 589
564 590 @webcommand('tags')
565 591 def tags(web, req, tmpl):
566 592 """
567 593 /tags
568 594 -----
569 595
570 596 Show information about tags.
571 597
572 598 No arguments are accepted.
573 599
574 600 The ``tags`` template is rendered.
575 601 """
576 602 i = list(reversed(web.repo.tagslist()))
577 603 parity = paritygen(web.stripecount)
578 604
579 605 def entries(notip, latestonly, **map):
580 606 t = i
581 607 if notip:
582 608 t = [(k, n) for k, n in i if k != "tip"]
583 609 if latestonly:
584 610 t = t[:1]
585 611 for k, n in t:
586 612 yield {"parity": parity.next(),
587 613 "tag": k,
588 614 "date": web.repo[n].date(),
589 615 "node": hex(n)}
590 616
591 617 return tmpl("tags",
592 618 node=hex(web.repo.changelog.tip()),
593 619 entries=lambda **x: entries(False, False, **x),
594 620 entriesnotip=lambda **x: entries(True, False, **x),
595 621 latestentry=lambda **x: entries(True, True, **x))
596 622
597 623 @webcommand('bookmarks')
598 624 def bookmarks(web, req, tmpl):
599 625 """
600 626 /bookmarks
601 627 ----------
602 628
603 629 Show information about bookmarks.
604 630
605 631 No arguments are accepted.
606 632
607 633 The ``bookmarks`` template is rendered.
608 634 """
609 635 i = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
610 636 parity = paritygen(web.stripecount)
611 637
612 638 def entries(latestonly, **map):
613 639 if latestonly:
614 640 t = [min(i)]
615 641 else:
616 642 t = sorted(i)
617 643 for k, n in t:
618 644 yield {"parity": parity.next(),
619 645 "bookmark": k,
620 646 "date": web.repo[n].date(),
621 647 "node": hex(n)}
622 648
623 649 return tmpl("bookmarks",
624 650 node=hex(web.repo.changelog.tip()),
625 651 entries=lambda **x: entries(latestonly=False, **x),
626 652 latestentry=lambda **x: entries(latestonly=True, **x))
627 653
628 654 @webcommand('branches')
629 655 def branches(web, req, tmpl):
630 656 """
631 657 /branches
632 658 ---------
633 659
634 660 Show information about branches.
635 661
636 662 All known branches are contained in the output, even closed branches.
637 663
638 664 No arguments are accepted.
639 665
640 666 The ``branches`` template is rendered.
641 667 """
642 668 entries = webutil.branchentries(web.repo, web.stripecount)
643 669 latestentry = webutil.branchentries(web.repo, web.stripecount, 1)
644 670 return tmpl('branches', node=hex(web.repo.changelog.tip()),
645 671 entries=entries, latestentry=latestentry)
646 672
647 673 @webcommand('summary')
648 674 def summary(web, req, tmpl):
649 675 """
650 676 /summary
651 677 --------
652 678
653 679 Show a summary of repository state.
654 680
655 681 Information about the latest changesets, bookmarks, tags, and branches
656 682 is captured by this handler.
657 683
658 684 The ``summary`` template is rendered.
659 685 """
660 686 i = reversed(web.repo.tagslist())
661 687
662 688 def tagentries(**map):
663 689 parity = paritygen(web.stripecount)
664 690 count = 0
665 691 for k, n in i:
666 692 if k == "tip": # skip tip
667 693 continue
668 694
669 695 count += 1
670 696 if count > 10: # limit to 10 tags
671 697 break
672 698
673 699 yield tmpl("tagentry",
674 700 parity=parity.next(),
675 701 tag=k,
676 702 node=hex(n),
677 703 date=web.repo[n].date())
678 704
679 705 def bookmarks(**map):
680 706 parity = paritygen(web.stripecount)
681 707 marks = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
682 708 for k, n in sorted(marks)[:10]: # limit to 10 bookmarks
683 709 yield {'parity': parity.next(),
684 710 'bookmark': k,
685 711 'date': web.repo[n].date(),
686 712 'node': hex(n)}
687 713
688 714 def changelist(**map):
689 715 parity = paritygen(web.stripecount, offset=start - end)
690 716 l = [] # build a list in forward order for efficiency
691 717 revs = []
692 718 if start < end:
693 719 revs = web.repo.changelog.revs(start, end - 1)
694 720 for i in revs:
695 721 ctx = web.repo[i]
696 722 n = ctx.node()
697 723 hn = hex(n)
698 724
699 725 l.append(tmpl(
700 726 'shortlogentry',
701 727 parity=parity.next(),
702 728 author=ctx.user(),
703 729 desc=ctx.description(),
704 730 extra=ctx.extra(),
705 731 date=ctx.date(),
706 732 rev=i,
707 733 node=hn,
708 734 tags=webutil.nodetagsdict(web.repo, n),
709 735 bookmarks=webutil.nodebookmarksdict(web.repo, n),
710 736 inbranch=webutil.nodeinbranch(web.repo, ctx),
711 737 branches=webutil.nodebranchdict(web.repo, ctx)))
712 738
713 739 l.reverse()
714 740 yield l
715 741
716 742 tip = web.repo['tip']
717 743 count = len(web.repo)
718 744 start = max(0, count - web.maxchanges)
719 745 end = min(count, start + web.maxchanges)
720 746
721 747 return tmpl("summary",
722 748 desc=web.config("web", "description", "unknown"),
723 749 owner=get_contact(web.config) or "unknown",
724 750 lastchange=tip.date(),
725 751 tags=tagentries,
726 752 bookmarks=bookmarks,
727 753 branches=webutil.branchentries(web.repo, web.stripecount, 10),
728 754 shortlog=changelist,
729 755 node=tip.hex(),
730 756 symrev='tip',
731 757 archives=web.archivelist("tip"))
732 758
733 759 @webcommand('filediff')
734 760 def filediff(web, req, tmpl):
735 761 """
736 762 /diff/{revision}/{path}
737 763 -----------------------
738 764
739 765 Show how a file changed in a particular commit.
740 766
741 767 The ``filediff`` template is rendered.
742 768
743 769 This handler is registered under both the ``/diff`` and ``/filediff``
744 770 paths. ``/diff`` is used in modern code.
745 771 """
746 772 fctx, ctx = None, None
747 773 try:
748 774 fctx = webutil.filectx(web.repo, req)
749 775 except LookupError:
750 776 ctx = webutil.changectx(web.repo, req)
751 777 path = webutil.cleanpath(web.repo, req.form['file'][0])
752 778 if path not in ctx.files():
753 779 raise
754 780
755 781 if fctx is not None:
756 782 n = fctx.node()
757 783 path = fctx.path()
758 784 ctx = fctx.changectx()
759 785 else:
760 786 n = ctx.node()
761 787 # path already defined in except clause
762 788
763 789 parity = paritygen(web.stripecount)
764 790 style = web.config('web', 'style', 'paper')
765 791 if 'style' in req.form:
766 792 style = req.form['style'][0]
767 793
768 794 diffs = webutil.diffs(web.repo, tmpl, ctx, None, [path], parity, style)
769 795 if fctx:
770 796 rename = webutil.renamelink(fctx)
771 797 ctx = fctx
772 798 else:
773 799 rename = []
774 800 ctx = ctx
775 801 return tmpl("filediff",
776 802 file=path,
777 803 node=hex(n),
778 804 rev=ctx.rev(),
779 805 symrev=webutil.symrevorshortnode(req, ctx),
780 806 date=ctx.date(),
781 807 desc=ctx.description(),
782 808 extra=ctx.extra(),
783 809 author=ctx.user(),
784 810 rename=rename,
785 811 branch=webutil.nodebranchnodefault(ctx),
786 812 parent=webutil.parents(ctx),
787 813 child=webutil.children(ctx),
788 814 tags=webutil.nodetagsdict(web.repo, n),
789 815 bookmarks=webutil.nodebookmarksdict(web.repo, n),
790 816 diff=diffs)
791 817
792 818 diff = webcommand('diff')(filediff)
793 819
794 820 @webcommand('comparison')
795 821 def comparison(web, req, tmpl):
796 822 """
797 823 /comparison/{revision}/{path}
798 824 -----------------------------
799 825
800 826 Show a comparison between the old and new versions of a file from changes
801 827 made on a particular revision.
802 828
803 829 This is similar to the ``diff`` handler. However, this form features
804 830 a split or side-by-side diff rather than a unified diff.
805 831
806 832 The ``context`` query string argument can be used to control the lines of
807 833 context in the diff.
808 834
809 835 The ``filecomparison`` template is rendered.
810 836 """
811 837 ctx = webutil.changectx(web.repo, req)
812 838 if 'file' not in req.form:
813 839 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
814 840 path = webutil.cleanpath(web.repo, req.form['file'][0])
815 841 rename = path in ctx and webutil.renamelink(ctx[path]) or []
816 842
817 843 parsecontext = lambda v: v == 'full' and -1 or int(v)
818 844 if 'context' in req.form:
819 845 context = parsecontext(req.form['context'][0])
820 846 else:
821 847 context = parsecontext(web.config('web', 'comparisoncontext', '5'))
822 848
823 849 def filelines(f):
824 850 if util.binary(f.data()):
825 851 mt = mimetypes.guess_type(f.path())[0]
826 852 if not mt:
827 853 mt = 'application/octet-stream'
828 854 return [_('(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
829 855 return f.data().splitlines()
830 856
831 857 parent = ctx.p1()
832 858 leftrev = parent.rev()
833 859 leftnode = parent.node()
834 860 rightrev = ctx.rev()
835 861 rightnode = ctx.node()
836 862 if path in ctx:
837 863 fctx = ctx[path]
838 864 rightlines = filelines(fctx)
839 865 if path not in parent:
840 866 leftlines = ()
841 867 else:
842 868 pfctx = parent[path]
843 869 leftlines = filelines(pfctx)
844 870 else:
845 871 rightlines = ()
846 872 fctx = ctx.parents()[0][path]
847 873 leftlines = filelines(fctx)
848 874
849 875 comparison = webutil.compare(tmpl, context, leftlines, rightlines)
850 876 return tmpl('filecomparison',
851 877 file=path,
852 878 node=hex(ctx.node()),
853 879 rev=ctx.rev(),
854 880 symrev=webutil.symrevorshortnode(req, ctx),
855 881 date=ctx.date(),
856 882 desc=ctx.description(),
857 883 extra=ctx.extra(),
858 884 author=ctx.user(),
859 885 rename=rename,
860 886 branch=webutil.nodebranchnodefault(ctx),
861 887 parent=webutil.parents(fctx),
862 888 child=webutil.children(fctx),
863 889 tags=webutil.nodetagsdict(web.repo, ctx.node()),
864 890 bookmarks=webutil.nodebookmarksdict(web.repo, ctx.node()),
865 891 leftrev=leftrev,
866 892 leftnode=hex(leftnode),
867 893 rightrev=rightrev,
868 894 rightnode=hex(rightnode),
869 895 comparison=comparison)
870 896
871 897 @webcommand('annotate')
872 898 def annotate(web, req, tmpl):
873 899 """
874 900 /annotate/{revision}/{path}
875 901 ---------------------------
876 902
877 903 Show changeset information for each line in a file.
878 904
879 905 The ``fileannotate`` template is rendered.
880 906 """
881 907 fctx = webutil.filectx(web.repo, req)
882 908 f = fctx.path()
883 909 parity = paritygen(web.stripecount)
884 910 diffopts = patch.difffeatureopts(web.repo.ui, untrusted=True,
885 911 section='annotate', whitespace=True)
886 912
887 913 def annotate(**map):
888 914 last = None
889 915 if util.binary(fctx.data()):
890 916 mt = (mimetypes.guess_type(fctx.path())[0]
891 917 or 'application/octet-stream')
892 918 lines = enumerate([((fctx.filectx(fctx.filerev()), 1),
893 919 '(binary:%s)' % mt)])
894 920 else:
895 921 lines = enumerate(fctx.annotate(follow=True, linenumber=True,
896 922 diffopts=diffopts))
897 923 for lineno, ((f, targetline), l) in lines:
898 924 fnode = f.filenode()
899 925
900 926 if last != fnode:
901 927 last = fnode
902 928
903 929 yield {"parity": parity.next(),
904 930 "node": f.hex(),
905 931 "rev": f.rev(),
906 932 "author": f.user(),
907 933 "desc": f.description(),
908 934 "extra": f.extra(),
909 935 "file": f.path(),
910 936 "targetline": targetline,
911 937 "line": l,
912 938 "lineno": lineno + 1,
913 939 "lineid": "l%d" % (lineno + 1),
914 940 "linenumber": "% 6d" % (lineno + 1),
915 941 "revdate": f.date()}
916 942
917 943 return tmpl("fileannotate",
918 944 file=f,
919 945 annotate=annotate,
920 946 path=webutil.up(f),
921 947 rev=fctx.rev(),
922 948 symrev=webutil.symrevorshortnode(req, fctx),
923 949 node=fctx.hex(),
924 950 author=fctx.user(),
925 951 date=fctx.date(),
926 952 desc=fctx.description(),
927 953 extra=fctx.extra(),
928 954 rename=webutil.renamelink(fctx),
929 955 branch=webutil.nodebranchnodefault(fctx),
930 956 parent=webutil.parents(fctx),
931 957 child=webutil.children(fctx),
932 958 tags=webutil.nodetagsdict(web.repo, fctx.node()),
933 959 bookmarks=webutil.nodebookmarksdict(web.repo, fctx.node()),
934 960 permissions=fctx.manifest().flags(f))
935 961
936 962 @webcommand('filelog')
937 963 def filelog(web, req, tmpl):
938 964 """
939 965 /filelog/{revision}/{path}
940 966 --------------------------
941 967
942 968 Show information about the history of a file in the repository.
943 969
944 970 The ``revcount`` query string argument can be defined to control the
945 971 maximum number of entries to show.
946 972
947 973 The ``filelog`` template will be rendered.
948 974 """
949 975
950 976 try:
951 977 fctx = webutil.filectx(web.repo, req)
952 978 f = fctx.path()
953 979 fl = fctx.filelog()
954 980 except error.LookupError:
955 981 f = webutil.cleanpath(web.repo, req.form['file'][0])
956 982 fl = web.repo.file(f)
957 983 numrevs = len(fl)
958 984 if not numrevs: # file doesn't exist at all
959 985 raise
960 986 rev = webutil.changectx(web.repo, req).rev()
961 987 first = fl.linkrev(0)
962 988 if rev < first: # current rev is from before file existed
963 989 raise
964 990 frev = numrevs - 1
965 991 while fl.linkrev(frev) > rev:
966 992 frev -= 1
967 993 fctx = web.repo.filectx(f, fl.linkrev(frev))
968 994
969 995 revcount = web.maxshortchanges
970 996 if 'revcount' in req.form:
971 997 try:
972 998 revcount = int(req.form.get('revcount', [revcount])[0])
973 999 revcount = max(revcount, 1)
974 1000 tmpl.defaults['sessionvars']['revcount'] = revcount
975 1001 except ValueError:
976 1002 pass
977 1003
978 1004 lessvars = copy.copy(tmpl.defaults['sessionvars'])
979 1005 lessvars['revcount'] = max(revcount / 2, 1)
980 1006 morevars = copy.copy(tmpl.defaults['sessionvars'])
981 1007 morevars['revcount'] = revcount * 2
982 1008
983 1009 count = fctx.filerev() + 1
984 1010 start = max(0, fctx.filerev() - revcount + 1) # first rev on this page
985 1011 end = min(count, start + revcount) # last rev on this page
986 1012 parity = paritygen(web.stripecount, offset=start - end)
987 1013
988 1014 def entries():
989 1015 l = []
990 1016
991 1017 repo = web.repo
992 1018 revs = fctx.filelog().revs(start, end - 1)
993 1019 for i in revs:
994 1020 iterfctx = fctx.filectx(i)
995 1021
996 1022 l.append({"parity": parity.next(),
997 1023 "filerev": i,
998 1024 "file": f,
999 1025 "node": iterfctx.hex(),
1000 1026 "author": iterfctx.user(),
1001 1027 "date": iterfctx.date(),
1002 1028 "rename": webutil.renamelink(iterfctx),
1003 1029 "parent": lambda **x: webutil.parents(iterfctx),
1004 1030 "child": lambda **x: webutil.children(iterfctx),
1005 1031 "desc": iterfctx.description(),
1006 1032 "extra": iterfctx.extra(),
1007 1033 "tags": webutil.nodetagsdict(repo, iterfctx.node()),
1008 1034 "bookmarks": webutil.nodebookmarksdict(
1009 1035 repo, iterfctx.node()),
1010 1036 "branch": webutil.nodebranchnodefault(iterfctx),
1011 1037 "inbranch": webutil.nodeinbranch(repo, iterfctx),
1012 1038 "branches": webutil.nodebranchdict(repo, iterfctx)})
1013 1039 for e in reversed(l):
1014 1040 yield e
1015 1041
1016 1042 entries = list(entries())
1017 1043 latestentry = entries[:1]
1018 1044
1019 1045 revnav = webutil.filerevnav(web.repo, fctx.path())
1020 1046 nav = revnav.gen(end - 1, revcount, count)
1021 1047 return tmpl("filelog", file=f, node=fctx.hex(), nav=nav,
1022 1048 symrev=webutil.symrevorshortnode(req, fctx),
1023 1049 entries=entries,
1024 1050 latestentry=latestentry,
1025 1051 revcount=revcount, morevars=morevars, lessvars=lessvars)
1026 1052
1027 1053 @webcommand('archive')
1028 1054 def archive(web, req, tmpl):
1029 1055 """
1030 1056 /archive/{revision}.{format}[/{path}]
1031 1057 -------------------------------------
1032 1058
1033 1059 Obtain an archive of repository content.
1034 1060
1035 1061 The content and type of the archive is defined by a URL path parameter.
1036 1062 ``format`` is the file extension of the archive type to be generated. e.g.
1037 1063 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1038 1064 server configuration.
1039 1065
1040 1066 The optional ``path`` URL parameter controls content to include in the
1041 1067 archive. If omitted, every file in the specified revision is present in the
1042 1068 archive. If included, only the specified file or contents of the specified
1043 1069 directory will be included in the archive.
1044 1070
1045 1071 No template is used for this handler. Raw, binary content is generated.
1046 1072 """
1047 1073
1048 1074 type_ = req.form.get('type', [None])[0]
1049 1075 allowed = web.configlist("web", "allow_archive")
1050 1076 key = req.form['node'][0]
1051 1077
1052 1078 if type_ not in web.archives:
1053 1079 msg = 'Unsupported archive type: %s' % type_
1054 1080 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1055 1081
1056 1082 if not ((type_ in allowed or
1057 1083 web.configbool("web", "allow" + type_, False))):
1058 1084 msg = 'Archive type not allowed: %s' % type_
1059 1085 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1060 1086
1061 1087 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
1062 1088 cnode = web.repo.lookup(key)
1063 1089 arch_version = key
1064 1090 if cnode == key or key == 'tip':
1065 1091 arch_version = short(cnode)
1066 1092 name = "%s-%s" % (reponame, arch_version)
1067 1093
1068 1094 ctx = webutil.changectx(web.repo, req)
1069 1095 pats = []
1070 1096 matchfn = scmutil.match(ctx, [])
1071 1097 file = req.form.get('file', None)
1072 1098 if file:
1073 1099 pats = ['path:' + file[0]]
1074 1100 matchfn = scmutil.match(ctx, pats, default='path')
1075 1101 if pats:
1076 1102 files = [f for f in ctx.manifest().keys() if matchfn(f)]
1077 1103 if not files:
1078 1104 raise ErrorResponse(HTTP_NOT_FOUND,
1079 1105 'file(s) not found: %s' % file[0])
1080 1106
1081 1107 mimetype, artype, extension, encoding = web.archivespecs[type_]
1082 1108 headers = [
1083 1109 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
1084 1110 ]
1085 1111 if encoding:
1086 1112 headers.append(('Content-Encoding', encoding))
1087 1113 req.headers.extend(headers)
1088 1114 req.respond(HTTP_OK, mimetype)
1089 1115
1090 1116 archival.archive(web.repo, req, cnode, artype, prefix=name,
1091 1117 matchfn=matchfn,
1092 1118 subrepos=web.configbool("web", "archivesubrepos"))
1093 1119 return []
1094 1120
1095 1121
1096 1122 @webcommand('static')
1097 1123 def static(web, req, tmpl):
1098 1124 fname = req.form['file'][0]
1099 1125 # a repo owner may set web.static in .hg/hgrc to get any file
1100 1126 # readable by the user running the CGI script
1101 1127 static = web.config("web", "static", None, untrusted=False)
1102 1128 if not static:
1103 1129 tp = web.templatepath or templater.templatepaths()
1104 1130 if isinstance(tp, str):
1105 1131 tp = [tp]
1106 1132 static = [os.path.join(p, 'static') for p in tp]
1107 1133 staticfile(static, fname, req)
1108 1134 return []
1109 1135
1110 1136 @webcommand('graph')
1111 1137 def graph(web, req, tmpl):
1112 1138 """
1113 1139 /graph[/{revision}]
1114 1140 -------------------
1115 1141
1116 1142 Show information about the graphical topology of the repository.
1117 1143
1118 1144 Information rendered by this handler can be used to create visual
1119 1145 representations of repository topology.
1120 1146
1121 1147 The ``revision`` URL parameter controls the starting changeset.
1122 1148
1123 1149 The ``revcount`` query string argument can define the number of changesets
1124 1150 to show information for.
1125 1151
1126 1152 This handler will render the ``graph`` template.
1127 1153 """
1128 1154
1129 1155 if 'node' in req.form:
1130 1156 ctx = webutil.changectx(web.repo, req)
1131 1157 symrev = webutil.symrevorshortnode(req, ctx)
1132 1158 else:
1133 1159 ctx = web.repo['tip']
1134 1160 symrev = 'tip'
1135 1161 rev = ctx.rev()
1136 1162
1137 1163 bg_height = 39
1138 1164 revcount = web.maxshortchanges
1139 1165 if 'revcount' in req.form:
1140 1166 try:
1141 1167 revcount = int(req.form.get('revcount', [revcount])[0])
1142 1168 revcount = max(revcount, 1)
1143 1169 tmpl.defaults['sessionvars']['revcount'] = revcount
1144 1170 except ValueError:
1145 1171 pass
1146 1172
1147 1173 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1148 1174 lessvars['revcount'] = max(revcount / 2, 1)
1149 1175 morevars = copy.copy(tmpl.defaults['sessionvars'])
1150 1176 morevars['revcount'] = revcount * 2
1151 1177
1152 1178 count = len(web.repo)
1153 1179 pos = rev
1154 1180
1155 1181 uprev = min(max(0, count - 1), rev + revcount)
1156 1182 downrev = max(0, rev - revcount)
1157 1183 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1158 1184
1159 1185 tree = []
1160 1186 if pos != -1:
1161 1187 allrevs = web.repo.changelog.revs(pos, 0)
1162 1188 revs = []
1163 1189 for i in allrevs:
1164 1190 revs.append(i)
1165 1191 if len(revs) >= revcount:
1166 1192 break
1167 1193
1168 1194 # We have to feed a baseset to dagwalker as it is expecting smartset
1169 1195 # object. This does not have a big impact on hgweb performance itself
1170 1196 # since hgweb graphing code is not itself lazy yet.
1171 1197 dag = graphmod.dagwalker(web.repo, revset.baseset(revs))
1172 1198 # As we said one line above... not lazy.
1173 1199 tree = list(graphmod.colored(dag, web.repo))
1174 1200
1175 1201 def getcolumns(tree):
1176 1202 cols = 0
1177 1203 for (id, type, ctx, vtx, edges) in tree:
1178 1204 if type != graphmod.CHANGESET:
1179 1205 continue
1180 1206 cols = max(cols, max([edge[0] for edge in edges] or [0]),
1181 1207 max([edge[1] for edge in edges] or [0]))
1182 1208 return cols
1183 1209
1184 1210 def graphdata(usetuples, **map):
1185 1211 data = []
1186 1212
1187 1213 row = 0
1188 1214 for (id, type, ctx, vtx, edges) in tree:
1189 1215 if type != graphmod.CHANGESET:
1190 1216 continue
1191 1217 node = str(ctx)
1192 1218 age = templatefilters.age(ctx.date())
1193 1219 desc = templatefilters.firstline(ctx.description())
1194 1220 desc = cgi.escape(templatefilters.nonempty(desc))
1195 1221 user = cgi.escape(templatefilters.person(ctx.user()))
1196 1222 branch = cgi.escape(ctx.branch())
1197 1223 try:
1198 1224 branchnode = web.repo.branchtip(branch)
1199 1225 except error.RepoLookupError:
1200 1226 branchnode = None
1201 1227 branch = branch, branchnode == ctx.node()
1202 1228
1203 1229 if usetuples:
1204 1230 data.append((node, vtx, edges, desc, user, age, branch,
1205 1231 [cgi.escape(x) for x in ctx.tags()],
1206 1232 [cgi.escape(x) for x in ctx.bookmarks()]))
1207 1233 else:
1208 1234 edgedata = [{'col': edge[0], 'nextcol': edge[1],
1209 1235 'color': (edge[2] - 1) % 6 + 1,
1210 1236 'width': edge[3], 'bcolor': edge[4]}
1211 1237 for edge in edges]
1212 1238
1213 1239 data.append(
1214 1240 {'node': node,
1215 1241 'col': vtx[0],
1216 1242 'color': (vtx[1] - 1) % 6 + 1,
1217 1243 'edges': edgedata,
1218 1244 'row': row,
1219 1245 'nextrow': row + 1,
1220 1246 'desc': desc,
1221 1247 'user': user,
1222 1248 'age': age,
1223 1249 'bookmarks': webutil.nodebookmarksdict(
1224 1250 web.repo, ctx.node()),
1225 1251 'branches': webutil.nodebranchdict(web.repo, ctx),
1226 1252 'inbranch': webutil.nodeinbranch(web.repo, ctx),
1227 1253 'tags': webutil.nodetagsdict(web.repo, ctx.node())})
1228 1254
1229 1255 row += 1
1230 1256
1231 1257 return data
1232 1258
1233 1259 cols = getcolumns(tree)
1234 1260 rows = len(tree)
1235 1261 canvasheight = (rows + 1) * bg_height - 27
1236 1262
1237 1263 return tmpl('graph', rev=rev, symrev=symrev, revcount=revcount,
1238 1264 uprev=uprev,
1239 1265 lessvars=lessvars, morevars=morevars, downrev=downrev,
1240 1266 cols=cols, rows=rows,
1241 1267 canvaswidth=(cols + 1) * bg_height,
1242 1268 truecanvasheight=rows * bg_height,
1243 1269 canvasheight=canvasheight, bg_height=bg_height,
1244 1270 jsdata=lambda **x: graphdata(True, **x),
1245 1271 nodes=lambda **x: graphdata(False, **x),
1246 1272 node=ctx.hex(), changenav=changenav)
1247 1273
1248 1274 def _getdoc(e):
1249 1275 doc = e[0].__doc__
1250 1276 if doc:
1251 1277 doc = _(doc).partition('\n')[0]
1252 1278 else:
1253 1279 doc = _('(no help text available)')
1254 1280 return doc
1255 1281
1256 1282 @webcommand('help')
1257 1283 def help(web, req, tmpl):
1258 1284 """
1259 1285 /help[/{topic}]
1260 1286 ---------------
1261 1287
1262 1288 Render help documentation.
1263 1289
1264 1290 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1265 1291 is defined, that help topic will be rendered. If not, an index of
1266 1292 available help topics will be rendered.
1267 1293
1268 1294 The ``help`` template will be rendered when requesting help for a topic.
1269 1295 ``helptopics`` will be rendered for the index of help topics.
1270 1296 """
1271 from mercurial import commands # avoid cycle
1272 from mercurial import help as helpmod # avoid cycle
1297 from .. import commands, help as helpmod # avoid cycle
1273 1298
1274 1299 topicname = req.form.get('node', [None])[0]
1275 1300 if not topicname:
1276 1301 def topics(**map):
1277 1302 for entries, summary, _doc in helpmod.helptable:
1278 1303 yield {'topic': entries[0], 'summary': summary}
1279 1304
1280 1305 early, other = [], []
1281 1306 primary = lambda s: s.partition('|')[0]
1282 1307 for c, e in commands.table.iteritems():
1283 1308 doc = _getdoc(e)
1284 1309 if 'DEPRECATED' in doc or c.startswith('debug'):
1285 1310 continue
1286 1311 cmd = primary(c)
1287 1312 if cmd.startswith('^'):
1288 1313 early.append((cmd[1:], doc))
1289 1314 else:
1290 1315 other.append((cmd, doc))
1291 1316
1292 1317 early.sort()
1293 1318 other.sort()
1294 1319
1295 1320 def earlycommands(**map):
1296 1321 for c, doc in early:
1297 1322 yield {'topic': c, 'summary': doc}
1298 1323
1299 1324 def othercommands(**map):
1300 1325 for c, doc in other:
1301 1326 yield {'topic': c, 'summary': doc}
1302 1327
1303 1328 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
1304 1329 othercommands=othercommands, title='Index')
1305 1330
1306 1331 u = webutil.wsgiui()
1307 1332 u.verbose = True
1308 1333 try:
1309 1334 doc = helpmod.help_(u, topicname)
1310 1335 except error.UnknownCommand:
1311 1336 raise ErrorResponse(HTTP_NOT_FOUND)
1312 1337 return tmpl('help', topic=topicname, doc=doc)
1313 1338
1314 1339 # tell hggettext to extract docstrings from these functions:
1315 1340 i18nfunctions = commands.values()
@@ -1,593 +1,609 b''
1 1 # hgweb/webutil.py - utility library for the web interface.
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 import os, copy
9 from __future__ import absolute_import
10
11 import copy
12 import difflib
13 import os
10 14 import re
11 from mercurial import match, patch, error, util, pathutil, context
12 from mercurial import ui as uimod
13 from mercurial.i18n import _
14 from mercurial.node import hex, nullid, short
15 from mercurial import templatefilters
16 from common import ErrorResponse, paritygen
17 from common import HTTP_NOT_FOUND
18 import difflib
15
16 from ..i18n import _
17 from ..node import hex, nullid, short
18
19 from .common import (
20 ErrorResponse,
21 HTTP_NOT_FOUND,
22 paritygen,
23 )
24
25 from .. import (
26 context,
27 error,
28 match,
29 patch,
30 pathutil,
31 templatefilters,
32 ui as uimod,
33 util,
34 )
19 35
20 36 def up(p):
21 37 if p[0] != "/":
22 38 p = "/" + p
23 39 if p[-1] == "/":
24 40 p = p[:-1]
25 41 up = os.path.dirname(p)
26 42 if up == "/":
27 43 return "/"
28 44 return up + "/"
29 45
30 46 def _navseq(step, firststep=None):
31 47 if firststep:
32 48 yield firststep
33 49 if firststep >= 20 and firststep <= 40:
34 50 firststep = 50
35 51 yield firststep
36 52 assert step > 0
37 53 assert firststep > 0
38 54 while step <= firststep:
39 55 step *= 10
40 56 while True:
41 57 yield 1 * step
42 58 yield 3 * step
43 59 step *= 10
44 60
45 61 class revnav(object):
46 62
47 63 def __init__(self, repo):
48 64 """Navigation generation object
49 65
50 66 :repo: repo object we generate nav for
51 67 """
52 68 # used for hex generation
53 69 self._revlog = repo.changelog
54 70
55 71 def __nonzero__(self):
56 72 """return True if any revision to navigate over"""
57 73 return self._first() is not None
58 74
59 75 def _first(self):
60 76 """return the minimum non-filtered changeset or None"""
61 77 try:
62 78 return iter(self._revlog).next()
63 79 except StopIteration:
64 80 return None
65 81
66 82 def hex(self, rev):
67 83 return hex(self._revlog.node(rev))
68 84
69 85 def gen(self, pos, pagelen, limit):
70 86 """computes label and revision id for navigation link
71 87
72 88 :pos: is the revision relative to which we generate navigation.
73 89 :pagelen: the size of each navigation page
74 90 :limit: how far shall we link
75 91
76 92 The return is:
77 93 - a single element tuple
78 94 - containing a dictionary with a `before` and `after` key
79 95 - values are generator functions taking arbitrary number of kwargs
80 96 - yield items are dictionaries with `label` and `node` keys
81 97 """
82 98 if not self:
83 99 # empty repo
84 100 return ({'before': (), 'after': ()},)
85 101
86 102 targets = []
87 103 for f in _navseq(1, pagelen):
88 104 if f > limit:
89 105 break
90 106 targets.append(pos + f)
91 107 targets.append(pos - f)
92 108 targets.sort()
93 109
94 110 first = self._first()
95 111 navbefore = [("(%i)" % first, self.hex(first))]
96 112 navafter = []
97 113 for rev in targets:
98 114 if rev not in self._revlog:
99 115 continue
100 116 if pos < rev < limit:
101 117 navafter.append(("+%d" % abs(rev - pos), self.hex(rev)))
102 118 if 0 < rev < pos:
103 119 navbefore.append(("-%d" % abs(rev - pos), self.hex(rev)))
104 120
105 121
106 122 navafter.append(("tip", "tip"))
107 123
108 124 data = lambda i: {"label": i[0], "node": i[1]}
109 125 return ({'before': lambda **map: (data(i) for i in navbefore),
110 126 'after': lambda **map: (data(i) for i in navafter)},)
111 127
112 128 class filerevnav(revnav):
113 129
114 130 def __init__(self, repo, path):
115 131 """Navigation generation object
116 132
117 133 :repo: repo object we generate nav for
118 134 :path: path of the file we generate nav for
119 135 """
120 136 # used for iteration
121 137 self._changelog = repo.unfiltered().changelog
122 138 # used for hex generation
123 139 self._revlog = repo.file(path)
124 140
125 141 def hex(self, rev):
126 142 return hex(self._changelog.node(self._revlog.linkrev(rev)))
127 143
128 144 class _siblings(object):
129 145 def __init__(self, siblings=[], hiderev=None):
130 146 self.siblings = [s for s in siblings if s.node() != nullid]
131 147 if len(self.siblings) == 1 and self.siblings[0].rev() == hiderev:
132 148 self.siblings = []
133 149
134 150 def __iter__(self):
135 151 for s in self.siblings:
136 152 d = {
137 153 'node': s.hex(),
138 154 'rev': s.rev(),
139 155 'user': s.user(),
140 156 'date': s.date(),
141 157 'description': s.description(),
142 158 'branch': s.branch(),
143 159 }
144 160 if util.safehasattr(s, 'path'):
145 161 d['file'] = s.path()
146 162 yield d
147 163
148 164 def __len__(self):
149 165 return len(self.siblings)
150 166
151 167 def parents(ctx, hide=None):
152 168 if isinstance(ctx, context.basefilectx):
153 169 introrev = ctx.introrev()
154 170 if ctx.changectx().rev() != introrev:
155 171 return _siblings([ctx.repo()[introrev]], hide)
156 172 return _siblings(ctx.parents(), hide)
157 173
158 174 def children(ctx, hide=None):
159 175 return _siblings(ctx.children(), hide)
160 176
161 177 def renamelink(fctx):
162 178 r = fctx.renamed()
163 179 if r:
164 180 return [{'file': r[0], 'node': hex(r[1])}]
165 181 return []
166 182
167 183 def nodetagsdict(repo, node):
168 184 return [{"name": i} for i in repo.nodetags(node)]
169 185
170 186 def nodebookmarksdict(repo, node):
171 187 return [{"name": i} for i in repo.nodebookmarks(node)]
172 188
173 189 def nodebranchdict(repo, ctx):
174 190 branches = []
175 191 branch = ctx.branch()
176 192 # If this is an empty repo, ctx.node() == nullid,
177 193 # ctx.branch() == 'default'.
178 194 try:
179 195 branchnode = repo.branchtip(branch)
180 196 except error.RepoLookupError:
181 197 branchnode = None
182 198 if branchnode == ctx.node():
183 199 branches.append({"name": branch})
184 200 return branches
185 201
186 202 def nodeinbranch(repo, ctx):
187 203 branches = []
188 204 branch = ctx.branch()
189 205 try:
190 206 branchnode = repo.branchtip(branch)
191 207 except error.RepoLookupError:
192 208 branchnode = None
193 209 if branch != 'default' and branchnode != ctx.node():
194 210 branches.append({"name": branch})
195 211 return branches
196 212
197 213 def nodebranchnodefault(ctx):
198 214 branches = []
199 215 branch = ctx.branch()
200 216 if branch != 'default':
201 217 branches.append({"name": branch})
202 218 return branches
203 219
204 220 def showtag(repo, tmpl, t1, node=nullid, **args):
205 221 for t in repo.nodetags(node):
206 222 yield tmpl(t1, tag=t, **args)
207 223
208 224 def showbookmark(repo, tmpl, t1, node=nullid, **args):
209 225 for t in repo.nodebookmarks(node):
210 226 yield tmpl(t1, bookmark=t, **args)
211 227
212 228 def branchentries(repo, stripecount, limit=0):
213 229 tips = []
214 230 heads = repo.heads()
215 231 parity = paritygen(stripecount)
216 232 sortkey = lambda item: (not item[1], item[0].rev())
217 233
218 234 def entries(**map):
219 235 count = 0
220 236 if not tips:
221 237 for tag, hs, tip, closed in repo.branchmap().iterbranches():
222 238 tips.append((repo[tip], closed))
223 239 for ctx, closed in sorted(tips, key=sortkey, reverse=True):
224 240 if limit > 0 and count >= limit:
225 241 return
226 242 count += 1
227 243 if closed:
228 244 status = 'closed'
229 245 elif ctx.node() not in heads:
230 246 status = 'inactive'
231 247 else:
232 248 status = 'open'
233 249 yield {
234 250 'parity': parity.next(),
235 251 'branch': ctx.branch(),
236 252 'status': status,
237 253 'node': ctx.hex(),
238 254 'date': ctx.date()
239 255 }
240 256
241 257 return entries
242 258
243 259 def cleanpath(repo, path):
244 260 path = path.lstrip('/')
245 261 return pathutil.canonpath(repo.root, '', path)
246 262
247 263 def changeidctx(repo, changeid):
248 264 try:
249 265 ctx = repo[changeid]
250 266 except error.RepoError:
251 267 man = repo.manifest
252 268 ctx = repo[man.linkrev(man.rev(man.lookup(changeid)))]
253 269
254 270 return ctx
255 271
256 272 def changectx(repo, req):
257 273 changeid = "tip"
258 274 if 'node' in req.form:
259 275 changeid = req.form['node'][0]
260 276 ipos = changeid.find(':')
261 277 if ipos != -1:
262 278 changeid = changeid[(ipos + 1):]
263 279 elif 'manifest' in req.form:
264 280 changeid = req.form['manifest'][0]
265 281
266 282 return changeidctx(repo, changeid)
267 283
268 284 def basechangectx(repo, req):
269 285 if 'node' in req.form:
270 286 changeid = req.form['node'][0]
271 287 ipos = changeid.find(':')
272 288 if ipos != -1:
273 289 changeid = changeid[:ipos]
274 290 return changeidctx(repo, changeid)
275 291
276 292 return None
277 293
278 294 def filectx(repo, req):
279 295 if 'file' not in req.form:
280 296 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
281 297 path = cleanpath(repo, req.form['file'][0])
282 298 if 'node' in req.form:
283 299 changeid = req.form['node'][0]
284 300 elif 'filenode' in req.form:
285 301 changeid = req.form['filenode'][0]
286 302 else:
287 303 raise ErrorResponse(HTTP_NOT_FOUND, 'node or filenode not given')
288 304 try:
289 305 fctx = repo[changeid][path]
290 306 except error.RepoError:
291 307 fctx = repo.filectx(path, fileid=changeid)
292 308
293 309 return fctx
294 310
295 311 def changelistentry(web, ctx, tmpl):
296 312 '''Obtain a dictionary to be used for entries in a changelist.
297 313
298 314 This function is called when producing items for the "entries" list passed
299 315 to the "shortlog" and "changelog" templates.
300 316 '''
301 317 repo = web.repo
302 318 rev = ctx.rev()
303 319 n = ctx.node()
304 320 showtags = showtag(repo, tmpl, 'changelogtag', n)
305 321 files = listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
306 322
307 323 return {
308 324 "author": ctx.user(),
309 325 "parent": lambda **x: parents(ctx, rev - 1),
310 326 "child": lambda **x: children(ctx, rev + 1),
311 327 "changelogtag": showtags,
312 328 "desc": ctx.description(),
313 329 "extra": ctx.extra(),
314 330 "date": ctx.date(),
315 331 "files": files,
316 332 "rev": rev,
317 333 "node": hex(n),
318 334 "tags": nodetagsdict(repo, n),
319 335 "bookmarks": nodebookmarksdict(repo, n),
320 336 "inbranch": nodeinbranch(repo, ctx),
321 337 "branches": nodebranchdict(repo, ctx)
322 338 }
323 339
324 340 def symrevorshortnode(req, ctx):
325 341 if 'node' in req.form:
326 342 return templatefilters.revescape(req.form['node'][0])
327 343 else:
328 344 return short(ctx.node())
329 345
330 346 def changesetentry(web, req, tmpl, ctx):
331 347 '''Obtain a dictionary to be used to render the "changeset" template.'''
332 348
333 349 showtags = showtag(web.repo, tmpl, 'changesettag', ctx.node())
334 350 showbookmarks = showbookmark(web.repo, tmpl, 'changesetbookmark',
335 351 ctx.node())
336 352 showbranch = nodebranchnodefault(ctx)
337 353
338 354 files = []
339 355 parity = paritygen(web.stripecount)
340 356 for blockno, f in enumerate(ctx.files()):
341 357 template = f in ctx and 'filenodelink' or 'filenolink'
342 358 files.append(tmpl(template,
343 359 node=ctx.hex(), file=f, blockno=blockno + 1,
344 360 parity=parity.next()))
345 361
346 362 basectx = basechangectx(web.repo, req)
347 363 if basectx is None:
348 364 basectx = ctx.p1()
349 365
350 366 style = web.config('web', 'style', 'paper')
351 367 if 'style' in req.form:
352 368 style = req.form['style'][0]
353 369
354 370 parity = paritygen(web.stripecount)
355 371 diff = diffs(web.repo, tmpl, ctx, basectx, None, parity, style)
356 372
357 373 parity = paritygen(web.stripecount)
358 374 diffstatsgen = diffstatgen(ctx, basectx)
359 375 diffstats = diffstat(tmpl, ctx, diffstatsgen, parity)
360 376
361 377 return dict(
362 378 diff=diff,
363 379 rev=ctx.rev(),
364 380 node=ctx.hex(),
365 381 symrev=symrevorshortnode(req, ctx),
366 382 parent=parents(ctx),
367 383 child=children(ctx),
368 384 basenode=basectx.hex(),
369 385 changesettag=showtags,
370 386 changesetbookmark=showbookmarks,
371 387 changesetbranch=showbranch,
372 388 author=ctx.user(),
373 389 desc=ctx.description(),
374 390 extra=ctx.extra(),
375 391 date=ctx.date(),
376 392 phase=ctx.phasestr(),
377 393 files=files,
378 394 diffsummary=lambda **x: diffsummary(diffstatsgen),
379 395 diffstat=diffstats,
380 396 archives=web.archivelist(ctx.hex()),
381 397 tags=nodetagsdict(web.repo, ctx.node()),
382 398 bookmarks=nodebookmarksdict(web.repo, ctx.node()),
383 399 branch=showbranch,
384 400 inbranch=nodeinbranch(web.repo, ctx),
385 401 branches=nodebranchdict(web.repo, ctx))
386 402
387 403 def listfilediffs(tmpl, files, node, max):
388 404 for f in files[:max]:
389 405 yield tmpl('filedifflink', node=hex(node), file=f)
390 406 if len(files) > max:
391 407 yield tmpl('fileellipses')
392 408
393 409 def diffs(repo, tmpl, ctx, basectx, files, parity, style):
394 410
395 411 def countgen():
396 412 start = 1
397 413 while True:
398 414 yield start
399 415 start += 1
400 416
401 417 blockcount = countgen()
402 418 def prettyprintlines(diff, blockno):
403 419 for lineno, l in enumerate(diff.splitlines(True)):
404 420 difflineno = "%d.%d" % (blockno, lineno + 1)
405 421 if l.startswith('+'):
406 422 ltype = "difflineplus"
407 423 elif l.startswith('-'):
408 424 ltype = "difflineminus"
409 425 elif l.startswith('@'):
410 426 ltype = "difflineat"
411 427 else:
412 428 ltype = "diffline"
413 429 yield tmpl(ltype,
414 430 line=l,
415 431 lineno=lineno + 1,
416 432 lineid="l%s" % difflineno,
417 433 linenumber="% 8s" % difflineno)
418 434
419 435 if files:
420 436 m = match.exact(repo.root, repo.getcwd(), files)
421 437 else:
422 438 m = match.always(repo.root, repo.getcwd())
423 439
424 440 diffopts = patch.diffopts(repo.ui, untrusted=True)
425 441 if basectx is None:
426 442 parents = ctx.parents()
427 443 if parents:
428 444 node1 = parents[0].node()
429 445 else:
430 446 node1 = nullid
431 447 else:
432 448 node1 = basectx.node()
433 449 node2 = ctx.node()
434 450
435 451 block = []
436 452 for chunk in patch.diff(repo, node1, node2, m, opts=diffopts):
437 453 if chunk.startswith('diff') and block:
438 454 blockno = blockcount.next()
439 455 yield tmpl('diffblock', parity=parity.next(), blockno=blockno,
440 456 lines=prettyprintlines(''.join(block), blockno))
441 457 block = []
442 458 if chunk.startswith('diff') and style != 'raw':
443 459 chunk = ''.join(chunk.splitlines(True)[1:])
444 460 block.append(chunk)
445 461 blockno = blockcount.next()
446 462 yield tmpl('diffblock', parity=parity.next(), blockno=blockno,
447 463 lines=prettyprintlines(''.join(block), blockno))
448 464
449 465 def compare(tmpl, context, leftlines, rightlines):
450 466 '''Generator function that provides side-by-side comparison data.'''
451 467
452 468 def compline(type, leftlineno, leftline, rightlineno, rightline):
453 469 lineid = leftlineno and ("l%s" % leftlineno) or ''
454 470 lineid += rightlineno and ("r%s" % rightlineno) or ''
455 471 return tmpl('comparisonline',
456 472 type=type,
457 473 lineid=lineid,
458 474 leftlineno=leftlineno,
459 475 leftlinenumber="% 6s" % (leftlineno or ''),
460 476 leftline=leftline or '',
461 477 rightlineno=rightlineno,
462 478 rightlinenumber="% 6s" % (rightlineno or ''),
463 479 rightline=rightline or '')
464 480
465 481 def getblock(opcodes):
466 482 for type, llo, lhi, rlo, rhi in opcodes:
467 483 len1 = lhi - llo
468 484 len2 = rhi - rlo
469 485 count = min(len1, len2)
470 486 for i in xrange(count):
471 487 yield compline(type=type,
472 488 leftlineno=llo + i + 1,
473 489 leftline=leftlines[llo + i],
474 490 rightlineno=rlo + i + 1,
475 491 rightline=rightlines[rlo + i])
476 492 if len1 > len2:
477 493 for i in xrange(llo + count, lhi):
478 494 yield compline(type=type,
479 495 leftlineno=i + 1,
480 496 leftline=leftlines[i],
481 497 rightlineno=None,
482 498 rightline=None)
483 499 elif len2 > len1:
484 500 for i in xrange(rlo + count, rhi):
485 501 yield compline(type=type,
486 502 leftlineno=None,
487 503 leftline=None,
488 504 rightlineno=i + 1,
489 505 rightline=rightlines[i])
490 506
491 507 s = difflib.SequenceMatcher(None, leftlines, rightlines)
492 508 if context < 0:
493 509 yield tmpl('comparisonblock', lines=getblock(s.get_opcodes()))
494 510 else:
495 511 for oc in s.get_grouped_opcodes(n=context):
496 512 yield tmpl('comparisonblock', lines=getblock(oc))
497 513
498 514 def diffstatgen(ctx, basectx):
499 515 '''Generator function that provides the diffstat data.'''
500 516
501 517 stats = patch.diffstatdata(util.iterlines(ctx.diff(basectx)))
502 518 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
503 519 while True:
504 520 yield stats, maxname, maxtotal, addtotal, removetotal, binary
505 521
506 522 def diffsummary(statgen):
507 523 '''Return a short summary of the diff.'''
508 524
509 525 stats, maxname, maxtotal, addtotal, removetotal, binary = statgen.next()
510 526 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
511 527 len(stats), addtotal, removetotal)
512 528
513 529 def diffstat(tmpl, ctx, statgen, parity):
514 530 '''Return a diffstat template for each file in the diff.'''
515 531
516 532 stats, maxname, maxtotal, addtotal, removetotal, binary = statgen.next()
517 533 files = ctx.files()
518 534
519 535 def pct(i):
520 536 if maxtotal == 0:
521 537 return 0
522 538 return (float(i) / maxtotal) * 100
523 539
524 540 fileno = 0
525 541 for filename, adds, removes, isbinary in stats:
526 542 template = filename in files and 'diffstatlink' or 'diffstatnolink'
527 543 total = adds + removes
528 544 fileno += 1
529 545 yield tmpl(template, node=ctx.hex(), file=filename, fileno=fileno,
530 546 total=total, addpct=pct(adds), removepct=pct(removes),
531 547 parity=parity.next())
532 548
533 549 class sessionvars(object):
534 550 def __init__(self, vars, start='?'):
535 551 self.start = start
536 552 self.vars = vars
537 553 def __getitem__(self, key):
538 554 return self.vars[key]
539 555 def __setitem__(self, key, value):
540 556 self.vars[key] = value
541 557 def __copy__(self):
542 558 return sessionvars(copy.copy(self.vars), self.start)
543 559 def __iter__(self):
544 560 separator = self.start
545 561 for key, value in sorted(self.vars.iteritems()):
546 562 yield {'name': key, 'value': str(value), 'separator': separator}
547 563 separator = '&'
548 564
549 565 class wsgiui(uimod.ui):
550 566 # default termwidth breaks under mod_wsgi
551 567 def termwidth(self):
552 568 return 80
553 569
554 570 def getwebsubs(repo):
555 571 websubtable = []
556 572 websubdefs = repo.ui.configitems('websub')
557 573 # we must maintain interhg backwards compatibility
558 574 websubdefs += repo.ui.configitems('interhg')
559 575 for key, pattern in websubdefs:
560 576 # grab the delimiter from the character after the "s"
561 577 unesc = pattern[1]
562 578 delim = re.escape(unesc)
563 579
564 580 # identify portions of the pattern, taking care to avoid escaped
565 581 # delimiters. the replace format and flags are optional, but
566 582 # delimiters are required.
567 583 match = re.match(
568 584 r'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
569 585 % (delim, delim, delim), pattern)
570 586 if not match:
571 587 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
572 588 % (key, pattern))
573 589 continue
574 590
575 591 # we need to unescape the delimiter for regexp and format
576 592 delim_re = re.compile(r'(?<!\\)\\%s' % delim)
577 593 regexp = delim_re.sub(unesc, match.group(1))
578 594 format = delim_re.sub(unesc, match.group(2))
579 595
580 596 # the pattern allows for 6 regexp flags, so set them if necessary
581 597 flagin = match.group(3)
582 598 flags = 0
583 599 if flagin:
584 600 for flag in flagin.upper():
585 601 flags |= re.__dict__[flag]
586 602
587 603 try:
588 604 regexp = re.compile(regexp, flags)
589 605 websubtable.append((regexp, format))
590 606 except re.error:
591 607 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
592 608 % (key, regexp))
593 609 return websubtable
@@ -1,83 +1,92 b''
1 1 # hgweb/wsgicgi.py - CGI->WSGI translator
2 2 #
3 3 # Copyright 2006 Eric Hopper <hopper@omnifarious.org>
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 # This was originally copied from the public domain code at
9 9 # http://www.python.org/dev/peps/pep-0333/#the-server-gateway-side
10 10
11 import os, sys
12 from mercurial import util
13 from mercurial.hgweb import common
11 from __future__ import absolute_import
12
13 import os
14 import sys
15
16 from .. import (
17 util,
18 )
19
20 from . import (
21 common,
22 )
14 23
15 24 def launch(application):
16 25 util.setbinary(sys.stdin)
17 26 util.setbinary(sys.stdout)
18 27
19 28 environ = dict(os.environ.iteritems())
20 29 environ.setdefault('PATH_INFO', '')
21 30 if environ.get('SERVER_SOFTWARE', '').startswith('Microsoft-IIS'):
22 31 # IIS includes script_name in PATH_INFO
23 32 scriptname = environ['SCRIPT_NAME']
24 33 if environ['PATH_INFO'].startswith(scriptname):
25 34 environ['PATH_INFO'] = environ['PATH_INFO'][len(scriptname):]
26 35
27 36 stdin = sys.stdin
28 37 if environ.get('HTTP_EXPECT', '').lower() == '100-continue':
29 38 stdin = common.continuereader(stdin, sys.stdout.write)
30 39
31 40 environ['wsgi.input'] = stdin
32 41 environ['wsgi.errors'] = sys.stderr
33 42 environ['wsgi.version'] = (1, 0)
34 43 environ['wsgi.multithread'] = False
35 44 environ['wsgi.multiprocess'] = True
36 45 environ['wsgi.run_once'] = True
37 46
38 47 if environ.get('HTTPS', 'off').lower() in ('on', '1', 'yes'):
39 48 environ['wsgi.url_scheme'] = 'https'
40 49 else:
41 50 environ['wsgi.url_scheme'] = 'http'
42 51
43 52 headers_set = []
44 53 headers_sent = []
45 54 out = sys.stdout
46 55
47 56 def write(data):
48 57 if not headers_set:
49 58 raise AssertionError("write() before start_response()")
50 59
51 60 elif not headers_sent:
52 61 # Before the first output, send the stored headers
53 62 status, response_headers = headers_sent[:] = headers_set
54 63 out.write('Status: %s\r\n' % status)
55 64 for header in response_headers:
56 65 out.write('%s: %s\r\n' % header)
57 66 out.write('\r\n')
58 67
59 68 out.write(data)
60 69 out.flush()
61 70
62 71 def start_response(status, response_headers, exc_info=None):
63 72 if exc_info:
64 73 try:
65 74 if headers_sent:
66 75 # Re-raise original exception if headers sent
67 76 raise exc_info[0](exc_info[1], exc_info[2])
68 77 finally:
69 78 exc_info = None # avoid dangling circular ref
70 79 elif headers_set:
71 80 raise AssertionError("Headers already set!")
72 81
73 82 headers_set[:] = [status, response_headers]
74 83 return write
75 84
76 85 content = application(environ, start_response)
77 86 try:
78 87 for chunk in content:
79 88 write(chunk)
80 89 if not headers_sent:
81 90 write('') # send headers now if body was empty
82 91 finally:
83 92 getattr(content, 'close', lambda : None)()
General Comments 0
You need to be logged in to leave comments. Login now