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