##// END OF EJS Templates
hgweb: do not try to replace signal handlers while locking...
Yuya Nishihara -
r38158:5b831053 stable
parent child Browse files
Show More
@@ -1,464 +1,470 b''
1 # hgweb/hgweb_mod.py - Web interface for a repository.
1 # hgweb/hgweb_mod.py - Web interface for a repository.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import contextlib
11 import contextlib
12 import os
12 import os
13
13
14 from .common import (
14 from .common import (
15 ErrorResponse,
15 ErrorResponse,
16 HTTP_BAD_REQUEST,
16 HTTP_BAD_REQUEST,
17 cspvalues,
17 cspvalues,
18 permhooks,
18 permhooks,
19 statusmessage,
19 statusmessage,
20 )
20 )
21
21
22 from .. import (
22 from .. import (
23 encoding,
23 encoding,
24 error,
24 error,
25 formatter,
25 formatter,
26 hg,
26 hg,
27 hook,
27 hook,
28 profiling,
28 profiling,
29 pycompat,
29 pycompat,
30 registrar,
30 registrar,
31 repoview,
31 repoview,
32 templatefilters,
32 templatefilters,
33 templater,
33 templater,
34 templateutil,
34 templateutil,
35 ui as uimod,
35 ui as uimod,
36 util,
36 util,
37 wireprotoserver,
37 wireprotoserver,
38 )
38 )
39
39
40 from . import (
40 from . import (
41 request as requestmod,
41 request as requestmod,
42 webcommands,
42 webcommands,
43 webutil,
43 webutil,
44 wsgicgi,
44 wsgicgi,
45 )
45 )
46
46
47 def getstyle(req, configfn, templatepath):
47 def getstyle(req, configfn, templatepath):
48 styles = (
48 styles = (
49 req.qsparams.get('style', None),
49 req.qsparams.get('style', None),
50 configfn('web', 'style'),
50 configfn('web', 'style'),
51 'paper',
51 'paper',
52 )
52 )
53 return styles, templater.stylemap(styles, templatepath)
53 return styles, templater.stylemap(styles, templatepath)
54
54
55 def makebreadcrumb(url, prefix=''):
55 def makebreadcrumb(url, prefix=''):
56 '''Return a 'URL breadcrumb' list
56 '''Return a 'URL breadcrumb' list
57
57
58 A 'URL breadcrumb' is a list of URL-name pairs,
58 A 'URL breadcrumb' is a list of URL-name pairs,
59 corresponding to each of the path items on a URL.
59 corresponding to each of the path items on a URL.
60 This can be used to create path navigation entries.
60 This can be used to create path navigation entries.
61 '''
61 '''
62 if url.endswith('/'):
62 if url.endswith('/'):
63 url = url[:-1]
63 url = url[:-1]
64 if prefix:
64 if prefix:
65 url = '/' + prefix + url
65 url = '/' + prefix + url
66 relpath = url
66 relpath = url
67 if relpath.startswith('/'):
67 if relpath.startswith('/'):
68 relpath = relpath[1:]
68 relpath = relpath[1:]
69
69
70 breadcrumb = []
70 breadcrumb = []
71 urlel = url
71 urlel = url
72 pathitems = [''] + relpath.split('/')
72 pathitems = [''] + relpath.split('/')
73 for pathel in reversed(pathitems):
73 for pathel in reversed(pathitems):
74 if not pathel or not urlel:
74 if not pathel or not urlel:
75 break
75 break
76 breadcrumb.append({'url': urlel, 'name': pathel})
76 breadcrumb.append({'url': urlel, 'name': pathel})
77 urlel = os.path.dirname(urlel)
77 urlel = os.path.dirname(urlel)
78 return templateutil.mappinglist(reversed(breadcrumb))
78 return templateutil.mappinglist(reversed(breadcrumb))
79
79
80 class requestcontext(object):
80 class requestcontext(object):
81 """Holds state/context for an individual request.
81 """Holds state/context for an individual request.
82
82
83 Servers can be multi-threaded. Holding state on the WSGI application
83 Servers can be multi-threaded. Holding state on the WSGI application
84 is prone to race conditions. Instances of this class exist to hold
84 is prone to race conditions. Instances of this class exist to hold
85 mutable and race-free state for requests.
85 mutable and race-free state for requests.
86 """
86 """
87 def __init__(self, app, repo, req, res):
87 def __init__(self, app, repo, req, res):
88 self.repo = repo
88 self.repo = repo
89 self.reponame = app.reponame
89 self.reponame = app.reponame
90 self.req = req
90 self.req = req
91 self.res = res
91 self.res = res
92
92
93 self.maxchanges = self.configint('web', 'maxchanges')
93 self.maxchanges = self.configint('web', 'maxchanges')
94 self.stripecount = self.configint('web', 'stripes')
94 self.stripecount = self.configint('web', 'stripes')
95 self.maxshortchanges = self.configint('web', 'maxshortchanges')
95 self.maxshortchanges = self.configint('web', 'maxshortchanges')
96 self.maxfiles = self.configint('web', 'maxfiles')
96 self.maxfiles = self.configint('web', 'maxfiles')
97 self.allowpull = self.configbool('web', 'allow-pull')
97 self.allowpull = self.configbool('web', 'allow-pull')
98
98
99 # we use untrusted=False to prevent a repo owner from using
99 # we use untrusted=False to prevent a repo owner from using
100 # web.templates in .hg/hgrc to get access to any file readable
100 # web.templates in .hg/hgrc to get access to any file readable
101 # by the user running the CGI script
101 # by the user running the CGI script
102 self.templatepath = self.config('web', 'templates', untrusted=False)
102 self.templatepath = self.config('web', 'templates', untrusted=False)
103
103
104 # This object is more expensive to build than simple config values.
104 # This object is more expensive to build than simple config values.
105 # It is shared across requests. The app will replace the object
105 # It is shared across requests. The app will replace the object
106 # if it is updated. Since this is a reference and nothing should
106 # if it is updated. Since this is a reference and nothing should
107 # modify the underlying object, it should be constant for the lifetime
107 # modify the underlying object, it should be constant for the lifetime
108 # of the request.
108 # of the request.
109 self.websubtable = app.websubtable
109 self.websubtable = app.websubtable
110
110
111 self.csp, self.nonce = cspvalues(self.repo.ui)
111 self.csp, self.nonce = cspvalues(self.repo.ui)
112
112
113 # Trust the settings from the .hg/hgrc files by default.
113 # Trust the settings from the .hg/hgrc files by default.
114 def config(self, section, name, default=uimod._unset, untrusted=True):
114 def config(self, section, name, default=uimod._unset, untrusted=True):
115 return self.repo.ui.config(section, name, default,
115 return self.repo.ui.config(section, name, default,
116 untrusted=untrusted)
116 untrusted=untrusted)
117
117
118 def configbool(self, section, name, default=uimod._unset, untrusted=True):
118 def configbool(self, section, name, default=uimod._unset, untrusted=True):
119 return self.repo.ui.configbool(section, name, default,
119 return self.repo.ui.configbool(section, name, default,
120 untrusted=untrusted)
120 untrusted=untrusted)
121
121
122 def configint(self, section, name, default=uimod._unset, untrusted=True):
122 def configint(self, section, name, default=uimod._unset, untrusted=True):
123 return self.repo.ui.configint(section, name, default,
123 return self.repo.ui.configint(section, name, default,
124 untrusted=untrusted)
124 untrusted=untrusted)
125
125
126 def configlist(self, section, name, default=uimod._unset, untrusted=True):
126 def configlist(self, section, name, default=uimod._unset, untrusted=True):
127 return self.repo.ui.configlist(section, name, default,
127 return self.repo.ui.configlist(section, name, default,
128 untrusted=untrusted)
128 untrusted=untrusted)
129
129
130 def archivelist(self, nodeid):
130 def archivelist(self, nodeid):
131 return webutil.archivelist(self.repo.ui, nodeid)
131 return webutil.archivelist(self.repo.ui, nodeid)
132
132
133 def templater(self, req):
133 def templater(self, req):
134 # determine scheme, port and server name
134 # determine scheme, port and server name
135 # this is needed to create absolute urls
135 # this is needed to create absolute urls
136 logourl = self.config('web', 'logourl')
136 logourl = self.config('web', 'logourl')
137 logoimg = self.config('web', 'logoimg')
137 logoimg = self.config('web', 'logoimg')
138 staticurl = (self.config('web', 'staticurl')
138 staticurl = (self.config('web', 'staticurl')
139 or req.apppath + '/static/')
139 or req.apppath + '/static/')
140 if not staticurl.endswith('/'):
140 if not staticurl.endswith('/'):
141 staticurl += '/'
141 staticurl += '/'
142
142
143 # some functions for the templater
143 # some functions for the templater
144
144
145 def motd(**map):
145 def motd(**map):
146 yield self.config('web', 'motd')
146 yield self.config('web', 'motd')
147
147
148 # figure out which style to use
148 # figure out which style to use
149
149
150 vars = {}
150 vars = {}
151 styles, (style, mapfile) = getstyle(req, self.config,
151 styles, (style, mapfile) = getstyle(req, self.config,
152 self.templatepath)
152 self.templatepath)
153 if style == styles[0]:
153 if style == styles[0]:
154 vars['style'] = style
154 vars['style'] = style
155
155
156 sessionvars = webutil.sessionvars(vars, '?')
156 sessionvars = webutil.sessionvars(vars, '?')
157
157
158 if not self.reponame:
158 if not self.reponame:
159 self.reponame = (self.config('web', 'name', '')
159 self.reponame = (self.config('web', 'name', '')
160 or req.reponame
160 or req.reponame
161 or req.apppath
161 or req.apppath
162 or self.repo.root)
162 or self.repo.root)
163
163
164 filters = {}
164 filters = {}
165 templatefilter = registrar.templatefilter(filters)
165 templatefilter = registrar.templatefilter(filters)
166 @templatefilter('websub', intype=bytes)
166 @templatefilter('websub', intype=bytes)
167 def websubfilter(text):
167 def websubfilter(text):
168 return templatefilters.websub(text, self.websubtable)
168 return templatefilters.websub(text, self.websubtable)
169
169
170 # create the templater
170 # create the templater
171 # TODO: export all keywords: defaults = templatekw.keywords.copy()
171 # TODO: export all keywords: defaults = templatekw.keywords.copy()
172 defaults = {
172 defaults = {
173 'url': req.apppath + '/',
173 'url': req.apppath + '/',
174 'logourl': logourl,
174 'logourl': logourl,
175 'logoimg': logoimg,
175 'logoimg': logoimg,
176 'staticurl': staticurl,
176 'staticurl': staticurl,
177 'urlbase': req.advertisedbaseurl,
177 'urlbase': req.advertisedbaseurl,
178 'repo': self.reponame,
178 'repo': self.reponame,
179 'encoding': encoding.encoding,
179 'encoding': encoding.encoding,
180 'motd': motd,
180 'motd': motd,
181 'sessionvars': sessionvars,
181 'sessionvars': sessionvars,
182 'pathdef': makebreadcrumb(req.apppath),
182 'pathdef': makebreadcrumb(req.apppath),
183 'style': style,
183 'style': style,
184 'nonce': self.nonce,
184 'nonce': self.nonce,
185 }
185 }
186 tres = formatter.templateresources(self.repo.ui, self.repo)
186 tres = formatter.templateresources(self.repo.ui, self.repo)
187 tmpl = templater.templater.frommapfile(mapfile,
187 tmpl = templater.templater.frommapfile(mapfile,
188 filters=filters,
188 filters=filters,
189 defaults=defaults,
189 defaults=defaults,
190 resources=tres)
190 resources=tres)
191 return tmpl
191 return tmpl
192
192
193 def sendtemplate(self, name, **kwargs):
193 def sendtemplate(self, name, **kwargs):
194 """Helper function to send a response generated from a template."""
194 """Helper function to send a response generated from a template."""
195 kwargs = pycompat.byteskwargs(kwargs)
195 kwargs = pycompat.byteskwargs(kwargs)
196 self.res.setbodygen(self.tmpl.generate(name, kwargs))
196 self.res.setbodygen(self.tmpl.generate(name, kwargs))
197 return self.res.sendresponse()
197 return self.res.sendresponse()
198
198
199 class hgweb(object):
199 class hgweb(object):
200 """HTTP server for individual repositories.
200 """HTTP server for individual repositories.
201
201
202 Instances of this class serve HTTP responses for a particular
202 Instances of this class serve HTTP responses for a particular
203 repository.
203 repository.
204
204
205 Instances are typically used as WSGI applications.
205 Instances are typically used as WSGI applications.
206
206
207 Some servers are multi-threaded. On these servers, there may
207 Some servers are multi-threaded. On these servers, there may
208 be multiple active threads inside __call__.
208 be multiple active threads inside __call__.
209 """
209 """
210 def __init__(self, repo, name=None, baseui=None):
210 def __init__(self, repo, name=None, baseui=None):
211 if isinstance(repo, bytes):
211 if isinstance(repo, bytes):
212 if baseui:
212 if baseui:
213 u = baseui.copy()
213 u = baseui.copy()
214 else:
214 else:
215 u = uimod.ui.load()
215 u = uimod.ui.load()
216 r = hg.repository(u, repo)
216 r = hg.repository(u, repo)
217 else:
217 else:
218 # we trust caller to give us a private copy
218 # we trust caller to give us a private copy
219 r = repo
219 r = repo
220
220
221 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
221 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
222 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
222 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
223 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
223 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
224 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
224 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
225 # resolve file patterns relative to repo root
225 # resolve file patterns relative to repo root
226 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
226 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
227 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
227 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
228 # it's unlikely that we can replace signal handlers in WSGI server,
229 # and mod_wsgi issues a big warning. a plain hgweb process (with no
230 # threading) could replace signal handlers, but we don't bother
231 # conditionally enabling it.
232 r.ui.setconfig('ui', 'signal-safe-lock', 'false', 'hgweb')
233 r.baseui.setconfig('ui', 'signal-safe-lock', 'false', 'hgweb')
228 # displaying bundling progress bar while serving feel wrong and may
234 # displaying bundling progress bar while serving feel wrong and may
229 # break some wsgi implementation.
235 # break some wsgi implementation.
230 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
236 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
231 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
237 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
232 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
238 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
233 self._lastrepo = self._repos[0]
239 self._lastrepo = self._repos[0]
234 hook.redirect(True)
240 hook.redirect(True)
235 self.reponame = name
241 self.reponame = name
236
242
237 def _webifyrepo(self, repo):
243 def _webifyrepo(self, repo):
238 repo = getwebview(repo)
244 repo = getwebview(repo)
239 self.websubtable = webutil.getwebsubs(repo)
245 self.websubtable = webutil.getwebsubs(repo)
240 return repo
246 return repo
241
247
242 @contextlib.contextmanager
248 @contextlib.contextmanager
243 def _obtainrepo(self):
249 def _obtainrepo(self):
244 """Obtain a repo unique to the caller.
250 """Obtain a repo unique to the caller.
245
251
246 Internally we maintain a stack of cachedlocalrepo instances
252 Internally we maintain a stack of cachedlocalrepo instances
247 to be handed out. If one is available, we pop it and return it,
253 to be handed out. If one is available, we pop it and return it,
248 ensuring it is up to date in the process. If one is not available,
254 ensuring it is up to date in the process. If one is not available,
249 we clone the most recently used repo instance and return it.
255 we clone the most recently used repo instance and return it.
250
256
251 It is currently possible for the stack to grow without bounds
257 It is currently possible for the stack to grow without bounds
252 if the server allows infinite threads. However, servers should
258 if the server allows infinite threads. However, servers should
253 have a thread limit, thus establishing our limit.
259 have a thread limit, thus establishing our limit.
254 """
260 """
255 if self._repos:
261 if self._repos:
256 cached = self._repos.pop()
262 cached = self._repos.pop()
257 r, created = cached.fetch()
263 r, created = cached.fetch()
258 else:
264 else:
259 cached = self._lastrepo.copy()
265 cached = self._lastrepo.copy()
260 r, created = cached.fetch()
266 r, created = cached.fetch()
261 if created:
267 if created:
262 r = self._webifyrepo(r)
268 r = self._webifyrepo(r)
263
269
264 self._lastrepo = cached
270 self._lastrepo = cached
265 self.mtime = cached.mtime
271 self.mtime = cached.mtime
266 try:
272 try:
267 yield r
273 yield r
268 finally:
274 finally:
269 self._repos.append(cached)
275 self._repos.append(cached)
270
276
271 def run(self):
277 def run(self):
272 """Start a server from CGI environment.
278 """Start a server from CGI environment.
273
279
274 Modern servers should be using WSGI and should avoid this
280 Modern servers should be using WSGI and should avoid this
275 method, if possible.
281 method, if possible.
276 """
282 """
277 if not encoding.environ.get('GATEWAY_INTERFACE',
283 if not encoding.environ.get('GATEWAY_INTERFACE',
278 '').startswith("CGI/1."):
284 '').startswith("CGI/1."):
279 raise RuntimeError("This function is only intended to be "
285 raise RuntimeError("This function is only intended to be "
280 "called while running as a CGI script.")
286 "called while running as a CGI script.")
281 wsgicgi.launch(self)
287 wsgicgi.launch(self)
282
288
283 def __call__(self, env, respond):
289 def __call__(self, env, respond):
284 """Run the WSGI application.
290 """Run the WSGI application.
285
291
286 This may be called by multiple threads.
292 This may be called by multiple threads.
287 """
293 """
288 req = requestmod.parserequestfromenv(env)
294 req = requestmod.parserequestfromenv(env)
289 res = requestmod.wsgiresponse(req, respond)
295 res = requestmod.wsgiresponse(req, respond)
290
296
291 return self.run_wsgi(req, res)
297 return self.run_wsgi(req, res)
292
298
293 def run_wsgi(self, req, res):
299 def run_wsgi(self, req, res):
294 """Internal method to run the WSGI application.
300 """Internal method to run the WSGI application.
295
301
296 This is typically only called by Mercurial. External consumers
302 This is typically only called by Mercurial. External consumers
297 should be using instances of this class as the WSGI application.
303 should be using instances of this class as the WSGI application.
298 """
304 """
299 with self._obtainrepo() as repo:
305 with self._obtainrepo() as repo:
300 profile = repo.ui.configbool('profiling', 'enabled')
306 profile = repo.ui.configbool('profiling', 'enabled')
301 with profiling.profile(repo.ui, enabled=profile):
307 with profiling.profile(repo.ui, enabled=profile):
302 for r in self._runwsgi(req, res, repo):
308 for r in self._runwsgi(req, res, repo):
303 yield r
309 yield r
304
310
305 def _runwsgi(self, req, res, repo):
311 def _runwsgi(self, req, res, repo):
306 rctx = requestcontext(self, repo, req, res)
312 rctx = requestcontext(self, repo, req, res)
307
313
308 # This state is global across all threads.
314 # This state is global across all threads.
309 encoding.encoding = rctx.config('web', 'encoding')
315 encoding.encoding = rctx.config('web', 'encoding')
310 rctx.repo.ui.environ = req.rawenv
316 rctx.repo.ui.environ = req.rawenv
311
317
312 if rctx.csp:
318 if rctx.csp:
313 # hgwebdir may have added CSP header. Since we generate our own,
319 # hgwebdir may have added CSP header. Since we generate our own,
314 # replace it.
320 # replace it.
315 res.headers['Content-Security-Policy'] = rctx.csp
321 res.headers['Content-Security-Policy'] = rctx.csp
316
322
317 # /api/* is reserved for various API implementations. Dispatch
323 # /api/* is reserved for various API implementations. Dispatch
318 # accordingly. But URL paths can conflict with subrepos and virtual
324 # accordingly. But URL paths can conflict with subrepos and virtual
319 # repos in hgwebdir. So until we have a workaround for this, only
325 # repos in hgwebdir. So until we have a workaround for this, only
320 # expose the URLs if the feature is enabled.
326 # expose the URLs if the feature is enabled.
321 apienabled = rctx.repo.ui.configbool('experimental', 'web.apiserver')
327 apienabled = rctx.repo.ui.configbool('experimental', 'web.apiserver')
322 if apienabled and req.dispatchparts and req.dispatchparts[0] == b'api':
328 if apienabled and req.dispatchparts and req.dispatchparts[0] == b'api':
323 wireprotoserver.handlewsgiapirequest(rctx, req, res,
329 wireprotoserver.handlewsgiapirequest(rctx, req, res,
324 self.check_perm)
330 self.check_perm)
325 return res.sendresponse()
331 return res.sendresponse()
326
332
327 handled = wireprotoserver.handlewsgirequest(
333 handled = wireprotoserver.handlewsgirequest(
328 rctx, req, res, self.check_perm)
334 rctx, req, res, self.check_perm)
329 if handled:
335 if handled:
330 return res.sendresponse()
336 return res.sendresponse()
331
337
332 # Old implementations of hgweb supported dispatching the request via
338 # Old implementations of hgweb supported dispatching the request via
333 # the initial query string parameter instead of using PATH_INFO.
339 # the initial query string parameter instead of using PATH_INFO.
334 # If PATH_INFO is present (signaled by ``req.dispatchpath`` having
340 # If PATH_INFO is present (signaled by ``req.dispatchpath`` having
335 # a value), we use it. Otherwise fall back to the query string.
341 # a value), we use it. Otherwise fall back to the query string.
336 if req.dispatchpath is not None:
342 if req.dispatchpath is not None:
337 query = req.dispatchpath
343 query = req.dispatchpath
338 else:
344 else:
339 query = req.querystring.partition('&')[0].partition(';')[0]
345 query = req.querystring.partition('&')[0].partition(';')[0]
340
346
341 # translate user-visible url structure to internal structure
347 # translate user-visible url structure to internal structure
342
348
343 args = query.split('/', 2)
349 args = query.split('/', 2)
344 if 'cmd' not in req.qsparams and args and args[0]:
350 if 'cmd' not in req.qsparams and args and args[0]:
345 cmd = args.pop(0)
351 cmd = args.pop(0)
346 style = cmd.rfind('-')
352 style = cmd.rfind('-')
347 if style != -1:
353 if style != -1:
348 req.qsparams['style'] = cmd[:style]
354 req.qsparams['style'] = cmd[:style]
349 cmd = cmd[style + 1:]
355 cmd = cmd[style + 1:]
350
356
351 # avoid accepting e.g. style parameter as command
357 # avoid accepting e.g. style parameter as command
352 if util.safehasattr(webcommands, cmd):
358 if util.safehasattr(webcommands, cmd):
353 req.qsparams['cmd'] = cmd
359 req.qsparams['cmd'] = cmd
354
360
355 if cmd == 'static':
361 if cmd == 'static':
356 req.qsparams['file'] = '/'.join(args)
362 req.qsparams['file'] = '/'.join(args)
357 else:
363 else:
358 if args and args[0]:
364 if args and args[0]:
359 node = args.pop(0).replace('%2F', '/')
365 node = args.pop(0).replace('%2F', '/')
360 req.qsparams['node'] = node
366 req.qsparams['node'] = node
361 if args:
367 if args:
362 if 'file' in req.qsparams:
368 if 'file' in req.qsparams:
363 del req.qsparams['file']
369 del req.qsparams['file']
364 for a in args:
370 for a in args:
365 req.qsparams.add('file', a)
371 req.qsparams.add('file', a)
366
372
367 ua = req.headers.get('User-Agent', '')
373 ua = req.headers.get('User-Agent', '')
368 if cmd == 'rev' and 'mercurial' in ua:
374 if cmd == 'rev' and 'mercurial' in ua:
369 req.qsparams['style'] = 'raw'
375 req.qsparams['style'] = 'raw'
370
376
371 if cmd == 'archive':
377 if cmd == 'archive':
372 fn = req.qsparams['node']
378 fn = req.qsparams['node']
373 for type_, spec in webutil.archivespecs.iteritems():
379 for type_, spec in webutil.archivespecs.iteritems():
374 ext = spec[2]
380 ext = spec[2]
375 if fn.endswith(ext):
381 if fn.endswith(ext):
376 req.qsparams['node'] = fn[:-len(ext)]
382 req.qsparams['node'] = fn[:-len(ext)]
377 req.qsparams['type'] = type_
383 req.qsparams['type'] = type_
378 else:
384 else:
379 cmd = req.qsparams.get('cmd', '')
385 cmd = req.qsparams.get('cmd', '')
380
386
381 # process the web interface request
387 # process the web interface request
382
388
383 try:
389 try:
384 rctx.tmpl = rctx.templater(req)
390 rctx.tmpl = rctx.templater(req)
385 ctype = rctx.tmpl.render('mimetype',
391 ctype = rctx.tmpl.render('mimetype',
386 {'encoding': encoding.encoding})
392 {'encoding': encoding.encoding})
387
393
388 # check read permissions non-static content
394 # check read permissions non-static content
389 if cmd != 'static':
395 if cmd != 'static':
390 self.check_perm(rctx, req, None)
396 self.check_perm(rctx, req, None)
391
397
392 if cmd == '':
398 if cmd == '':
393 req.qsparams['cmd'] = rctx.tmpl.render('default', {})
399 req.qsparams['cmd'] = rctx.tmpl.render('default', {})
394 cmd = req.qsparams['cmd']
400 cmd = req.qsparams['cmd']
395
401
396 # Don't enable caching if using a CSP nonce because then it wouldn't
402 # Don't enable caching if using a CSP nonce because then it wouldn't
397 # be a nonce.
403 # be a nonce.
398 if rctx.configbool('web', 'cache') and not rctx.nonce:
404 if rctx.configbool('web', 'cache') and not rctx.nonce:
399 tag = 'W/"%d"' % self.mtime
405 tag = 'W/"%d"' % self.mtime
400 if req.headers.get('If-None-Match') == tag:
406 if req.headers.get('If-None-Match') == tag:
401 res.status = '304 Not Modified'
407 res.status = '304 Not Modified'
402 # Content-Type may be defined globally. It isn't valid on a
408 # Content-Type may be defined globally. It isn't valid on a
403 # 304, so discard it.
409 # 304, so discard it.
404 try:
410 try:
405 del res.headers[b'Content-Type']
411 del res.headers[b'Content-Type']
406 except KeyError:
412 except KeyError:
407 pass
413 pass
408 # Response body not allowed on 304.
414 # Response body not allowed on 304.
409 res.setbodybytes('')
415 res.setbodybytes('')
410 return res.sendresponse()
416 return res.sendresponse()
411
417
412 res.headers['ETag'] = tag
418 res.headers['ETag'] = tag
413
419
414 if cmd not in webcommands.__all__:
420 if cmd not in webcommands.__all__:
415 msg = 'no such method: %s' % cmd
421 msg = 'no such method: %s' % cmd
416 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
422 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
417 else:
423 else:
418 # Set some globals appropriate for web handlers. Commands can
424 # Set some globals appropriate for web handlers. Commands can
419 # override easily enough.
425 # override easily enough.
420 res.status = '200 Script output follows'
426 res.status = '200 Script output follows'
421 res.headers['Content-Type'] = ctype
427 res.headers['Content-Type'] = ctype
422 return getattr(webcommands, cmd)(rctx)
428 return getattr(webcommands, cmd)(rctx)
423
429
424 except (error.LookupError, error.RepoLookupError) as err:
430 except (error.LookupError, error.RepoLookupError) as err:
425 msg = pycompat.bytestr(err)
431 msg = pycompat.bytestr(err)
426 if (util.safehasattr(err, 'name') and
432 if (util.safehasattr(err, 'name') and
427 not isinstance(err, error.ManifestLookupError)):
433 not isinstance(err, error.ManifestLookupError)):
428 msg = 'revision not found: %s' % err.name
434 msg = 'revision not found: %s' % err.name
429
435
430 res.status = '404 Not Found'
436 res.status = '404 Not Found'
431 res.headers['Content-Type'] = ctype
437 res.headers['Content-Type'] = ctype
432 return rctx.sendtemplate('error', error=msg)
438 return rctx.sendtemplate('error', error=msg)
433 except (error.RepoError, error.RevlogError) as e:
439 except (error.RepoError, error.RevlogError) as e:
434 res.status = '500 Internal Server Error'
440 res.status = '500 Internal Server Error'
435 res.headers['Content-Type'] = ctype
441 res.headers['Content-Type'] = ctype
436 return rctx.sendtemplate('error', error=pycompat.bytestr(e))
442 return rctx.sendtemplate('error', error=pycompat.bytestr(e))
437 except ErrorResponse as e:
443 except ErrorResponse as e:
438 res.status = statusmessage(e.code, pycompat.bytestr(e))
444 res.status = statusmessage(e.code, pycompat.bytestr(e))
439 res.headers['Content-Type'] = ctype
445 res.headers['Content-Type'] = ctype
440 return rctx.sendtemplate('error', error=pycompat.bytestr(e))
446 return rctx.sendtemplate('error', error=pycompat.bytestr(e))
441
447
442 def check_perm(self, rctx, req, op):
448 def check_perm(self, rctx, req, op):
443 for permhook in permhooks:
449 for permhook in permhooks:
444 permhook(rctx, req, op)
450 permhook(rctx, req, op)
445
451
446 def getwebview(repo):
452 def getwebview(repo):
447 """The 'web.view' config controls changeset filter to hgweb. Possible
453 """The 'web.view' config controls changeset filter to hgweb. Possible
448 values are ``served``, ``visible`` and ``all``. Default is ``served``.
454 values are ``served``, ``visible`` and ``all``. Default is ``served``.
449 The ``served`` filter only shows changesets that can be pulled from the
455 The ``served`` filter only shows changesets that can be pulled from the
450 hgweb instance. The``visible`` filter includes secret changesets but
456 hgweb instance. The``visible`` filter includes secret changesets but
451 still excludes "hidden" one.
457 still excludes "hidden" one.
452
458
453 See the repoview module for details.
459 See the repoview module for details.
454
460
455 The option has been around undocumented since Mercurial 2.5, but no
461 The option has been around undocumented since Mercurial 2.5, but no
456 user ever asked about it. So we better keep it undocumented for now."""
462 user ever asked about it. So we better keep it undocumented for now."""
457 # experimental config: web.view
463 # experimental config: web.view
458 viewconfig = repo.ui.config('web', 'view', untrusted=True)
464 viewconfig = repo.ui.config('web', 'view', untrusted=True)
459 if viewconfig == 'all':
465 if viewconfig == 'all':
460 return repo.unfiltered()
466 return repo.unfiltered()
461 elif viewconfig in repoview.filtertable:
467 elif viewconfig in repoview.filtertable:
462 return repo.filtered(viewconfig)
468 return repo.filtered(viewconfig)
463 else:
469 else:
464 return repo.filtered('served')
470 return repo.filtered('served')
@@ -1,897 +1,943 b''
1 #require serve
1 #require serve
2
2
3 Some tests for hgweb. Tests static files, plain files and different 404's.
3 Some tests for hgweb. Tests static files, plain files and different 404's.
4
4
5 $ hg init test
5 $ hg init test
6 $ cd test
6 $ cd test
7 $ mkdir da
7 $ mkdir da
8 $ echo foo > da/foo
8 $ echo foo > da/foo
9 $ echo foo > foo
9 $ echo foo > foo
10 $ hg ci -Ambase
10 $ hg ci -Ambase
11 adding da/foo
11 adding da/foo
12 adding foo
12 adding foo
13 $ hg bookmark -r0 '@'
13 $ hg bookmark -r0 '@'
14 $ hg bookmark -r0 'a b c'
14 $ hg bookmark -r0 'a b c'
15 $ hg bookmark -r0 'd/e/f'
15 $ hg bookmark -r0 'd/e/f'
16 $ hg serve -n test -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
16 $ hg serve -n test -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
17 $ cat hg.pid >> $DAEMON_PIDS
17 $ cat hg.pid >> $DAEMON_PIDS
18
18
19 manifest
19 manifest
20
20
21 $ (get-with-headers.py localhost:$HGPORT 'file/tip/?style=raw')
21 $ (get-with-headers.py localhost:$HGPORT 'file/tip/?style=raw')
22 200 Script output follows
22 200 Script output follows
23
23
24
24
25 drwxr-xr-x da
25 drwxr-xr-x da
26 -rw-r--r-- 4 foo
26 -rw-r--r-- 4 foo
27
27
28
28
29 $ (get-with-headers.py localhost:$HGPORT 'file/tip/da?style=raw')
29 $ (get-with-headers.py localhost:$HGPORT 'file/tip/da?style=raw')
30 200 Script output follows
30 200 Script output follows
31
31
32
32
33 -rw-r--r-- 4 foo
33 -rw-r--r-- 4 foo
34
34
35
35
36
36
37 plain file
37 plain file
38
38
39 $ get-with-headers.py localhost:$HGPORT 'file/tip/foo?style=raw'
39 $ get-with-headers.py localhost:$HGPORT 'file/tip/foo?style=raw'
40 200 Script output follows
40 200 Script output follows
41
41
42 foo
42 foo
43
43
44 should give a 404 - static file that does not exist
44 should give a 404 - static file that does not exist
45
45
46 $ get-with-headers.py localhost:$HGPORT 'static/bogus'
46 $ get-with-headers.py localhost:$HGPORT 'static/bogus'
47 404 Not Found
47 404 Not Found
48
48
49 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
49 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
50 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
50 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
51 <head>
51 <head>
52 <link rel="icon" href="/static/hgicon.png" type="image/png" />
52 <link rel="icon" href="/static/hgicon.png" type="image/png" />
53 <meta name="robots" content="index, nofollow" />
53 <meta name="robots" content="index, nofollow" />
54 <link rel="stylesheet" href="/static/style-paper.css" type="text/css" />
54 <link rel="stylesheet" href="/static/style-paper.css" type="text/css" />
55 <script type="text/javascript" src="/static/mercurial.js"></script>
55 <script type="text/javascript" src="/static/mercurial.js"></script>
56
56
57 <title>test: error</title>
57 <title>test: error</title>
58 </head>
58 </head>
59 <body>
59 <body>
60
60
61 <div class="container">
61 <div class="container">
62 <div class="menu">
62 <div class="menu">
63 <div class="logo">
63 <div class="logo">
64 <a href="https://mercurial-scm.org/">
64 <a href="https://mercurial-scm.org/">
65 <img src="/static/hglogo.png" width=75 height=90 border=0 alt="mercurial" /></a>
65 <img src="/static/hglogo.png" width=75 height=90 border=0 alt="mercurial" /></a>
66 </div>
66 </div>
67 <ul>
67 <ul>
68 <li><a href="/shortlog">log</a></li>
68 <li><a href="/shortlog">log</a></li>
69 <li><a href="/graph">graph</a></li>
69 <li><a href="/graph">graph</a></li>
70 <li><a href="/tags">tags</a></li>
70 <li><a href="/tags">tags</a></li>
71 <li><a href="/bookmarks">bookmarks</a></li>
71 <li><a href="/bookmarks">bookmarks</a></li>
72 <li><a href="/branches">branches</a></li>
72 <li><a href="/branches">branches</a></li>
73 </ul>
73 </ul>
74 <ul>
74 <ul>
75 <li><a href="/help">help</a></li>
75 <li><a href="/help">help</a></li>
76 </ul>
76 </ul>
77 </div>
77 </div>
78
78
79 <div class="main">
79 <div class="main">
80
80
81 <h2 class="breadcrumb"><a href="/">Mercurial</a> </h2>
81 <h2 class="breadcrumb"><a href="/">Mercurial</a> </h2>
82 <h3>error</h3>
82 <h3>error</h3>
83
83
84
84
85 <form class="search" action="/log">
85 <form class="search" action="/log">
86
86
87 <p><input name="rev" id="search1" type="text" size="30" value="" /></p>
87 <p><input name="rev" id="search1" type="text" size="30" value="" /></p>
88 <div id="hint">Find changesets by keywords (author, files, the commit message), revision
88 <div id="hint">Find changesets by keywords (author, files, the commit message), revision
89 number or hash, or <a href="/help/revsets">revset expression</a>.</div>
89 number or hash, or <a href="/help/revsets">revset expression</a>.</div>
90 </form>
90 </form>
91
91
92 <div class="description">
92 <div class="description">
93 <p>
93 <p>
94 An error occurred while processing your request:
94 An error occurred while processing your request:
95 </p>
95 </p>
96 <p>
96 <p>
97 Not Found
97 Not Found
98 </p>
98 </p>
99 </div>
99 </div>
100 </div>
100 </div>
101 </div>
101 </div>
102
102
103
103
104
104
105 </body>
105 </body>
106 </html>
106 </html>
107
107
108 [1]
108 [1]
109
109
110 should give a 404 - bad revision
110 should give a 404 - bad revision
111
111
112 $ get-with-headers.py localhost:$HGPORT 'file/spam/foo?style=raw'
112 $ get-with-headers.py localhost:$HGPORT 'file/spam/foo?style=raw'
113 404 Not Found
113 404 Not Found
114
114
115
115
116 error: revision not found: spam
116 error: revision not found: spam
117 [1]
117 [1]
118
118
119 should give a 400 - bad command
119 should give a 400 - bad command
120
120
121 $ get-with-headers.py localhost:$HGPORT 'file/tip/foo?cmd=spam&style=raw'
121 $ get-with-headers.py localhost:$HGPORT 'file/tip/foo?cmd=spam&style=raw'
122 400* (glob)
122 400* (glob)
123
123
124
124
125 error: no such method: spam
125 error: no such method: spam
126 [1]
126 [1]
127
127
128 $ get-with-headers.py --headeronly localhost:$HGPORT '?cmd=spam'
128 $ get-with-headers.py --headeronly localhost:$HGPORT '?cmd=spam'
129 400 no such method: spam
129 400 no such method: spam
130 [1]
130 [1]
131
131
132 should give a 400 - bad command as a part of url path (issue4071)
132 should give a 400 - bad command as a part of url path (issue4071)
133
133
134 $ get-with-headers.py --headeronly localhost:$HGPORT 'spam'
134 $ get-with-headers.py --headeronly localhost:$HGPORT 'spam'
135 400 no such method: spam
135 400 no such method: spam
136 [1]
136 [1]
137
137
138 $ get-with-headers.py --headeronly localhost:$HGPORT 'raw-spam'
138 $ get-with-headers.py --headeronly localhost:$HGPORT 'raw-spam'
139 400 no such method: spam
139 400 no such method: spam
140 [1]
140 [1]
141
141
142 $ get-with-headers.py --headeronly localhost:$HGPORT 'spam/tip/foo'
142 $ get-with-headers.py --headeronly localhost:$HGPORT 'spam/tip/foo'
143 400 no such method: spam
143 400 no such method: spam
144 [1]
144 [1]
145
145
146 should give a 404 - file does not exist
146 should give a 404 - file does not exist
147
147
148 $ get-with-headers.py localhost:$HGPORT 'file/tip/bork?style=raw'
148 $ get-with-headers.py localhost:$HGPORT 'file/tip/bork?style=raw'
149 404 Not Found
149 404 Not Found
150
150
151
151
152 error: bork@2ef0ac749a14: not found in manifest
152 error: bork@2ef0ac749a14: not found in manifest
153 [1]
153 [1]
154 $ get-with-headers.py localhost:$HGPORT 'file/tip/bork'
154 $ get-with-headers.py localhost:$HGPORT 'file/tip/bork'
155 404 Not Found
155 404 Not Found
156
156
157 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
157 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
158 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
158 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
159 <head>
159 <head>
160 <link rel="icon" href="/static/hgicon.png" type="image/png" />
160 <link rel="icon" href="/static/hgicon.png" type="image/png" />
161 <meta name="robots" content="index, nofollow" />
161 <meta name="robots" content="index, nofollow" />
162 <link rel="stylesheet" href="/static/style-paper.css" type="text/css" />
162 <link rel="stylesheet" href="/static/style-paper.css" type="text/css" />
163 <script type="text/javascript" src="/static/mercurial.js"></script>
163 <script type="text/javascript" src="/static/mercurial.js"></script>
164
164
165 <title>test: error</title>
165 <title>test: error</title>
166 </head>
166 </head>
167 <body>
167 <body>
168
168
169 <div class="container">
169 <div class="container">
170 <div class="menu">
170 <div class="menu">
171 <div class="logo">
171 <div class="logo">
172 <a href="https://mercurial-scm.org/">
172 <a href="https://mercurial-scm.org/">
173 <img src="/static/hglogo.png" width=75 height=90 border=0 alt="mercurial" /></a>
173 <img src="/static/hglogo.png" width=75 height=90 border=0 alt="mercurial" /></a>
174 </div>
174 </div>
175 <ul>
175 <ul>
176 <li><a href="/shortlog">log</a></li>
176 <li><a href="/shortlog">log</a></li>
177 <li><a href="/graph">graph</a></li>
177 <li><a href="/graph">graph</a></li>
178 <li><a href="/tags">tags</a></li>
178 <li><a href="/tags">tags</a></li>
179 <li><a href="/bookmarks">bookmarks</a></li>
179 <li><a href="/bookmarks">bookmarks</a></li>
180 <li><a href="/branches">branches</a></li>
180 <li><a href="/branches">branches</a></li>
181 </ul>
181 </ul>
182 <ul>
182 <ul>
183 <li><a href="/help">help</a></li>
183 <li><a href="/help">help</a></li>
184 </ul>
184 </ul>
185 </div>
185 </div>
186
186
187 <div class="main">
187 <div class="main">
188
188
189 <h2 class="breadcrumb"><a href="/">Mercurial</a> </h2>
189 <h2 class="breadcrumb"><a href="/">Mercurial</a> </h2>
190 <h3>error</h3>
190 <h3>error</h3>
191
191
192
192
193 <form class="search" action="/log">
193 <form class="search" action="/log">
194
194
195 <p><input name="rev" id="search1" type="text" size="30" value="" /></p>
195 <p><input name="rev" id="search1" type="text" size="30" value="" /></p>
196 <div id="hint">Find changesets by keywords (author, files, the commit message), revision
196 <div id="hint">Find changesets by keywords (author, files, the commit message), revision
197 number or hash, or <a href="/help/revsets">revset expression</a>.</div>
197 number or hash, or <a href="/help/revsets">revset expression</a>.</div>
198 </form>
198 </form>
199
199
200 <div class="description">
200 <div class="description">
201 <p>
201 <p>
202 An error occurred while processing your request:
202 An error occurred while processing your request:
203 </p>
203 </p>
204 <p>
204 <p>
205 bork@2ef0ac749a14: not found in manifest
205 bork@2ef0ac749a14: not found in manifest
206 </p>
206 </p>
207 </div>
207 </div>
208 </div>
208 </div>
209 </div>
209 </div>
210
210
211
211
212
212
213 </body>
213 </body>
214 </html>
214 </html>
215
215
216 [1]
216 [1]
217 $ get-with-headers.py localhost:$HGPORT 'diff/tip/bork?style=raw'
217 $ get-with-headers.py localhost:$HGPORT 'diff/tip/bork?style=raw'
218 404 Not Found
218 404 Not Found
219
219
220
220
221 error: bork@2ef0ac749a14: not found in manifest
221 error: bork@2ef0ac749a14: not found in manifest
222 [1]
222 [1]
223
223
224 try bad style
224 try bad style
225
225
226 $ (get-with-headers.py localhost:$HGPORT 'file/tip/?style=foobar')
226 $ (get-with-headers.py localhost:$HGPORT 'file/tip/?style=foobar')
227 200 Script output follows
227 200 Script output follows
228
228
229 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
229 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
230 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
230 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
231 <head>
231 <head>
232 <link rel="icon" href="/static/hgicon.png" type="image/png" />
232 <link rel="icon" href="/static/hgicon.png" type="image/png" />
233 <meta name="robots" content="index, nofollow" />
233 <meta name="robots" content="index, nofollow" />
234 <link rel="stylesheet" href="/static/style-paper.css" type="text/css" />
234 <link rel="stylesheet" href="/static/style-paper.css" type="text/css" />
235 <script type="text/javascript" src="/static/mercurial.js"></script>
235 <script type="text/javascript" src="/static/mercurial.js"></script>
236
236
237 <title>test: 2ef0ac749a14 /</title>
237 <title>test: 2ef0ac749a14 /</title>
238 </head>
238 </head>
239 <body>
239 <body>
240
240
241 <div class="container">
241 <div class="container">
242 <div class="menu">
242 <div class="menu">
243 <div class="logo">
243 <div class="logo">
244 <a href="https://mercurial-scm.org/">
244 <a href="https://mercurial-scm.org/">
245 <img src="/static/hglogo.png" alt="mercurial" /></a>
245 <img src="/static/hglogo.png" alt="mercurial" /></a>
246 </div>
246 </div>
247 <ul>
247 <ul>
248 <li><a href="/shortlog/tip">log</a></li>
248 <li><a href="/shortlog/tip">log</a></li>
249 <li><a href="/graph/tip">graph</a></li>
249 <li><a href="/graph/tip">graph</a></li>
250 <li><a href="/tags">tags</a></li>
250 <li><a href="/tags">tags</a></li>
251 <li><a href="/bookmarks">bookmarks</a></li>
251 <li><a href="/bookmarks">bookmarks</a></li>
252 <li><a href="/branches">branches</a></li>
252 <li><a href="/branches">branches</a></li>
253 </ul>
253 </ul>
254 <ul>
254 <ul>
255 <li><a href="/rev/tip">changeset</a></li>
255 <li><a href="/rev/tip">changeset</a></li>
256 <li class="active">browse</li>
256 <li class="active">browse</li>
257 </ul>
257 </ul>
258 <ul>
258 <ul>
259
259
260 </ul>
260 </ul>
261 <ul>
261 <ul>
262 <li><a href="/help">help</a></li>
262 <li><a href="/help">help</a></li>
263 </ul>
263 </ul>
264 </div>
264 </div>
265
265
266 <div class="main">
266 <div class="main">
267 <h2 class="breadcrumb"><a href="/">Mercurial</a> </h2>
267 <h2 class="breadcrumb"><a href="/">Mercurial</a> </h2>
268 <h3>
268 <h3>
269 directory / @ 0:<a href="/rev/2ef0ac749a14">2ef0ac749a14</a>
269 directory / @ 0:<a href="/rev/2ef0ac749a14">2ef0ac749a14</a>
270 <span class="phase">draft</span> <span class="branchhead">default</span> <span class="tag">tip</span> <span class="tag">@</span> <span class="tag">a b c</span> <span class="tag">d/e/f</span>
270 <span class="phase">draft</span> <span class="branchhead">default</span> <span class="tag">tip</span> <span class="tag">@</span> <span class="tag">a b c</span> <span class="tag">d/e/f</span>
271 </h3>
271 </h3>
272
272
273
273
274 <form class="search" action="/log">
274 <form class="search" action="/log">
275
275
276 <p><input name="rev" id="search1" type="text" size="30" value="" /></p>
276 <p><input name="rev" id="search1" type="text" size="30" value="" /></p>
277 <div id="hint">Find changesets by keywords (author, files, the commit message), revision
277 <div id="hint">Find changesets by keywords (author, files, the commit message), revision
278 number or hash, or <a href="/help/revsets">revset expression</a>.</div>
278 number or hash, or <a href="/help/revsets">revset expression</a>.</div>
279 </form>
279 </form>
280
280
281 <table class="bigtable">
281 <table class="bigtable">
282 <thead>
282 <thead>
283 <tr>
283 <tr>
284 <th class="name">name</th>
284 <th class="name">name</th>
285 <th class="size">size</th>
285 <th class="size">size</th>
286 <th class="permissions">permissions</th>
286 <th class="permissions">permissions</th>
287 </tr>
287 </tr>
288 </thead>
288 </thead>
289 <tbody class="stripes2">
289 <tbody class="stripes2">
290 <tr class="fileline">
290 <tr class="fileline">
291 <td class="name"><a href="/file/tip/">[up]</a></td>
291 <td class="name"><a href="/file/tip/">[up]</a></td>
292 <td class="size"></td>
292 <td class="size"></td>
293 <td class="permissions">drwxr-xr-x</td>
293 <td class="permissions">drwxr-xr-x</td>
294 </tr>
294 </tr>
295
295
296 <tr class="fileline">
296 <tr class="fileline">
297 <td class="name">
297 <td class="name">
298 <a href="/file/tip/da">
298 <a href="/file/tip/da">
299 <img src="/static/coal-folder.png" alt="dir."/> da/
299 <img src="/static/coal-folder.png" alt="dir."/> da/
300 </a>
300 </a>
301 <a href="/file/tip/da/">
301 <a href="/file/tip/da/">
302
302
303 </a>
303 </a>
304 </td>
304 </td>
305 <td class="size"></td>
305 <td class="size"></td>
306 <td class="permissions">drwxr-xr-x</td>
306 <td class="permissions">drwxr-xr-x</td>
307 </tr>
307 </tr>
308
308
309 <tr class="fileline">
309 <tr class="fileline">
310 <td class="filename">
310 <td class="filename">
311 <a href="/file/tip/foo">
311 <a href="/file/tip/foo">
312 <img src="/static/coal-file.png" alt="file"/> foo
312 <img src="/static/coal-file.png" alt="file"/> foo
313 </a>
313 </a>
314 </td>
314 </td>
315 <td class="size">4</td>
315 <td class="size">4</td>
316 <td class="permissions">-rw-r--r--</td>
316 <td class="permissions">-rw-r--r--</td>
317 </tr>
317 </tr>
318 </tbody>
318 </tbody>
319 </table>
319 </table>
320 </div>
320 </div>
321 </div>
321 </div>
322
322
323
323
324 </body>
324 </body>
325 </html>
325 </html>
326
326
327
327
328 stop and restart
328 stop and restart
329
329
330 $ killdaemons.py
330 $ killdaemons.py
331 $ hg serve -p $HGPORT -d --pid-file=hg.pid -A access.log
331 $ hg serve -p $HGPORT -d --pid-file=hg.pid -A access.log
332 $ cat hg.pid >> $DAEMON_PIDS
332 $ cat hg.pid >> $DAEMON_PIDS
333
333
334 Test the access/error files are opened in append mode
334 Test the access/error files are opened in append mode
335
335
336 $ $PYTHON -c "print len(open('access.log', 'rb').readlines()), 'log lines written'"
336 $ $PYTHON -c "print len(open('access.log', 'rb').readlines()), 'log lines written'"
337 14 log lines written
337 14 log lines written
338
338
339 static file
339 static file
340
340
341 $ get-with-headers.py --twice localhost:$HGPORT 'static/style-gitweb.css' - date etag server
341 $ get-with-headers.py --twice localhost:$HGPORT 'static/style-gitweb.css' - date etag server
342 200 Script output follows
342 200 Script output follows
343 content-length: 9059
343 content-length: 9059
344 content-type: text/css
344 content-type: text/css
345
345
346 body { font-family: sans-serif; font-size: 12px; border:solid #d9d8d1; border-width:1px; margin:10px; background: white; color: black; }
346 body { font-family: sans-serif; font-size: 12px; border:solid #d9d8d1; border-width:1px; margin:10px; background: white; color: black; }
347 a { color:#0000cc; }
347 a { color:#0000cc; }
348 a:hover, a:visited, a:active { color:#880000; }
348 a:hover, a:visited, a:active { color:#880000; }
349 div.page_header { height:25px; padding:8px; font-size:18px; font-weight:bold; background-color:#d9d8d1; }
349 div.page_header { height:25px; padding:8px; font-size:18px; font-weight:bold; background-color:#d9d8d1; }
350 div.page_header a:visited { color:#0000cc; }
350 div.page_header a:visited { color:#0000cc; }
351 div.page_header a:hover { color:#880000; }
351 div.page_header a:hover { color:#880000; }
352 div.page_nav {
352 div.page_nav {
353 padding:8px;
353 padding:8px;
354 display: flex;
354 display: flex;
355 justify-content: space-between;
355 justify-content: space-between;
356 align-items: center;
356 align-items: center;
357 }
357 }
358 div.page_nav a:visited { color:#0000cc; }
358 div.page_nav a:visited { color:#0000cc; }
359 div.extra_nav {
359 div.extra_nav {
360 padding: 8px;
360 padding: 8px;
361 }
361 }
362 div.extra_nav a:visited {
362 div.extra_nav a:visited {
363 color: #0000cc;
363 color: #0000cc;
364 }
364 }
365 div.page_path { padding:8px; border:solid #d9d8d1; border-width:0px 0px 1px}
365 div.page_path { padding:8px; border:solid #d9d8d1; border-width:0px 0px 1px}
366 div.page_footer { padding:4px 8px; background-color: #d9d8d1; }
366 div.page_footer { padding:4px 8px; background-color: #d9d8d1; }
367 div.page_footer_text { float:left; color:#555555; font-style:italic; }
367 div.page_footer_text { float:left; color:#555555; font-style:italic; }
368 div.page_body { padding:8px; }
368 div.page_body { padding:8px; }
369 div.title, a.title {
369 div.title, a.title {
370 display:block; padding:6px 8px;
370 display:block; padding:6px 8px;
371 font-weight:bold; background-color:#edece6; text-decoration:none; color:#000000;
371 font-weight:bold; background-color:#edece6; text-decoration:none; color:#000000;
372 }
372 }
373 a.title:hover { background-color: #d9d8d1; }
373 a.title:hover { background-color: #d9d8d1; }
374 div.title_text { padding:6px 0px; border: solid #d9d8d1; border-width:0px 0px 1px; }
374 div.title_text { padding:6px 0px; border: solid #d9d8d1; border-width:0px 0px 1px; }
375 div.log_body { padding:8px 8px 8px 150px; }
375 div.log_body { padding:8px 8px 8px 150px; }
376 .age { white-space:nowrap; }
376 .age { white-space:nowrap; }
377 a.title span.age { position:relative; float:left; width:142px; font-style:italic; }
377 a.title span.age { position:relative; float:left; width:142px; font-style:italic; }
378 div.log_link {
378 div.log_link {
379 padding:0px 8px;
379 padding:0px 8px;
380 font-size:10px; font-family:sans-serif; font-style:normal;
380 font-size:10px; font-family:sans-serif; font-style:normal;
381 position:relative; float:left; width:136px;
381 position:relative; float:left; width:136px;
382 }
382 }
383 div.list_head { padding:6px 8px 4px; border:solid #d9d8d1; border-width:1px 0px 0px; font-style:italic; }
383 div.list_head { padding:6px 8px 4px; border:solid #d9d8d1; border-width:1px 0px 0px; font-style:italic; }
384 a.list { text-decoration:none; color:#000000; }
384 a.list { text-decoration:none; color:#000000; }
385 a.list:hover { text-decoration:underline; color:#880000; }
385 a.list:hover { text-decoration:underline; color:#880000; }
386 table { padding:8px 4px; }
386 table { padding:8px 4px; }
387 th { padding:2px 5px; font-size:12px; text-align:left; }
387 th { padding:2px 5px; font-size:12px; text-align:left; }
388 .parity0 { background-color:#ffffff; }
388 .parity0 { background-color:#ffffff; }
389 tr.dark, .parity1, pre.sourcelines.stripes > :nth-child(4n+4) { background-color:#f6f6f0; }
389 tr.dark, .parity1, pre.sourcelines.stripes > :nth-child(4n+4) { background-color:#f6f6f0; }
390 tr.light:hover, .parity0:hover, tr.dark:hover, .parity1:hover,
390 tr.light:hover, .parity0:hover, tr.dark:hover, .parity1:hover,
391 pre.sourcelines.stripes > :nth-child(4n+2):hover,
391 pre.sourcelines.stripes > :nth-child(4n+2):hover,
392 pre.sourcelines.stripes > :nth-child(4n+4):hover,
392 pre.sourcelines.stripes > :nth-child(4n+4):hover,
393 pre.sourcelines.stripes > :nth-child(4n+1):hover + :nth-child(4n+2),
393 pre.sourcelines.stripes > :nth-child(4n+1):hover + :nth-child(4n+2),
394 pre.sourcelines.stripes > :nth-child(4n+3):hover + :nth-child(4n+4) { background-color:#edece6; }
394 pre.sourcelines.stripes > :nth-child(4n+3):hover + :nth-child(4n+4) { background-color:#edece6; }
395 td { padding:2px 5px; font-size:12px; vertical-align:top; }
395 td { padding:2px 5px; font-size:12px; vertical-align:top; }
396 td.closed { background-color: #99f; }
396 td.closed { background-color: #99f; }
397 td.link { padding:2px 5px; font-family:sans-serif; font-size:10px; }
397 td.link { padding:2px 5px; font-family:sans-serif; font-size:10px; }
398 td.indexlinks { white-space: nowrap; }
398 td.indexlinks { white-space: nowrap; }
399 td.indexlinks a {
399 td.indexlinks a {
400 padding: 2px 5px; line-height: 10px;
400 padding: 2px 5px; line-height: 10px;
401 border: 1px solid;
401 border: 1px solid;
402 color: #ffffff; background-color: #7777bb;
402 color: #ffffff; background-color: #7777bb;
403 border-color: #aaaadd #333366 #333366 #aaaadd;
403 border-color: #aaaadd #333366 #333366 #aaaadd;
404 font-weight: bold; text-align: center; text-decoration: none;
404 font-weight: bold; text-align: center; text-decoration: none;
405 font-size: 10px;
405 font-size: 10px;
406 }
406 }
407 td.indexlinks a:hover { background-color: #6666aa; }
407 td.indexlinks a:hover { background-color: #6666aa; }
408 div.pre { font-family:monospace; font-size:12px; white-space:pre; }
408 div.pre { font-family:monospace; font-size:12px; white-space:pre; }
409
409
410 .search {
410 .search {
411 margin-right: 8px;
411 margin-right: 8px;
412 }
412 }
413
413
414 div#hint {
414 div#hint {
415 position: absolute;
415 position: absolute;
416 display: none;
416 display: none;
417 width: 250px;
417 width: 250px;
418 padding: 5px;
418 padding: 5px;
419 background: #ffc;
419 background: #ffc;
420 border: 1px solid yellow;
420 border: 1px solid yellow;
421 border-radius: 5px;
421 border-radius: 5px;
422 }
422 }
423
423
424 #searchform:hover div#hint { display: block; }
424 #searchform:hover div#hint { display: block; }
425
425
426 tr.thisrev a { color:#999999; text-decoration: none; }
426 tr.thisrev a { color:#999999; text-decoration: none; }
427 tr.thisrev pre { color:#009900; }
427 tr.thisrev pre { color:#009900; }
428 td.annotate {
428 td.annotate {
429 white-space: nowrap;
429 white-space: nowrap;
430 }
430 }
431 div.annotate-info {
431 div.annotate-info {
432 z-index: 5;
432 z-index: 5;
433 display: none;
433 display: none;
434 position: absolute;
434 position: absolute;
435 background-color: #FFFFFF;
435 background-color: #FFFFFF;
436 border: 1px solid #d9d8d1;
436 border: 1px solid #d9d8d1;
437 text-align: left;
437 text-align: left;
438 color: #000000;
438 color: #000000;
439 padding: 5px;
439 padding: 5px;
440 }
440 }
441 div.annotate-info a { color: #0000FF; text-decoration: underline; }
441 div.annotate-info a { color: #0000FF; text-decoration: underline; }
442 td.annotate:hover div.annotate-info { display: inline; }
442 td.annotate:hover div.annotate-info { display: inline; }
443
443
444 #diffopts-form {
444 #diffopts-form {
445 padding-left: 8px;
445 padding-left: 8px;
446 display: none;
446 display: none;
447 }
447 }
448
448
449 .linenr { color:#999999; text-decoration:none }
449 .linenr { color:#999999; text-decoration:none }
450 div.rss_logo { float: right; white-space: nowrap; }
450 div.rss_logo { float: right; white-space: nowrap; }
451 div.rss_logo a {
451 div.rss_logo a {
452 padding:3px 6px; line-height:10px;
452 padding:3px 6px; line-height:10px;
453 border:1px solid; border-color:#fcc7a5 #7d3302 #3e1a01 #ff954e;
453 border:1px solid; border-color:#fcc7a5 #7d3302 #3e1a01 #ff954e;
454 color:#ffffff; background-color:#ff6600;
454 color:#ffffff; background-color:#ff6600;
455 font-weight:bold; font-family:sans-serif; font-size:10px;
455 font-weight:bold; font-family:sans-serif; font-size:10px;
456 text-align:center; text-decoration:none;
456 text-align:center; text-decoration:none;
457 }
457 }
458 div.rss_logo a:hover { background-color:#ee5500; }
458 div.rss_logo a:hover { background-color:#ee5500; }
459 pre { margin: 0; }
459 pre { margin: 0; }
460 span.logtags span {
460 span.logtags span {
461 padding: 0px 4px;
461 padding: 0px 4px;
462 font-size: 10px;
462 font-size: 10px;
463 font-weight: normal;
463 font-weight: normal;
464 border: 1px solid;
464 border: 1px solid;
465 background-color: #ffaaff;
465 background-color: #ffaaff;
466 border-color: #ffccff #ff00ee #ff00ee #ffccff;
466 border-color: #ffccff #ff00ee #ff00ee #ffccff;
467 }
467 }
468 span.logtags span.phasetag {
468 span.logtags span.phasetag {
469 background-color: #dfafff;
469 background-color: #dfafff;
470 border-color: #e2b8ff #ce48ff #ce48ff #e2b8ff;
470 border-color: #e2b8ff #ce48ff #ce48ff #e2b8ff;
471 }
471 }
472 span.logtags span.obsoletetag {
472 span.logtags span.obsoletetag {
473 background-color: #dddddd;
473 background-color: #dddddd;
474 border-color: #e4e4e4 #a3a3a3 #a3a3a3 #e4e4e4;
474 border-color: #e4e4e4 #a3a3a3 #a3a3a3 #e4e4e4;
475 }
475 }
476 span.logtags span.instabilitytag {
476 span.logtags span.instabilitytag {
477 background-color: #ffb1c0;
477 background-color: #ffb1c0;
478 border-color: #ffbbc8 #ff4476 #ff4476 #ffbbc8;
478 border-color: #ffbbc8 #ff4476 #ff4476 #ffbbc8;
479 }
479 }
480 span.logtags span.tagtag {
480 span.logtags span.tagtag {
481 background-color: #ffffaa;
481 background-color: #ffffaa;
482 border-color: #ffffcc #ffee00 #ffee00 #ffffcc;
482 border-color: #ffffcc #ffee00 #ffee00 #ffffcc;
483 }
483 }
484 span.logtags span.branchtag {
484 span.logtags span.branchtag {
485 background-color: #aaffaa;
485 background-color: #aaffaa;
486 border-color: #ccffcc #00cc33 #00cc33 #ccffcc;
486 border-color: #ccffcc #00cc33 #00cc33 #ccffcc;
487 }
487 }
488 span.logtags span.inbranchtag {
488 span.logtags span.inbranchtag {
489 background-color: #d5dde6;
489 background-color: #d5dde6;
490 border-color: #e3ecf4 #9398f4 #9398f4 #e3ecf4;
490 border-color: #e3ecf4 #9398f4 #9398f4 #e3ecf4;
491 }
491 }
492 span.logtags span.bookmarktag {
492 span.logtags span.bookmarktag {
493 background-color: #afdffa;
493 background-color: #afdffa;
494 border-color: #ccecff #46ace6 #46ace6 #ccecff;
494 border-color: #ccecff #46ace6 #46ace6 #ccecff;
495 }
495 }
496 span.difflineplus { color:#008800; }
496 span.difflineplus { color:#008800; }
497 span.difflineminus { color:#cc0000; }
497 span.difflineminus { color:#cc0000; }
498 span.difflineat { color:#990099; }
498 span.difflineat { color:#990099; }
499 div.diffblocks { counter-reset: lineno; }
499 div.diffblocks { counter-reset: lineno; }
500 div.diffblock { counter-increment: lineno; }
500 div.diffblock { counter-increment: lineno; }
501 pre.sourcelines { position: relative; counter-reset: lineno; }
501 pre.sourcelines { position: relative; counter-reset: lineno; }
502 pre.sourcelines > span {
502 pre.sourcelines > span {
503 display: inline-block;
503 display: inline-block;
504 box-sizing: border-box;
504 box-sizing: border-box;
505 width: 100%;
505 width: 100%;
506 padding: 0 0 0 5em;
506 padding: 0 0 0 5em;
507 counter-increment: lineno;
507 counter-increment: lineno;
508 vertical-align: top;
508 vertical-align: top;
509 }
509 }
510 pre.sourcelines > span:before {
510 pre.sourcelines > span:before {
511 -moz-user-select: -moz-none;
511 -moz-user-select: -moz-none;
512 -khtml-user-select: none;
512 -khtml-user-select: none;
513 -webkit-user-select: none;
513 -webkit-user-select: none;
514 -ms-user-select: none;
514 -ms-user-select: none;
515 user-select: none;
515 user-select: none;
516 display: inline-block;
516 display: inline-block;
517 margin-left: -6em;
517 margin-left: -6em;
518 width: 4em;
518 width: 4em;
519 color: #999;
519 color: #999;
520 text-align: right;
520 text-align: right;
521 content: counters(lineno,".");
521 content: counters(lineno,".");
522 float: left;
522 float: left;
523 }
523 }
524 pre.sourcelines > a {
524 pre.sourcelines > a {
525 display: inline-block;
525 display: inline-block;
526 position: absolute;
526 position: absolute;
527 left: 0px;
527 left: 0px;
528 width: 4em;
528 width: 4em;
529 height: 1em;
529 height: 1em;
530 }
530 }
531 tr:target td,
531 tr:target td,
532 pre.sourcelines > span:target,
532 pre.sourcelines > span:target,
533 pre.sourcelines.stripes > span:target {
533 pre.sourcelines.stripes > span:target {
534 background-color: #bfdfff;
534 background-color: #bfdfff;
535 }
535 }
536
536
537 .description {
537 .description {
538 font-family: monospace;
538 font-family: monospace;
539 white-space: pre;
539 white-space: pre;
540 }
540 }
541
541
542 /* Followlines */
542 /* Followlines */
543 tbody.sourcelines > tr.followlines-selected,
543 tbody.sourcelines > tr.followlines-selected,
544 pre.sourcelines > span.followlines-selected {
544 pre.sourcelines > span.followlines-selected {
545 background-color: #99C7E9 !important;
545 background-color: #99C7E9 !important;
546 }
546 }
547
547
548 div#followlines {
548 div#followlines {
549 background-color: #FFF;
549 background-color: #FFF;
550 border: 1px solid #d9d8d1;
550 border: 1px solid #d9d8d1;
551 padding: 5px;
551 padding: 5px;
552 position: fixed;
552 position: fixed;
553 }
553 }
554
554
555 div.followlines-cancel {
555 div.followlines-cancel {
556 text-align: right;
556 text-align: right;
557 }
557 }
558
558
559 div.followlines-cancel > button {
559 div.followlines-cancel > button {
560 line-height: 80%;
560 line-height: 80%;
561 padding: 0;
561 padding: 0;
562 border: 0;
562 border: 0;
563 border-radius: 2px;
563 border-radius: 2px;
564 background-color: inherit;
564 background-color: inherit;
565 font-weight: bold;
565 font-weight: bold;
566 }
566 }
567
567
568 div.followlines-cancel > button:hover {
568 div.followlines-cancel > button:hover {
569 color: #FFFFFF;
569 color: #FFFFFF;
570 background-color: #CF1F1F;
570 background-color: #CF1F1F;
571 }
571 }
572
572
573 div.followlines-link {
573 div.followlines-link {
574 margin: 2px;
574 margin: 2px;
575 margin-top: 4px;
575 margin-top: 4px;
576 font-family: sans-serif;
576 font-family: sans-serif;
577 }
577 }
578
578
579 .btn-followlines {
579 .btn-followlines {
580 position: absolute;
580 position: absolute;
581 display: none;
581 display: none;
582 cursor: pointer;
582 cursor: pointer;
583 box-sizing: content-box;
583 box-sizing: content-box;
584 font-size: 11px;
584 font-size: 11px;
585 width: 13px;
585 width: 13px;
586 height: 13px;
586 height: 13px;
587 border-radius: 3px;
587 border-radius: 3px;
588 margin: 0px;
588 margin: 0px;
589 margin-top: -2px;
589 margin-top: -2px;
590 padding: 0px;
590 padding: 0px;
591 background-color: #E5FDE5;
591 background-color: #E5FDE5;
592 border: 1px solid #9BC19B;
592 border: 1px solid #9BC19B;
593 font-family: monospace;
593 font-family: monospace;
594 text-align: center;
594 text-align: center;
595 line-height: 5px;
595 line-height: 5px;
596 }
596 }
597
597
598 span.followlines-select .btn-followlines {
598 span.followlines-select .btn-followlines {
599 margin-left: -1.6em;
599 margin-left: -1.6em;
600 }
600 }
601
601
602 .btn-followlines:hover {
602 .btn-followlines:hover {
603 transform: scale(1.1, 1.1);
603 transform: scale(1.1, 1.1);
604 }
604 }
605
605
606 .btn-followlines .followlines-plus {
606 .btn-followlines .followlines-plus {
607 color: green;
607 color: green;
608 }
608 }
609
609
610 .btn-followlines .followlines-minus {
610 .btn-followlines .followlines-minus {
611 color: red;
611 color: red;
612 }
612 }
613
613
614 .btn-followlines-end {
614 .btn-followlines-end {
615 background-color: #ffdcdc;
615 background-color: #ffdcdc;
616 }
616 }
617
617
618 .sourcelines tr:hover .btn-followlines,
618 .sourcelines tr:hover .btn-followlines,
619 .sourcelines span.followlines-select:hover > .btn-followlines {
619 .sourcelines span.followlines-select:hover > .btn-followlines {
620 display: inline;
620 display: inline;
621 }
621 }
622
622
623 .btn-followlines-hidden,
623 .btn-followlines-hidden,
624 .sourcelines tr:hover .btn-followlines-hidden {
624 .sourcelines tr:hover .btn-followlines-hidden {
625 display: none;
625 display: none;
626 }
626 }
627
627
628 /* Graph */
628 /* Graph */
629 div#wrapper {
629 div#wrapper {
630 position: relative;
630 position: relative;
631 margin: 0;
631 margin: 0;
632 padding: 0;
632 padding: 0;
633 margin-top: 3px;
633 margin-top: 3px;
634 }
634 }
635
635
636 canvas {
636 canvas {
637 position: absolute;
637 position: absolute;
638 z-index: 5;
638 z-index: 5;
639 top: -0.9em;
639 top: -0.9em;
640 margin: 0;
640 margin: 0;
641 }
641 }
642
642
643 ul#graphnodes {
643 ul#graphnodes {
644 list-style: none inside none;
644 list-style: none inside none;
645 padding: 0;
645 padding: 0;
646 margin: 0;
646 margin: 0;
647 }
647 }
648
648
649 ul#graphnodes li {
649 ul#graphnodes li {
650 position: relative;
650 position: relative;
651 height: 37px;
651 height: 37px;
652 overflow: visible;
652 overflow: visible;
653 padding-top: 2px;
653 padding-top: 2px;
654 }
654 }
655
655
656 ul#graphnodes li .fg {
656 ul#graphnodes li .fg {
657 position: absolute;
657 position: absolute;
658 z-index: 10;
658 z-index: 10;
659 }
659 }
660
660
661 ul#graphnodes li .info {
661 ul#graphnodes li .info {
662 font-size: 100%;
662 font-size: 100%;
663 font-style: italic;
663 font-style: italic;
664 }
664 }
665
665
666 /* Comparison */
666 /* Comparison */
667 .legend {
667 .legend {
668 padding: 1.5% 0 1.5% 0;
668 padding: 1.5% 0 1.5% 0;
669 }
669 }
670
670
671 .legendinfo {
671 .legendinfo {
672 border: 1px solid #d9d8d1;
672 border: 1px solid #d9d8d1;
673 font-size: 80%;
673 font-size: 80%;
674 text-align: center;
674 text-align: center;
675 padding: 0.5%;
675 padding: 0.5%;
676 }
676 }
677
677
678 .equal {
678 .equal {
679 background-color: #ffffff;
679 background-color: #ffffff;
680 }
680 }
681
681
682 .delete {
682 .delete {
683 background-color: #faa;
683 background-color: #faa;
684 color: #333;
684 color: #333;
685 }
685 }
686
686
687 .insert {
687 .insert {
688 background-color: #ffa;
688 background-color: #ffa;
689 }
689 }
690
690
691 .replace {
691 .replace {
692 background-color: #e8e8e8;
692 background-color: #e8e8e8;
693 }
693 }
694
694
695 .comparison {
695 .comparison {
696 overflow-x: auto;
696 overflow-x: auto;
697 }
697 }
698
698
699 .header th {
699 .header th {
700 text-align: center;
700 text-align: center;
701 }
701 }
702
702
703 .block {
703 .block {
704 border-top: 1px solid #d9d8d1;
704 border-top: 1px solid #d9d8d1;
705 }
705 }
706
706
707 .scroll-loading {
707 .scroll-loading {
708 -webkit-animation: change_color 1s linear 0s infinite alternate;
708 -webkit-animation: change_color 1s linear 0s infinite alternate;
709 -moz-animation: change_color 1s linear 0s infinite alternate;
709 -moz-animation: change_color 1s linear 0s infinite alternate;
710 -o-animation: change_color 1s linear 0s infinite alternate;
710 -o-animation: change_color 1s linear 0s infinite alternate;
711 animation: change_color 1s linear 0s infinite alternate;
711 animation: change_color 1s linear 0s infinite alternate;
712 }
712 }
713
713
714 @-webkit-keyframes change_color {
714 @-webkit-keyframes change_color {
715 from { background-color: #A0CEFF; } to { }
715 from { background-color: #A0CEFF; } to { }
716 }
716 }
717 @-moz-keyframes change_color {
717 @-moz-keyframes change_color {
718 from { background-color: #A0CEFF; } to { }
718 from { background-color: #A0CEFF; } to { }
719 }
719 }
720 @-o-keyframes change_color {
720 @-o-keyframes change_color {
721 from { background-color: #A0CEFF; } to { }
721 from { background-color: #A0CEFF; } to { }
722 }
722 }
723 @keyframes change_color {
723 @keyframes change_color {
724 from { background-color: #A0CEFF; } to { }
724 from { background-color: #A0CEFF; } to { }
725 }
725 }
726
726
727 .scroll-loading-error {
727 .scroll-loading-error {
728 background-color: #FFCCCC !important;
728 background-color: #FFCCCC !important;
729 }
729 }
730
730
731 #doc {
731 #doc {
732 margin: 0 8px;
732 margin: 0 8px;
733 }
733 }
734 304 Not Modified
734 304 Not Modified
735
735
736
736
737 phase changes are refreshed (issue4061)
737 phase changes are refreshed (issue4061)
738
738
739 $ echo bar >> foo
739 $ echo bar >> foo
740 $ hg ci -msecret --secret
740 $ hg ci -msecret --secret
741 $ get-with-headers.py localhost:$HGPORT 'log?style=raw'
741 $ get-with-headers.py localhost:$HGPORT 'log?style=raw'
742 200 Script output follows
742 200 Script output follows
743
743
744
744
745 # HG changelog
745 # HG changelog
746 # Node ID 2ef0ac749a14e4f57a5a822464a0902c6f7f448f
746 # Node ID 2ef0ac749a14e4f57a5a822464a0902c6f7f448f
747
747
748 changeset: 2ef0ac749a14e4f57a5a822464a0902c6f7f448f
748 changeset: 2ef0ac749a14e4f57a5a822464a0902c6f7f448f
749 revision: 0
749 revision: 0
750 user: test
750 user: test
751 date: Thu, 01 Jan 1970 00:00:00 +0000
751 date: Thu, 01 Jan 1970 00:00:00 +0000
752 summary: base
752 summary: base
753 branch: default
753 branch: default
754 tag: tip
754 tag: tip
755 bookmark: @
755 bookmark: @
756 bookmark: a b c
756 bookmark: a b c
757 bookmark: d/e/f
757 bookmark: d/e/f
758
758
759
759
760 $ hg phase --draft tip
760 $ hg phase --draft tip
761 $ get-with-headers.py localhost:$HGPORT 'log?style=raw'
761 $ get-with-headers.py localhost:$HGPORT 'log?style=raw'
762 200 Script output follows
762 200 Script output follows
763
763
764
764
765 # HG changelog
765 # HG changelog
766 # Node ID a084749e708a9c4c0a5b652a2a446322ce290e04
766 # Node ID a084749e708a9c4c0a5b652a2a446322ce290e04
767
767
768 changeset: a084749e708a9c4c0a5b652a2a446322ce290e04
768 changeset: a084749e708a9c4c0a5b652a2a446322ce290e04
769 revision: 1
769 revision: 1
770 user: test
770 user: test
771 date: Thu, 01 Jan 1970 00:00:00 +0000
771 date: Thu, 01 Jan 1970 00:00:00 +0000
772 summary: secret
772 summary: secret
773 branch: default
773 branch: default
774 tag: tip
774 tag: tip
775
775
776 changeset: 2ef0ac749a14e4f57a5a822464a0902c6f7f448f
776 changeset: 2ef0ac749a14e4f57a5a822464a0902c6f7f448f
777 revision: 0
777 revision: 0
778 user: test
778 user: test
779 date: Thu, 01 Jan 1970 00:00:00 +0000
779 date: Thu, 01 Jan 1970 00:00:00 +0000
780 summary: base
780 summary: base
781 bookmark: @
781 bookmark: @
782 bookmark: a b c
782 bookmark: a b c
783 bookmark: d/e/f
783 bookmark: d/e/f
784
784
785
785
786
786
787 access bookmarks
787 access bookmarks
788
788
789 $ get-with-headers.py localhost:$HGPORT 'rev/@?style=paper' | egrep '^200|changeset 0:'
789 $ get-with-headers.py localhost:$HGPORT 'rev/@?style=paper' | egrep '^200|changeset 0:'
790 200 Script output follows
790 200 Script output follows
791 changeset 0:<a href="/rev/2ef0ac749a14?style=paper">2ef0ac749a14</a>
791 changeset 0:<a href="/rev/2ef0ac749a14?style=paper">2ef0ac749a14</a>
792
792
793 $ get-with-headers.py localhost:$HGPORT 'rev/%40?style=paper' | egrep '^200|changeset 0:'
793 $ get-with-headers.py localhost:$HGPORT 'rev/%40?style=paper' | egrep '^200|changeset 0:'
794 200 Script output follows
794 200 Script output follows
795 changeset 0:<a href="/rev/2ef0ac749a14?style=paper">2ef0ac749a14</a>
795 changeset 0:<a href="/rev/2ef0ac749a14?style=paper">2ef0ac749a14</a>
796
796
797 $ get-with-headers.py localhost:$HGPORT 'rev/a%20b%20c?style=paper' | egrep '^200|changeset 0:'
797 $ get-with-headers.py localhost:$HGPORT 'rev/a%20b%20c?style=paper' | egrep '^200|changeset 0:'
798 200 Script output follows
798 200 Script output follows
799 changeset 0:<a href="/rev/2ef0ac749a14?style=paper">2ef0ac749a14</a>
799 changeset 0:<a href="/rev/2ef0ac749a14?style=paper">2ef0ac749a14</a>
800
800
801 $ get-with-headers.py localhost:$HGPORT 'rev/d%252Fe%252Ff?style=paper' | egrep '^200|changeset 0:'
801 $ get-with-headers.py localhost:$HGPORT 'rev/d%252Fe%252Ff?style=paper' | egrep '^200|changeset 0:'
802 200 Script output follows
802 200 Script output follows
803 changeset 0:<a href="/rev/2ef0ac749a14?style=paper">2ef0ac749a14</a>
803 changeset 0:<a href="/rev/2ef0ac749a14?style=paper">2ef0ac749a14</a>
804
804
805 no style can be loaded from directories other than the specified paths
805 no style can be loaded from directories other than the specified paths
806
806
807 $ mkdir -p x/templates/fallback
807 $ mkdir -p x/templates/fallback
808 $ cat <<EOF > x/templates/fallback/map
808 $ cat <<EOF > x/templates/fallback/map
809 > default = 'shortlog'
809 > default = 'shortlog'
810 > shortlog = 'fall back to default\n'
810 > shortlog = 'fall back to default\n'
811 > mimetype = 'text/plain'
811 > mimetype = 'text/plain'
812 > EOF
812 > EOF
813 $ cat <<EOF > x/map
813 $ cat <<EOF > x/map
814 > default = 'shortlog'
814 > default = 'shortlog'
815 > shortlog = 'access to outside of templates directory\n'
815 > shortlog = 'access to outside of templates directory\n'
816 > mimetype = 'text/plain'
816 > mimetype = 'text/plain'
817 > EOF
817 > EOF
818
818
819 $ killdaemons.py
819 $ killdaemons.py
820 $ hg serve -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log \
820 $ hg serve -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log \
821 > --config web.style=fallback --config web.templates=x/templates
821 > --config web.style=fallback --config web.templates=x/templates
822 $ cat hg.pid >> $DAEMON_PIDS
822 $ cat hg.pid >> $DAEMON_PIDS
823
823
824 $ get-with-headers.py localhost:$HGPORT "?style=`pwd`/x"
824 $ get-with-headers.py localhost:$HGPORT "?style=`pwd`/x"
825 200 Script output follows
825 200 Script output follows
826
826
827 fall back to default
827 fall back to default
828
828
829 $ get-with-headers.py localhost:$HGPORT '?style=..'
829 $ get-with-headers.py localhost:$HGPORT '?style=..'
830 200 Script output follows
830 200 Script output follows
831
831
832 fall back to default
832 fall back to default
833
833
834 $ get-with-headers.py localhost:$HGPORT '?style=./..'
834 $ get-with-headers.py localhost:$HGPORT '?style=./..'
835 200 Script output follows
835 200 Script output follows
836
836
837 fall back to default
837 fall back to default
838
838
839 $ get-with-headers.py localhost:$HGPORT '?style=.../.../'
839 $ get-with-headers.py localhost:$HGPORT '?style=.../.../'
840 200 Script output follows
840 200 Script output follows
841
841
842 fall back to default
842 fall back to default
843
843
844 $ killdaemons.py
845
846 Test signal-safe-lock in web and non-web processes
847
848 $ cat <<'EOF' > disablesig.py
849 > import signal
850 > from mercurial import error, extensions
851 > def disabledsig(orig, signalnum, handler):
852 > if signalnum == signal.SIGTERM:
853 > raise error.Abort(b'SIGTERM cannot be replaced')
854 > try:
855 > return orig(signalnum, handler)
856 > except ValueError:
857 > raise error.Abort(b'signal.signal() called in thread?')
858 > def uisetup(ui):
859 > extensions.wrapfunction(signal, b'signal', disabledsig)
860 > EOF
861
862 by default, signal interrupt should be disabled while making a lock file
863
864 $ hg debuglock -s --config extensions.disablesig=disablesig.py
865 abort: SIGTERM cannot be replaced
866 [255]
867
868 but in hgweb, it isn't disabled since some WSGI servers complains about
869 unsupported signal.signal() calls (see issue5889)
870
871 $ hg serve --config extensions.disablesig=disablesig.py \
872 > --config web.allow-push='*' --config web.push_ssl=False \
873 > -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
874 $ cat hg.pid >> $DAEMON_PIDS
875
876 $ hg clone -q http://localhost:$HGPORT/ repo
877 $ hg bookmark -R repo foo
878
879 push would fail if signal.signal() were called
880
881 $ hg push -R repo -B foo
882 pushing to http://localhost:$HGPORT/
883 searching for changes
884 no changes found
885 exporting bookmark foo
886 [1]
887
888 $ rm -R repo
889 $ killdaemons.py
890
844 errors
891 errors
845
892
846 $ cat errors.log
893 $ cat errors.log
847
894
848 Uncaught exceptions result in a logged error and canned HTTP response
895 Uncaught exceptions result in a logged error and canned HTTP response
849
896
850 $ killdaemons.py
851 $ hg serve --config extensions.hgweberror=$TESTDIR/hgweberror.py -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
897 $ hg serve --config extensions.hgweberror=$TESTDIR/hgweberror.py -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
852 $ cat hg.pid >> $DAEMON_PIDS
898 $ cat hg.pid >> $DAEMON_PIDS
853
899
854 $ get-with-headers.py localhost:$HGPORT 'raiseerror' transfer-encoding content-type
900 $ get-with-headers.py localhost:$HGPORT 'raiseerror' transfer-encoding content-type
855 500 Internal Server Error
901 500 Internal Server Error
856 transfer-encoding: chunked
902 transfer-encoding: chunked
857
903
858 Internal Server Error (no-eol)
904 Internal Server Error (no-eol)
859 [1]
905 [1]
860
906
861 $ killdaemons.py
907 $ killdaemons.py
862 $ head -1 errors.log
908 $ head -1 errors.log
863 .* Exception happened during processing request '/raiseerror': (re)
909 .* Exception happened during processing request '/raiseerror': (re)
864
910
865 Uncaught exception after partial content sent
911 Uncaught exception after partial content sent
866
912
867 $ hg serve --config extensions.hgweberror=$TESTDIR/hgweberror.py -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
913 $ hg serve --config extensions.hgweberror=$TESTDIR/hgweberror.py -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
868 $ cat hg.pid >> $DAEMON_PIDS
914 $ cat hg.pid >> $DAEMON_PIDS
869 $ get-with-headers.py localhost:$HGPORT 'raiseerror?partialresponse=1' transfer-encoding content-type
915 $ get-with-headers.py localhost:$HGPORT 'raiseerror?partialresponse=1' transfer-encoding content-type
870 200 Script output follows
916 200 Script output follows
871 transfer-encoding: chunked
917 transfer-encoding: chunked
872 content-type: text/plain
918 content-type: text/plain
873
919
874 partial content
920 partial content
875 Internal Server Error (no-eol)
921 Internal Server Error (no-eol)
876
922
877 $ killdaemons.py
923 $ killdaemons.py
878
924
879 HTTP 304 works with hgwebdir (issue5844)
925 HTTP 304 works with hgwebdir (issue5844)
880
926
881 $ cat > hgweb.conf << EOF
927 $ cat > hgweb.conf << EOF
882 > [paths]
928 > [paths]
883 > /repo = $TESTTMP/test
929 > /repo = $TESTTMP/test
884 > EOF
930 > EOF
885
931
886 $ hg serve --web-conf hgweb.conf -p $HGPORT -d --pid-file hg.pid -E error.log
932 $ hg serve --web-conf hgweb.conf -p $HGPORT -d --pid-file hg.pid -E error.log
887 $ cat hg.pid >> $DAEMON_PIDS
933 $ cat hg.pid >> $DAEMON_PIDS
888
934
889 $ get-with-headers.py --twice --headeronly localhost:$HGPORT 'repo/static/style.css' - date etag server
935 $ get-with-headers.py --twice --headeronly localhost:$HGPORT 'repo/static/style.css' - date etag server
890 200 Script output follows
936 200 Script output follows
891 content-length: 2677
937 content-length: 2677
892 content-type: text/css
938 content-type: text/css
893 304 Not Modified
939 304 Not Modified
894
940
895 $ killdaemons.py
941 $ killdaemons.py
896
942
897 $ cd ..
943 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now