##// END OF EJS Templates
hgweb: discard Content-Type header for 304 responses (issue5844)...
Gregory Szorc -
r37846:11ee9bf2 stable
parent child Browse files
Show More
@@ -1,458 +1,464 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 # displaying bundling progress bar while serving feel wrong and may
228 # displaying bundling progress bar while serving feel wrong and may
229 # break some wsgi implementation.
229 # break some wsgi implementation.
230 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
230 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
231 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
231 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
232 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
232 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
233 self._lastrepo = self._repos[0]
233 self._lastrepo = self._repos[0]
234 hook.redirect(True)
234 hook.redirect(True)
235 self.reponame = name
235 self.reponame = name
236
236
237 def _webifyrepo(self, repo):
237 def _webifyrepo(self, repo):
238 repo = getwebview(repo)
238 repo = getwebview(repo)
239 self.websubtable = webutil.getwebsubs(repo)
239 self.websubtable = webutil.getwebsubs(repo)
240 return repo
240 return repo
241
241
242 @contextlib.contextmanager
242 @contextlib.contextmanager
243 def _obtainrepo(self):
243 def _obtainrepo(self):
244 """Obtain a repo unique to the caller.
244 """Obtain a repo unique to the caller.
245
245
246 Internally we maintain a stack of cachedlocalrepo instances
246 Internally we maintain a stack of cachedlocalrepo instances
247 to be handed out. If one is available, we pop it and return it,
247 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,
248 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.
249 we clone the most recently used repo instance and return it.
250
250
251 It is currently possible for the stack to grow without bounds
251 It is currently possible for the stack to grow without bounds
252 if the server allows infinite threads. However, servers should
252 if the server allows infinite threads. However, servers should
253 have a thread limit, thus establishing our limit.
253 have a thread limit, thus establishing our limit.
254 """
254 """
255 if self._repos:
255 if self._repos:
256 cached = self._repos.pop()
256 cached = self._repos.pop()
257 r, created = cached.fetch()
257 r, created = cached.fetch()
258 else:
258 else:
259 cached = self._lastrepo.copy()
259 cached = self._lastrepo.copy()
260 r, created = cached.fetch()
260 r, created = cached.fetch()
261 if created:
261 if created:
262 r = self._webifyrepo(r)
262 r = self._webifyrepo(r)
263
263
264 self._lastrepo = cached
264 self._lastrepo = cached
265 self.mtime = cached.mtime
265 self.mtime = cached.mtime
266 try:
266 try:
267 yield r
267 yield r
268 finally:
268 finally:
269 self._repos.append(cached)
269 self._repos.append(cached)
270
270
271 def run(self):
271 def run(self):
272 """Start a server from CGI environment.
272 """Start a server from CGI environment.
273
273
274 Modern servers should be using WSGI and should avoid this
274 Modern servers should be using WSGI and should avoid this
275 method, if possible.
275 method, if possible.
276 """
276 """
277 if not encoding.environ.get('GATEWAY_INTERFACE',
277 if not encoding.environ.get('GATEWAY_INTERFACE',
278 '').startswith("CGI/1."):
278 '').startswith("CGI/1."):
279 raise RuntimeError("This function is only intended to be "
279 raise RuntimeError("This function is only intended to be "
280 "called while running as a CGI script.")
280 "called while running as a CGI script.")
281 wsgicgi.launch(self)
281 wsgicgi.launch(self)
282
282
283 def __call__(self, env, respond):
283 def __call__(self, env, respond):
284 """Run the WSGI application.
284 """Run the WSGI application.
285
285
286 This may be called by multiple threads.
286 This may be called by multiple threads.
287 """
287 """
288 req = requestmod.parserequestfromenv(env)
288 req = requestmod.parserequestfromenv(env)
289 res = requestmod.wsgiresponse(req, respond)
289 res = requestmod.wsgiresponse(req, respond)
290
290
291 return self.run_wsgi(req, res)
291 return self.run_wsgi(req, res)
292
292
293 def run_wsgi(self, req, res):
293 def run_wsgi(self, req, res):
294 """Internal method to run the WSGI application.
294 """Internal method to run the WSGI application.
295
295
296 This is typically only called by Mercurial. External consumers
296 This is typically only called by Mercurial. External consumers
297 should be using instances of this class as the WSGI application.
297 should be using instances of this class as the WSGI application.
298 """
298 """
299 with self._obtainrepo() as repo:
299 with self._obtainrepo() as repo:
300 profile = repo.ui.configbool('profiling', 'enabled')
300 profile = repo.ui.configbool('profiling', 'enabled')
301 with profiling.profile(repo.ui, enabled=profile):
301 with profiling.profile(repo.ui, enabled=profile):
302 for r in self._runwsgi(req, res, repo):
302 for r in self._runwsgi(req, res, repo):
303 yield r
303 yield r
304
304
305 def _runwsgi(self, req, res, repo):
305 def _runwsgi(self, req, res, repo):
306 rctx = requestcontext(self, repo, req, res)
306 rctx = requestcontext(self, repo, req, res)
307
307
308 # This state is global across all threads.
308 # This state is global across all threads.
309 encoding.encoding = rctx.config('web', 'encoding')
309 encoding.encoding = rctx.config('web', 'encoding')
310 rctx.repo.ui.environ = req.rawenv
310 rctx.repo.ui.environ = req.rawenv
311
311
312 if rctx.csp:
312 if rctx.csp:
313 # hgwebdir may have added CSP header. Since we generate our own,
313 # hgwebdir may have added CSP header. Since we generate our own,
314 # replace it.
314 # replace it.
315 res.headers['Content-Security-Policy'] = rctx.csp
315 res.headers['Content-Security-Policy'] = rctx.csp
316
316
317 # /api/* is reserved for various API implementations. Dispatch
317 # /api/* is reserved for various API implementations. Dispatch
318 # accordingly. But URL paths can conflict with subrepos and virtual
318 # accordingly. But URL paths can conflict with subrepos and virtual
319 # repos in hgwebdir. So until we have a workaround for this, only
319 # repos in hgwebdir. So until we have a workaround for this, only
320 # expose the URLs if the feature is enabled.
320 # expose the URLs if the feature is enabled.
321 apienabled = rctx.repo.ui.configbool('experimental', 'web.apiserver')
321 apienabled = rctx.repo.ui.configbool('experimental', 'web.apiserver')
322 if apienabled and req.dispatchparts and req.dispatchparts[0] == b'api':
322 if apienabled and req.dispatchparts and req.dispatchparts[0] == b'api':
323 wireprotoserver.handlewsgiapirequest(rctx, req, res,
323 wireprotoserver.handlewsgiapirequest(rctx, req, res,
324 self.check_perm)
324 self.check_perm)
325 return res.sendresponse()
325 return res.sendresponse()
326
326
327 handled = wireprotoserver.handlewsgirequest(
327 handled = wireprotoserver.handlewsgirequest(
328 rctx, req, res, self.check_perm)
328 rctx, req, res, self.check_perm)
329 if handled:
329 if handled:
330 return res.sendresponse()
330 return res.sendresponse()
331
331
332 # Old implementations of hgweb supported dispatching the request via
332 # Old implementations of hgweb supported dispatching the request via
333 # the initial query string parameter instead of using PATH_INFO.
333 # the initial query string parameter instead of using PATH_INFO.
334 # If PATH_INFO is present (signaled by ``req.dispatchpath`` having
334 # If PATH_INFO is present (signaled by ``req.dispatchpath`` having
335 # a value), we use it. Otherwise fall back to the query string.
335 # a value), we use it. Otherwise fall back to the query string.
336 if req.dispatchpath is not None:
336 if req.dispatchpath is not None:
337 query = req.dispatchpath
337 query = req.dispatchpath
338 else:
338 else:
339 query = req.querystring.partition('&')[0].partition(';')[0]
339 query = req.querystring.partition('&')[0].partition(';')[0]
340
340
341 # translate user-visible url structure to internal structure
341 # translate user-visible url structure to internal structure
342
342
343 args = query.split('/', 2)
343 args = query.split('/', 2)
344 if 'cmd' not in req.qsparams and args and args[0]:
344 if 'cmd' not in req.qsparams and args and args[0]:
345 cmd = args.pop(0)
345 cmd = args.pop(0)
346 style = cmd.rfind('-')
346 style = cmd.rfind('-')
347 if style != -1:
347 if style != -1:
348 req.qsparams['style'] = cmd[:style]
348 req.qsparams['style'] = cmd[:style]
349 cmd = cmd[style + 1:]
349 cmd = cmd[style + 1:]
350
350
351 # avoid accepting e.g. style parameter as command
351 # avoid accepting e.g. style parameter as command
352 if util.safehasattr(webcommands, cmd):
352 if util.safehasattr(webcommands, cmd):
353 req.qsparams['cmd'] = cmd
353 req.qsparams['cmd'] = cmd
354
354
355 if cmd == 'static':
355 if cmd == 'static':
356 req.qsparams['file'] = '/'.join(args)
356 req.qsparams['file'] = '/'.join(args)
357 else:
357 else:
358 if args and args[0]:
358 if args and args[0]:
359 node = args.pop(0).replace('%2F', '/')
359 node = args.pop(0).replace('%2F', '/')
360 req.qsparams['node'] = node
360 req.qsparams['node'] = node
361 if args:
361 if args:
362 if 'file' in req.qsparams:
362 if 'file' in req.qsparams:
363 del req.qsparams['file']
363 del req.qsparams['file']
364 for a in args:
364 for a in args:
365 req.qsparams.add('file', a)
365 req.qsparams.add('file', a)
366
366
367 ua = req.headers.get('User-Agent', '')
367 ua = req.headers.get('User-Agent', '')
368 if cmd == 'rev' and 'mercurial' in ua:
368 if cmd == 'rev' and 'mercurial' in ua:
369 req.qsparams['style'] = 'raw'
369 req.qsparams['style'] = 'raw'
370
370
371 if cmd == 'archive':
371 if cmd == 'archive':
372 fn = req.qsparams['node']
372 fn = req.qsparams['node']
373 for type_, spec in webutil.archivespecs.iteritems():
373 for type_, spec in webutil.archivespecs.iteritems():
374 ext = spec[2]
374 ext = spec[2]
375 if fn.endswith(ext):
375 if fn.endswith(ext):
376 req.qsparams['node'] = fn[:-len(ext)]
376 req.qsparams['node'] = fn[:-len(ext)]
377 req.qsparams['type'] = type_
377 req.qsparams['type'] = type_
378 else:
378 else:
379 cmd = req.qsparams.get('cmd', '')
379 cmd = req.qsparams.get('cmd', '')
380
380
381 # process the web interface request
381 # process the web interface request
382
382
383 try:
383 try:
384 rctx.tmpl = rctx.templater(req)
384 rctx.tmpl = rctx.templater(req)
385 ctype = rctx.tmpl.render('mimetype',
385 ctype = rctx.tmpl.render('mimetype',
386 {'encoding': encoding.encoding})
386 {'encoding': encoding.encoding})
387
387
388 # check read permissions non-static content
388 # check read permissions non-static content
389 if cmd != 'static':
389 if cmd != 'static':
390 self.check_perm(rctx, req, None)
390 self.check_perm(rctx, req, None)
391
391
392 if cmd == '':
392 if cmd == '':
393 req.qsparams['cmd'] = rctx.tmpl.render('default', {})
393 req.qsparams['cmd'] = rctx.tmpl.render('default', {})
394 cmd = req.qsparams['cmd']
394 cmd = req.qsparams['cmd']
395
395
396 # Don't enable caching if using a CSP nonce because then it wouldn't
396 # Don't enable caching if using a CSP nonce because then it wouldn't
397 # be a nonce.
397 # be a nonce.
398 if rctx.configbool('web', 'cache') and not rctx.nonce:
398 if rctx.configbool('web', 'cache') and not rctx.nonce:
399 tag = 'W/"%d"' % self.mtime
399 tag = 'W/"%d"' % self.mtime
400 if req.headers.get('If-None-Match') == tag:
400 if req.headers.get('If-None-Match') == tag:
401 res.status = '304 Not Modified'
401 res.status = '304 Not Modified'
402 # Content-Type may be defined globally. It isn't valid on a
403 # 304, so discard it.
404 try:
405 del res.headers[b'Content-Type']
406 except KeyError:
407 pass
402 # Response body not allowed on 304.
408 # Response body not allowed on 304.
403 res.setbodybytes('')
409 res.setbodybytes('')
404 return res.sendresponse()
410 return res.sendresponse()
405
411
406 res.headers['ETag'] = tag
412 res.headers['ETag'] = tag
407
413
408 if cmd not in webcommands.__all__:
414 if cmd not in webcommands.__all__:
409 msg = 'no such method: %s' % cmd
415 msg = 'no such method: %s' % cmd
410 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
416 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
411 else:
417 else:
412 # Set some globals appropriate for web handlers. Commands can
418 # Set some globals appropriate for web handlers. Commands can
413 # override easily enough.
419 # override easily enough.
414 res.status = '200 Script output follows'
420 res.status = '200 Script output follows'
415 res.headers['Content-Type'] = ctype
421 res.headers['Content-Type'] = ctype
416 return getattr(webcommands, cmd)(rctx)
422 return getattr(webcommands, cmd)(rctx)
417
423
418 except (error.LookupError, error.RepoLookupError) as err:
424 except (error.LookupError, error.RepoLookupError) as err:
419 msg = pycompat.bytestr(err)
425 msg = pycompat.bytestr(err)
420 if (util.safehasattr(err, 'name') and
426 if (util.safehasattr(err, 'name') and
421 not isinstance(err, error.ManifestLookupError)):
427 not isinstance(err, error.ManifestLookupError)):
422 msg = 'revision not found: %s' % err.name
428 msg = 'revision not found: %s' % err.name
423
429
424 res.status = '404 Not Found'
430 res.status = '404 Not Found'
425 res.headers['Content-Type'] = ctype
431 res.headers['Content-Type'] = ctype
426 return rctx.sendtemplate('error', error=msg)
432 return rctx.sendtemplate('error', error=msg)
427 except (error.RepoError, error.RevlogError) as e:
433 except (error.RepoError, error.RevlogError) as e:
428 res.status = '500 Internal Server Error'
434 res.status = '500 Internal Server Error'
429 res.headers['Content-Type'] = ctype
435 res.headers['Content-Type'] = ctype
430 return rctx.sendtemplate('error', error=pycompat.bytestr(e))
436 return rctx.sendtemplate('error', error=pycompat.bytestr(e))
431 except ErrorResponse as e:
437 except ErrorResponse as e:
432 res.status = statusmessage(e.code, pycompat.bytestr(e))
438 res.status = statusmessage(e.code, pycompat.bytestr(e))
433 res.headers['Content-Type'] = ctype
439 res.headers['Content-Type'] = ctype
434 return rctx.sendtemplate('error', error=pycompat.bytestr(e))
440 return rctx.sendtemplate('error', error=pycompat.bytestr(e))
435
441
436 def check_perm(self, rctx, req, op):
442 def check_perm(self, rctx, req, op):
437 for permhook in permhooks:
443 for permhook in permhooks:
438 permhook(rctx, req, op)
444 permhook(rctx, req, op)
439
445
440 def getwebview(repo):
446 def getwebview(repo):
441 """The 'web.view' config controls changeset filter to hgweb. Possible
447 """The 'web.view' config controls changeset filter to hgweb. Possible
442 values are ``served``, ``visible`` and ``all``. Default is ``served``.
448 values are ``served``, ``visible`` and ``all``. Default is ``served``.
443 The ``served`` filter only shows changesets that can be pulled from the
449 The ``served`` filter only shows changesets that can be pulled from the
444 hgweb instance. The``visible`` filter includes secret changesets but
450 hgweb instance. The``visible`` filter includes secret changesets but
445 still excludes "hidden" one.
451 still excludes "hidden" one.
446
452
447 See the repoview module for details.
453 See the repoview module for details.
448
454
449 The option has been around undocumented since Mercurial 2.5, but no
455 The option has been around undocumented since Mercurial 2.5, but no
450 user ever asked about it. So we better keep it undocumented for now."""
456 user ever asked about it. So we better keep it undocumented for now."""
451 # experimental config: web.view
457 # experimental config: web.view
452 viewconfig = repo.ui.config('web', 'view', untrusted=True)
458 viewconfig = repo.ui.config('web', 'view', untrusted=True)
453 if viewconfig == 'all':
459 if viewconfig == 'all':
454 return repo.unfiltered()
460 return repo.unfiltered()
455 elif viewconfig in repoview.filtertable:
461 elif viewconfig in repoview.filtertable:
456 return repo.filtered(viewconfig)
462 return repo.filtered(viewconfig)
457 else:
463 else:
458 return repo.filtered('served')
464 return repo.filtered('served')
@@ -1,899 +1,897 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 errors
844 errors
845
845
846 $ cat errors.log
846 $ cat errors.log
847
847
848 Uncaught exceptions result in a logged error and canned HTTP response
848 Uncaught exceptions result in a logged error and canned HTTP response
849
849
850 $ killdaemons.py
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
851 $ 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
852 $ cat hg.pid >> $DAEMON_PIDS
853
853
854 $ get-with-headers.py localhost:$HGPORT 'raiseerror' transfer-encoding content-type
854 $ get-with-headers.py localhost:$HGPORT 'raiseerror' transfer-encoding content-type
855 500 Internal Server Error
855 500 Internal Server Error
856 transfer-encoding: chunked
856 transfer-encoding: chunked
857
857
858 Internal Server Error (no-eol)
858 Internal Server Error (no-eol)
859 [1]
859 [1]
860
860
861 $ killdaemons.py
861 $ killdaemons.py
862 $ head -1 errors.log
862 $ head -1 errors.log
863 .* Exception happened during processing request '/raiseerror': (re)
863 .* Exception happened during processing request '/raiseerror': (re)
864
864
865 Uncaught exception after partial content sent
865 Uncaught exception after partial content sent
866
866
867 $ hg serve --config extensions.hgweberror=$TESTDIR/hgweberror.py -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
867 $ 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
868 $ cat hg.pid >> $DAEMON_PIDS
869 $ get-with-headers.py localhost:$HGPORT 'raiseerror?partialresponse=1' transfer-encoding content-type
869 $ get-with-headers.py localhost:$HGPORT 'raiseerror?partialresponse=1' transfer-encoding content-type
870 200 Script output follows
870 200 Script output follows
871 transfer-encoding: chunked
871 transfer-encoding: chunked
872 content-type: text/plain
872 content-type: text/plain
873
873
874 partial content
874 partial content
875 Internal Server Error (no-eol)
875 Internal Server Error (no-eol)
876
876
877 $ killdaemons.py
877 $ killdaemons.py
878
878
879 HTTP 304 works with hgwebdir (issue5844)
879 HTTP 304 works with hgwebdir (issue5844)
880
880
881 $ cat > hgweb.conf << EOF
881 $ cat > hgweb.conf << EOF
882 > [paths]
882 > [paths]
883 > /repo = $TESTTMP/test
883 > /repo = $TESTTMP/test
884 > EOF
884 > EOF
885
885
886 $ hg serve --web-conf hgweb.conf -p $HGPORT -d --pid-file hg.pid -E error.log
886 $ hg serve --web-conf hgweb.conf -p $HGPORT -d --pid-file hg.pid -E error.log
887 $ cat hg.pid >> $DAEMON_PIDS
887 $ cat hg.pid >> $DAEMON_PIDS
888
888
889 $ get-with-headers.py --twice --headeronly localhost:$HGPORT 'repo/static/style.css' - date etag server
889 $ get-with-headers.py --twice --headeronly localhost:$HGPORT 'repo/static/style.css' - date etag server
890 200 Script output follows
890 200 Script output follows
891 content-length: 2677
891 content-length: 2677
892 content-type: text/css
892 content-type: text/css
893 500 Internal Server Error
893 304 Not Modified
894 transfer-encoding: chunked
895 [1]
896
894
897 $ killdaemons.py
895 $ killdaemons.py
898
896
899 $ cd ..
897 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now