##// END OF EJS Templates
hgweb: create function to perform actions on new repo...
Gregory Szorc -
r26218:7d45ec47 default
parent child Browse files
Show More
@@ -1,432 +1,434
1 1 # hgweb/hgweb_mod.py - Web interface for a repository.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 import os
10 10 from mercurial import ui, hg, hook, error, encoding, templater, util, repoview
11 11 from mercurial.templatefilters import websub
12 12 from common import get_stat, ErrorResponse, permhooks, caching
13 13 from common import HTTP_OK, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST
14 14 from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR
15 15 from request import wsgirequest
16 16 import webcommands, protocol, webutil
17 17
18 18 perms = {
19 19 'changegroup': 'pull',
20 20 'changegroupsubset': 'pull',
21 21 'getbundle': 'pull',
22 22 'stream_out': 'pull',
23 23 'listkeys': 'pull',
24 24 'unbundle': 'push',
25 25 'pushkey': 'push',
26 26 }
27 27
28 28 ## Files of interest
29 29 # Used to check if the repository has changed looking at mtime and size of
30 30 # theses files. This should probably be relocated a bit higher in core.
31 31 foi = [('spath', '00changelog.i'),
32 32 ('spath', 'phaseroots'), # ! phase can change content at the same size
33 33 ('spath', 'obsstore'),
34 34 ('path', 'bookmarks'), # ! bookmark can change content at the same size
35 35 ]
36 36
37 37 def makebreadcrumb(url, prefix=''):
38 38 '''Return a 'URL breadcrumb' list
39 39
40 40 A 'URL breadcrumb' is a list of URL-name pairs,
41 41 corresponding to each of the path items on a URL.
42 42 This can be used to create path navigation entries.
43 43 '''
44 44 if url.endswith('/'):
45 45 url = url[:-1]
46 46 if prefix:
47 47 url = '/' + prefix + url
48 48 relpath = url
49 49 if relpath.startswith('/'):
50 50 relpath = relpath[1:]
51 51
52 52 breadcrumb = []
53 53 urlel = url
54 54 pathitems = [''] + relpath.split('/')
55 55 for pathel in reversed(pathitems):
56 56 if not pathel or not urlel:
57 57 break
58 58 breadcrumb.append({'url': urlel, 'name': pathel})
59 59 urlel = os.path.dirname(urlel)
60 60 return reversed(breadcrumb)
61 61
62 62 class requestcontext(object):
63 63 """Holds state/context for an individual request.
64 64
65 65 Servers can be multi-threaded. Holding state on the WSGI application
66 66 is prone to race conditions. Instances of this class exist to hold
67 67 mutable and race-free state for requests.
68 68 """
69 69 def __init__(self, app):
70 70 self.repo = app.repo
71 71 self.reponame = app.reponame
72 72
73 73 self.archives = ('zip', 'gz', 'bz2')
74 74
75 75 self.maxchanges = self.configint('web', 'maxchanges', 10)
76 76 self.stripecount = self.configint('web', 'stripes', 1)
77 77 self.maxshortchanges = self.configint('web', 'maxshortchanges', 60)
78 78 self.maxfiles = self.configint('web', 'maxfiles', 10)
79 79 self.allowpull = self.configbool('web', 'allowpull', True)
80 80
81 81 # we use untrusted=False to prevent a repo owner from using
82 82 # web.templates in .hg/hgrc to get access to any file readable
83 83 # by the user running the CGI script
84 84 self.templatepath = self.config('web', 'templates', untrusted=False)
85 85
86 86 # This object is more expensive to build than simple config values.
87 87 # It is shared across requests. The app will replace the object
88 88 # if it is updated. Since this is a reference and nothing should
89 89 # modify the underlying object, it should be constant for the lifetime
90 90 # of the request.
91 91 self.websubtable = app.websubtable
92 92
93 93 # Trust the settings from the .hg/hgrc files by default.
94 94 def config(self, section, name, default=None, untrusted=True):
95 95 return self.repo.ui.config(section, name, default,
96 96 untrusted=untrusted)
97 97
98 98 def configbool(self, section, name, default=False, untrusted=True):
99 99 return self.repo.ui.configbool(section, name, default,
100 100 untrusted=untrusted)
101 101
102 102 def configint(self, section, name, default=None, untrusted=True):
103 103 return self.repo.ui.configint(section, name, default,
104 104 untrusted=untrusted)
105 105
106 106 def configlist(self, section, name, default=None, untrusted=True):
107 107 return self.repo.ui.configlist(section, name, default,
108 108 untrusted=untrusted)
109 109
110 110 archivespecs = {
111 111 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
112 112 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
113 113 'zip': ('application/zip', 'zip', '.zip', None),
114 114 }
115 115
116 116 def archivelist(self, nodeid):
117 117 allowed = self.configlist('web', 'allow_archive')
118 118 for typ, spec in self.archivespecs.iteritems():
119 119 if typ in allowed or self.configbool('web', 'allow%s' % typ):
120 120 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
121 121
122 122 def templater(self, req):
123 123 # determine scheme, port and server name
124 124 # this is needed to create absolute urls
125 125
126 126 proto = req.env.get('wsgi.url_scheme')
127 127 if proto == 'https':
128 128 proto = 'https'
129 129 default_port = '443'
130 130 else:
131 131 proto = 'http'
132 132 default_port = '80'
133 133
134 134 port = req.env['SERVER_PORT']
135 135 port = port != default_port and (':' + port) or ''
136 136 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
137 137 logourl = self.config('web', 'logourl', 'http://mercurial.selenic.com/')
138 138 logoimg = self.config('web', 'logoimg', 'hglogo.png')
139 139 staticurl = self.config('web', 'staticurl') or req.url + 'static/'
140 140 if not staticurl.endswith('/'):
141 141 staticurl += '/'
142 142
143 143 # some functions for the templater
144 144
145 145 def motd(**map):
146 146 yield self.config('web', 'motd', '')
147 147
148 148 # figure out which style to use
149 149
150 150 vars = {}
151 151 styles = (
152 152 req.form.get('style', [None])[0],
153 153 self.config('web', 'style'),
154 154 'paper',
155 155 )
156 156 style, mapfile = templater.stylemap(styles, self.templatepath)
157 157 if style == styles[0]:
158 158 vars['style'] = style
159 159
160 160 start = req.url[-1] == '?' and '&' or '?'
161 161 sessionvars = webutil.sessionvars(vars, start)
162 162
163 163 if not self.reponame:
164 164 self.reponame = (self.config('web', 'name')
165 165 or req.env.get('REPO_NAME')
166 166 or req.url.strip('/') or self.repo.root)
167 167
168 168 def websubfilter(text):
169 169 return websub(text, self.websubtable)
170 170
171 171 # create the templater
172 172
173 173 tmpl = templater.templater(mapfile,
174 174 filters={'websub': websubfilter},
175 175 defaults={'url': req.url,
176 176 'logourl': logourl,
177 177 'logoimg': logoimg,
178 178 'staticurl': staticurl,
179 179 'urlbase': urlbase,
180 180 'repo': self.reponame,
181 181 'encoding': encoding.encoding,
182 182 'motd': motd,
183 183 'sessionvars': sessionvars,
184 184 'pathdef': makebreadcrumb(req.url),
185 185 'style': style,
186 186 })
187 187 return tmpl
188 188
189 189
190 190 class hgweb(object):
191 191 """HTTP server for individual repositories.
192 192
193 193 Instances of this class serve HTTP responses for a particular
194 194 repository.
195 195
196 196 Instances are typically used as WSGI applications.
197 197
198 198 Some servers are multi-threaded. On these servers, there may
199 199 be multiple active threads inside __call__.
200 200 """
201 201 def __init__(self, repo, name=None, baseui=None):
202 202 if isinstance(repo, str):
203 203 if baseui:
204 204 u = baseui.copy()
205 205 else:
206 206 u = ui.ui()
207 207 r = hg.repository(u, repo)
208 208 else:
209 209 # we trust caller to give us a private copy
210 210 r = repo
211 211
212 r = getwebview(r)
213 212 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
214 213 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
215 214 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
216 215 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
217 216 # displaying bundling progress bar while serving feel wrong and may
218 217 # break some wsgi implementation.
219 218 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
220 219 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
221 self.repo = r
220 self.repo = self._webifyrepo(r)
222 221 hook.redirect(True)
223 222 self.repostate = None
224 223 self.mtime = -1
225 224 self.reponame = name
226 225
226 def _webifyrepo(self, repo):
227 repo = getwebview(repo)
228 self.websubtable = webutil.getwebsubs(repo)
229 return repo
230
227 231 def refresh(self):
228 232 repostate = []
229 233 mtime = 0
230 234 # file of interrests mtime and size
231 235 for meth, fname in foi:
232 236 prefix = getattr(self.repo, meth)
233 237 st = get_stat(prefix, fname)
234 238 repostate.append((st.st_mtime, st.st_size))
235 239 mtime = max(mtime, st.st_mtime)
236 240 repostate = tuple(repostate)
237 241 # we need to compare file size in addition to mtime to catch
238 242 # changes made less than a second ago
239 243 if repostate != self.repostate:
240 244 r = hg.repository(self.repo.baseui, self.repo.url())
241 self.repo = getwebview(r)
245 self.repo = self._webifyrepo(r)
242 246 # update these last to avoid threads seeing empty settings
243 247 self.repostate = repostate
244 248 # mtime is needed for ETag
245 249 self.mtime = mtime
246 250
247 self.websubtable = webutil.getwebsubs(r)
248
249 251 def run(self):
250 252 """Start a server from CGI environment.
251 253
252 254 Modern servers should be using WSGI and should avoid this
253 255 method, if possible.
254 256 """
255 257 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
256 258 raise RuntimeError("This function is only intended to be "
257 259 "called while running as a CGI script.")
258 260 import mercurial.hgweb.wsgicgi as wsgicgi
259 261 wsgicgi.launch(self)
260 262
261 263 def __call__(self, env, respond):
262 264 """Run the WSGI application.
263 265
264 266 This may be called by multiple threads.
265 267 """
266 268 req = wsgirequest(env, respond)
267 269 return self.run_wsgi(req)
268 270
269 271 def run_wsgi(self, req):
270 272 """Internal method to run the WSGI application.
271 273
272 274 This is typically only called by Mercurial. External consumers
273 275 should be using instances of this class as the WSGI application.
274 276 """
275 277 self.refresh()
276 278 rctx = requestcontext(self)
277 279
278 280 # This state is global across all threads.
279 281 encoding.encoding = rctx.config('web', 'encoding', encoding.encoding)
280 282 rctx.repo.ui.environ = req.env
281 283
282 284 # work with CGI variables to create coherent structure
283 285 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
284 286
285 287 req.url = req.env['SCRIPT_NAME']
286 288 if not req.url.endswith('/'):
287 289 req.url += '/'
288 290 if 'REPO_NAME' in req.env:
289 291 req.url += req.env['REPO_NAME'] + '/'
290 292
291 293 if 'PATH_INFO' in req.env:
292 294 parts = req.env['PATH_INFO'].strip('/').split('/')
293 295 repo_parts = req.env.get('REPO_NAME', '').split('/')
294 296 if parts[:len(repo_parts)] == repo_parts:
295 297 parts = parts[len(repo_parts):]
296 298 query = '/'.join(parts)
297 299 else:
298 300 query = req.env['QUERY_STRING'].split('&', 1)[0]
299 301 query = query.split(';', 1)[0]
300 302
301 303 # process this if it's a protocol request
302 304 # protocol bits don't need to create any URLs
303 305 # and the clients always use the old URL structure
304 306
305 307 cmd = req.form.get('cmd', [''])[0]
306 308 if protocol.iscmd(cmd):
307 309 try:
308 310 if query:
309 311 raise ErrorResponse(HTTP_NOT_FOUND)
310 312 if cmd in perms:
311 313 self.check_perm(rctx, req, perms[cmd])
312 314 return protocol.call(rctx.repo, req, cmd)
313 315 except ErrorResponse as inst:
314 316 # A client that sends unbundle without 100-continue will
315 317 # break if we respond early.
316 318 if (cmd == 'unbundle' and
317 319 (req.env.get('HTTP_EXPECT',
318 320 '').lower() != '100-continue') or
319 321 req.env.get('X-HgHttp2', '')):
320 322 req.drain()
321 323 else:
322 324 req.headers.append(('Connection', 'Close'))
323 325 req.respond(inst, protocol.HGTYPE,
324 326 body='0\n%s\n' % inst)
325 327 return ''
326 328
327 329 # translate user-visible url structure to internal structure
328 330
329 331 args = query.split('/', 2)
330 332 if 'cmd' not in req.form and args and args[0]:
331 333
332 334 cmd = args.pop(0)
333 335 style = cmd.rfind('-')
334 336 if style != -1:
335 337 req.form['style'] = [cmd[:style]]
336 338 cmd = cmd[style + 1:]
337 339
338 340 # avoid accepting e.g. style parameter as command
339 341 if util.safehasattr(webcommands, cmd):
340 342 req.form['cmd'] = [cmd]
341 343
342 344 if cmd == 'static':
343 345 req.form['file'] = ['/'.join(args)]
344 346 else:
345 347 if args and args[0]:
346 348 node = args.pop(0).replace('%2F', '/')
347 349 req.form['node'] = [node]
348 350 if args:
349 351 req.form['file'] = args
350 352
351 353 ua = req.env.get('HTTP_USER_AGENT', '')
352 354 if cmd == 'rev' and 'mercurial' in ua:
353 355 req.form['style'] = ['raw']
354 356
355 357 if cmd == 'archive':
356 358 fn = req.form['node'][0]
357 359 for type_, spec in rctx.archivespecs.iteritems():
358 360 ext = spec[2]
359 361 if fn.endswith(ext):
360 362 req.form['node'] = [fn[:-len(ext)]]
361 363 req.form['type'] = [type_]
362 364
363 365 # process the web interface request
364 366
365 367 try:
366 368 tmpl = rctx.templater(req)
367 369 ctype = tmpl('mimetype', encoding=encoding.encoding)
368 370 ctype = templater.stringify(ctype)
369 371
370 372 # check read permissions non-static content
371 373 if cmd != 'static':
372 374 self.check_perm(rctx, req, None)
373 375
374 376 if cmd == '':
375 377 req.form['cmd'] = [tmpl.cache['default']]
376 378 cmd = req.form['cmd'][0]
377 379
378 380 if rctx.configbool('web', 'cache', True):
379 381 caching(self, req) # sets ETag header or raises NOT_MODIFIED
380 382 if cmd not in webcommands.__all__:
381 383 msg = 'no such method: %s' % cmd
382 384 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
383 385 elif cmd == 'file' and 'raw' in req.form.get('style', []):
384 386 rctx.ctype = ctype
385 387 content = webcommands.rawfile(rctx, req, tmpl)
386 388 else:
387 389 content = getattr(webcommands, cmd)(rctx, req, tmpl)
388 390 req.respond(HTTP_OK, ctype)
389 391
390 392 return content
391 393
392 394 except (error.LookupError, error.RepoLookupError) as err:
393 395 req.respond(HTTP_NOT_FOUND, ctype)
394 396 msg = str(err)
395 397 if (util.safehasattr(err, 'name') and
396 398 not isinstance(err, error.ManifestLookupError)):
397 399 msg = 'revision not found: %s' % err.name
398 400 return tmpl('error', error=msg)
399 401 except (error.RepoError, error.RevlogError) as inst:
400 402 req.respond(HTTP_SERVER_ERROR, ctype)
401 403 return tmpl('error', error=str(inst))
402 404 except ErrorResponse as inst:
403 405 req.respond(inst, ctype)
404 406 if inst.code == HTTP_NOT_MODIFIED:
405 407 # Not allowed to return a body on a 304
406 408 return ['']
407 409 return tmpl('error', error=str(inst))
408 410
409 411 def check_perm(self, rctx, req, op):
410 412 for permhook in permhooks:
411 413 permhook(rctx, req, op)
412 414
413 415 def getwebview(repo):
414 416 """The 'web.view' config controls changeset filter to hgweb. Possible
415 417 values are ``served``, ``visible`` and ``all``. Default is ``served``.
416 418 The ``served`` filter only shows changesets that can be pulled from the
417 419 hgweb instance. The``visible`` filter includes secret changesets but
418 420 still excludes "hidden" one.
419 421
420 422 See the repoview module for details.
421 423
422 424 The option has been around undocumented since Mercurial 2.5, but no
423 425 user ever asked about it. So we better keep it undocumented for now."""
424 426 viewconfig = repo.ui.config('web', 'view', 'served',
425 427 untrusted=True)
426 428 if viewconfig == 'all':
427 429 return repo.unfiltered()
428 430 elif viewconfig in repoview.filtertable:
429 431 return repo.filtered(viewconfig)
430 432 else:
431 433 return repo.filtered('served')
432 434
General Comments 0
You need to be logged in to leave comments. Login now