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