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