##// END OF EJS Templates
hgweb: refresh hgweb.repo on phase change (issue4061)...
Anton Shestakov -
r22577:a111e460 stable
parent child Browse files
Show More
@@ -1,192 +1,192 b''
1 # hgweb/common.py - Utility functions needed by hgweb_mod and hgwebdir_mod
1 # hgweb/common.py - Utility functions needed by hgweb_mod and hgwebdir_mod
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, 2006 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005, 2006 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 import errno, mimetypes, os
9 import errno, mimetypes, os
10
10
11 HTTP_OK = 200
11 HTTP_OK = 200
12 HTTP_NOT_MODIFIED = 304
12 HTTP_NOT_MODIFIED = 304
13 HTTP_BAD_REQUEST = 400
13 HTTP_BAD_REQUEST = 400
14 HTTP_UNAUTHORIZED = 401
14 HTTP_UNAUTHORIZED = 401
15 HTTP_FORBIDDEN = 403
15 HTTP_FORBIDDEN = 403
16 HTTP_NOT_FOUND = 404
16 HTTP_NOT_FOUND = 404
17 HTTP_METHOD_NOT_ALLOWED = 405
17 HTTP_METHOD_NOT_ALLOWED = 405
18 HTTP_SERVER_ERROR = 500
18 HTTP_SERVER_ERROR = 500
19
19
20
20
21 def ismember(ui, username, userlist):
21 def ismember(ui, username, userlist):
22 """Check if username is a member of userlist.
22 """Check if username is a member of userlist.
23
23
24 If userlist has a single '*' member, all users are considered members.
24 If userlist has a single '*' member, all users are considered members.
25 Can be overridden by extensions to provide more complex authorization
25 Can be overridden by extensions to provide more complex authorization
26 schemes.
26 schemes.
27 """
27 """
28 return userlist == ['*'] or username in userlist
28 return userlist == ['*'] or username in userlist
29
29
30 def checkauthz(hgweb, req, op):
30 def checkauthz(hgweb, req, op):
31 '''Check permission for operation based on request data (including
31 '''Check permission for operation based on request data (including
32 authentication info). Return if op allowed, else raise an ErrorResponse
32 authentication info). Return if op allowed, else raise an ErrorResponse
33 exception.'''
33 exception.'''
34
34
35 user = req.env.get('REMOTE_USER')
35 user = req.env.get('REMOTE_USER')
36
36
37 deny_read = hgweb.configlist('web', 'deny_read')
37 deny_read = hgweb.configlist('web', 'deny_read')
38 if deny_read and (not user or ismember(hgweb.repo.ui, user, deny_read)):
38 if deny_read and (not user or ismember(hgweb.repo.ui, user, deny_read)):
39 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
39 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
40
40
41 allow_read = hgweb.configlist('web', 'allow_read')
41 allow_read = hgweb.configlist('web', 'allow_read')
42 if allow_read and (not ismember(hgweb.repo.ui, user, allow_read)):
42 if allow_read and (not ismember(hgweb.repo.ui, user, allow_read)):
43 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
43 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
44
44
45 if op == 'pull' and not hgweb.allowpull:
45 if op == 'pull' and not hgweb.allowpull:
46 raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized')
46 raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized')
47 elif op == 'pull' or op is None: # op is None for interface requests
47 elif op == 'pull' or op is None: # op is None for interface requests
48 return
48 return
49
49
50 # enforce that you can only push using POST requests
50 # enforce that you can only push using POST requests
51 if req.env['REQUEST_METHOD'] != 'POST':
51 if req.env['REQUEST_METHOD'] != 'POST':
52 msg = 'push requires POST request'
52 msg = 'push requires POST request'
53 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
53 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
54
54
55 # require ssl by default for pushing, auth info cannot be sniffed
55 # require ssl by default for pushing, auth info cannot be sniffed
56 # and replayed
56 # and replayed
57 scheme = req.env.get('wsgi.url_scheme')
57 scheme = req.env.get('wsgi.url_scheme')
58 if hgweb.configbool('web', 'push_ssl', True) and scheme != 'https':
58 if hgweb.configbool('web', 'push_ssl', True) and scheme != 'https':
59 raise ErrorResponse(HTTP_FORBIDDEN, 'ssl required')
59 raise ErrorResponse(HTTP_FORBIDDEN, 'ssl required')
60
60
61 deny = hgweb.configlist('web', 'deny_push')
61 deny = hgweb.configlist('web', 'deny_push')
62 if deny and (not user or ismember(hgweb.repo.ui, user, deny)):
62 if deny and (not user or ismember(hgweb.repo.ui, user, deny)):
63 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
63 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
64
64
65 allow = hgweb.configlist('web', 'allow_push')
65 allow = hgweb.configlist('web', 'allow_push')
66 if not (allow and ismember(hgweb.repo.ui, user, allow)):
66 if not (allow and ismember(hgweb.repo.ui, user, allow)):
67 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
67 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
68
68
69 # Hooks for hgweb permission checks; extensions can add hooks here.
69 # Hooks for hgweb permission checks; extensions can add hooks here.
70 # Each hook is invoked like this: hook(hgweb, request, operation),
70 # Each hook is invoked like this: hook(hgweb, request, operation),
71 # where operation is either read, pull or push. Hooks should either
71 # where operation is either read, pull or push. Hooks should either
72 # raise an ErrorResponse exception, or just return.
72 # raise an ErrorResponse exception, or just return.
73 #
73 #
74 # It is possible to do both authentication and authorization through
74 # It is possible to do both authentication and authorization through
75 # this.
75 # this.
76 permhooks = [checkauthz]
76 permhooks = [checkauthz]
77
77
78
78
79 class ErrorResponse(Exception):
79 class ErrorResponse(Exception):
80 def __init__(self, code, message=None, headers=[]):
80 def __init__(self, code, message=None, headers=[]):
81 if message is None:
81 if message is None:
82 message = _statusmessage(code)
82 message = _statusmessage(code)
83 Exception.__init__(self)
83 Exception.__init__(self)
84 self.code = code
84 self.code = code
85 self.message = message
85 self.message = message
86 self.headers = headers
86 self.headers = headers
87 def __str__(self):
87 def __str__(self):
88 return self.message
88 return self.message
89
89
90 class continuereader(object):
90 class continuereader(object):
91 def __init__(self, f, write):
91 def __init__(self, f, write):
92 self.f = f
92 self.f = f
93 self._write = write
93 self._write = write
94 self.continued = False
94 self.continued = False
95
95
96 def read(self, amt=-1):
96 def read(self, amt=-1):
97 if not self.continued:
97 if not self.continued:
98 self.continued = True
98 self.continued = True
99 self._write('HTTP/1.1 100 Continue\r\n\r\n')
99 self._write('HTTP/1.1 100 Continue\r\n\r\n')
100 return self.f.read(amt)
100 return self.f.read(amt)
101
101
102 def __getattr__(self, attr):
102 def __getattr__(self, attr):
103 if attr in ('close', 'readline', 'readlines', '__iter__'):
103 if attr in ('close', 'readline', 'readlines', '__iter__'):
104 return getattr(self.f, attr)
104 return getattr(self.f, attr)
105 raise AttributeError
105 raise AttributeError
106
106
107 def _statusmessage(code):
107 def _statusmessage(code):
108 from BaseHTTPServer import BaseHTTPRequestHandler
108 from BaseHTTPServer import BaseHTTPRequestHandler
109 responses = BaseHTTPRequestHandler.responses
109 responses = BaseHTTPRequestHandler.responses
110 return responses.get(code, ('Error', 'Unknown error'))[0]
110 return responses.get(code, ('Error', 'Unknown error'))[0]
111
111
112 def statusmessage(code, message=None):
112 def statusmessage(code, message=None):
113 return '%d %s' % (code, message or _statusmessage(code))
113 return '%d %s' % (code, message or _statusmessage(code))
114
114
115 def get_stat(spath):
115 def get_stat(spath, fn="00changelog.i"):
116 """stat changelog if it exists, spath otherwise"""
116 """stat fn (00changelog.i by default) if it exists, spath otherwise"""
117 cl_path = os.path.join(spath, "00changelog.i")
117 cl_path = os.path.join(spath, fn)
118 if os.path.exists(cl_path):
118 if os.path.exists(cl_path):
119 return os.stat(cl_path)
119 return os.stat(cl_path)
120 else:
120 else:
121 return os.stat(spath)
121 return os.stat(spath)
122
122
123 def get_mtime(spath):
123 def get_mtime(spath):
124 return get_stat(spath).st_mtime
124 return get_stat(spath).st_mtime
125
125
126 def staticfile(directory, fname, req):
126 def staticfile(directory, fname, req):
127 """return a file inside directory with guessed Content-Type header
127 """return a file inside directory with guessed Content-Type header
128
128
129 fname always uses '/' as directory separator and isn't allowed to
129 fname always uses '/' as directory separator and isn't allowed to
130 contain unusual path components.
130 contain unusual path components.
131 Content-Type is guessed using the mimetypes module.
131 Content-Type is guessed using the mimetypes module.
132 Return an empty string if fname is illegal or file not found.
132 Return an empty string if fname is illegal or file not found.
133
133
134 """
134 """
135 parts = fname.split('/')
135 parts = fname.split('/')
136 for part in parts:
136 for part in parts:
137 if (part in ('', os.curdir, os.pardir) or
137 if (part in ('', os.curdir, os.pardir) or
138 os.sep in part or os.altsep is not None and os.altsep in part):
138 os.sep in part or os.altsep is not None and os.altsep in part):
139 return
139 return
140 fpath = os.path.join(*parts)
140 fpath = os.path.join(*parts)
141 if isinstance(directory, str):
141 if isinstance(directory, str):
142 directory = [directory]
142 directory = [directory]
143 for d in directory:
143 for d in directory:
144 path = os.path.join(d, fpath)
144 path = os.path.join(d, fpath)
145 if os.path.exists(path):
145 if os.path.exists(path):
146 break
146 break
147 try:
147 try:
148 os.stat(path)
148 os.stat(path)
149 ct = mimetypes.guess_type(path)[0] or "text/plain"
149 ct = mimetypes.guess_type(path)[0] or "text/plain"
150 fp = open(path, 'rb')
150 fp = open(path, 'rb')
151 data = fp.read()
151 data = fp.read()
152 fp.close()
152 fp.close()
153 req.respond(HTTP_OK, ct, body=data)
153 req.respond(HTTP_OK, ct, body=data)
154 except TypeError:
154 except TypeError:
155 raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal filename')
155 raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal filename')
156 except OSError, err:
156 except OSError, err:
157 if err.errno == errno.ENOENT:
157 if err.errno == errno.ENOENT:
158 raise ErrorResponse(HTTP_NOT_FOUND)
158 raise ErrorResponse(HTTP_NOT_FOUND)
159 else:
159 else:
160 raise ErrorResponse(HTTP_SERVER_ERROR, err.strerror)
160 raise ErrorResponse(HTTP_SERVER_ERROR, err.strerror)
161
161
162 def paritygen(stripecount, offset=0):
162 def paritygen(stripecount, offset=0):
163 """count parity of horizontal stripes for easier reading"""
163 """count parity of horizontal stripes for easier reading"""
164 if stripecount and offset:
164 if stripecount and offset:
165 # account for offset, e.g. due to building the list in reverse
165 # account for offset, e.g. due to building the list in reverse
166 count = (stripecount + offset) % stripecount
166 count = (stripecount + offset) % stripecount
167 parity = (stripecount + offset) / stripecount & 1
167 parity = (stripecount + offset) / stripecount & 1
168 else:
168 else:
169 count = 0
169 count = 0
170 parity = 0
170 parity = 0
171 while True:
171 while True:
172 yield parity
172 yield parity
173 count += 1
173 count += 1
174 if stripecount and count >= stripecount:
174 if stripecount and count >= stripecount:
175 parity = 1 - parity
175 parity = 1 - parity
176 count = 0
176 count = 0
177
177
178 def get_contact(config):
178 def get_contact(config):
179 """Return repo contact information or empty string.
179 """Return repo contact information or empty string.
180
180
181 web.contact is the primary source, but if that is not set, try
181 web.contact is the primary source, but if that is not set, try
182 ui.username or $EMAIL as a fallback to display something useful.
182 ui.username or $EMAIL as a fallback to display something useful.
183 """
183 """
184 return (config("web", "contact") or
184 return (config("web", "contact") or
185 config("ui", "username") or
185 config("ui", "username") or
186 os.environ.get("EMAIL") or "")
186 os.environ.get("EMAIL") or "")
187
187
188 def caching(web, req):
188 def caching(web, req):
189 tag = str(web.mtime)
189 tag = str(web.mtime)
190 if req.env.get('HTTP_IF_NONE_MATCH') == tag:
190 if req.env.get('HTTP_IF_NONE_MATCH') == tag:
191 raise ErrorResponse(HTTP_NOT_MODIFIED)
191 raise ErrorResponse(HTTP_NOT_MODIFIED)
192 req.headers.append(('ETag', tag))
192 req.headers.append(('ETag', tag))
@@ -1,394 +1,398 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 import os, re
9 import os, re
10 from mercurial import ui, hg, hook, error, encoding, templater, util, repoview
10 from mercurial import ui, hg, hook, error, encoding, templater, util, repoview
11 from mercurial.templatefilters import websub
11 from mercurial.templatefilters import websub
12 from mercurial.i18n import _
12 from mercurial.i18n import _
13 from common import get_stat, ErrorResponse, permhooks, caching
13 from common import get_stat, ErrorResponse, permhooks, caching
14 from common import HTTP_OK, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST
14 from common import HTTP_OK, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST
15 from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR
15 from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR
16 from request import wsgirequest
16 from request import wsgirequest
17 import webcommands, protocol, webutil
17 import webcommands, protocol, webutil
18
18
19 perms = {
19 perms = {
20 'changegroup': 'pull',
20 'changegroup': 'pull',
21 'changegroupsubset': 'pull',
21 'changegroupsubset': 'pull',
22 'getbundle': 'pull',
22 'getbundle': 'pull',
23 'stream_out': 'pull',
23 'stream_out': 'pull',
24 'listkeys': 'pull',
24 'listkeys': 'pull',
25 'unbundle': 'push',
25 'unbundle': 'push',
26 'pushkey': 'push',
26 'pushkey': 'push',
27 }
27 }
28
28
29 def makebreadcrumb(url, prefix=''):
29 def makebreadcrumb(url, prefix=''):
30 '''Return a 'URL breadcrumb' list
30 '''Return a 'URL breadcrumb' list
31
31
32 A 'URL breadcrumb' is a list of URL-name pairs,
32 A 'URL breadcrumb' is a list of URL-name pairs,
33 corresponding to each of the path items on a URL.
33 corresponding to each of the path items on a URL.
34 This can be used to create path navigation entries.
34 This can be used to create path navigation entries.
35 '''
35 '''
36 if url.endswith('/'):
36 if url.endswith('/'):
37 url = url[:-1]
37 url = url[:-1]
38 if prefix:
38 if prefix:
39 url = '/' + prefix + url
39 url = '/' + prefix + url
40 relpath = url
40 relpath = url
41 if relpath.startswith('/'):
41 if relpath.startswith('/'):
42 relpath = relpath[1:]
42 relpath = relpath[1:]
43
43
44 breadcrumb = []
44 breadcrumb = []
45 urlel = url
45 urlel = url
46 pathitems = [''] + relpath.split('/')
46 pathitems = [''] + relpath.split('/')
47 for pathel in reversed(pathitems):
47 for pathel in reversed(pathitems):
48 if not pathel or not urlel:
48 if not pathel or not urlel:
49 break
49 break
50 breadcrumb.append({'url': urlel, 'name': pathel})
50 breadcrumb.append({'url': urlel, 'name': pathel})
51 urlel = os.path.dirname(urlel)
51 urlel = os.path.dirname(urlel)
52 return reversed(breadcrumb)
52 return reversed(breadcrumb)
53
53
54
54
55 class hgweb(object):
55 class hgweb(object):
56 def __init__(self, repo, name=None, baseui=None):
56 def __init__(self, repo, name=None, baseui=None):
57 if isinstance(repo, str):
57 if isinstance(repo, str):
58 if baseui:
58 if baseui:
59 u = baseui.copy()
59 u = baseui.copy()
60 else:
60 else:
61 u = ui.ui()
61 u = ui.ui()
62 r = hg.repository(u, repo)
62 r = hg.repository(u, repo)
63 else:
63 else:
64 # we trust caller to give us a private copy
64 # we trust caller to give us a private copy
65 r = repo
65 r = repo
66
66
67 r = self._getview(r)
67 r = self._getview(r)
68 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
68 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
69 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
69 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
70 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
70 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
71 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
71 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
72 self.repo = r
72 self.repo = r
73 hook.redirect(True)
73 hook.redirect(True)
74 self.repostate = ((-1, -1), (-1, -1))
74 self.mtime = -1
75 self.mtime = -1
75 self.size = -1
76 self.reponame = name
76 self.reponame = name
77 self.archives = 'zip', 'gz', 'bz2'
77 self.archives = 'zip', 'gz', 'bz2'
78 self.stripecount = 1
78 self.stripecount = 1
79 # a repo owner may set web.templates in .hg/hgrc to get any file
79 # a repo owner may set web.templates in .hg/hgrc to get any file
80 # readable by the user running the CGI script
80 # readable by the user running the CGI script
81 self.templatepath = self.config('web', 'templates')
81 self.templatepath = self.config('web', 'templates')
82 self.websubtable = self.loadwebsub()
82 self.websubtable = self.loadwebsub()
83
83
84 # The CGI scripts are often run by a user different from the repo owner.
84 # The CGI scripts are often run by a user different from the repo owner.
85 # Trust the settings from the .hg/hgrc files by default.
85 # Trust the settings from the .hg/hgrc files by default.
86 def config(self, section, name, default=None, untrusted=True):
86 def config(self, section, name, default=None, untrusted=True):
87 return self.repo.ui.config(section, name, default,
87 return self.repo.ui.config(section, name, default,
88 untrusted=untrusted)
88 untrusted=untrusted)
89
89
90 def configbool(self, section, name, default=False, untrusted=True):
90 def configbool(self, section, name, default=False, untrusted=True):
91 return self.repo.ui.configbool(section, name, default,
91 return self.repo.ui.configbool(section, name, default,
92 untrusted=untrusted)
92 untrusted=untrusted)
93
93
94 def configlist(self, section, name, default=None, untrusted=True):
94 def configlist(self, section, name, default=None, untrusted=True):
95 return self.repo.ui.configlist(section, name, default,
95 return self.repo.ui.configlist(section, name, default,
96 untrusted=untrusted)
96 untrusted=untrusted)
97
97
98 def _getview(self, repo):
98 def _getview(self, repo):
99 viewconfig = repo.ui.config('web', 'view', 'served',
99 viewconfig = repo.ui.config('web', 'view', 'served',
100 untrusted=True)
100 untrusted=True)
101 if viewconfig == 'all':
101 if viewconfig == 'all':
102 return repo.unfiltered()
102 return repo.unfiltered()
103 elif viewconfig in repoview.filtertable:
103 elif viewconfig in repoview.filtertable:
104 return repo.filtered(viewconfig)
104 return repo.filtered(viewconfig)
105 else:
105 else:
106 return repo.filtered('served')
106 return repo.filtered('served')
107
107
108 def refresh(self, request=None):
108 def refresh(self, request=None):
109 st = get_stat(self.repo.spath)
109 st = get_stat(self.repo.spath)
110 # compare changelog size in addition to mtime to catch
110 pst = get_stat(self.repo.spath, 'phaseroots')
111 # rollbacks made less than a second ago
111 # changelog mtime and size, phaseroots mtime and size
112 if st.st_mtime != self.mtime or st.st_size != self.size:
112 repostate = ((st.st_mtime, st.st_size), (pst.st_mtime, pst.st_size))
113 # we need to compare file size in addition to mtime to catch
114 # changes made less than a second ago
115 if repostate != self.repostate:
113 r = hg.repository(self.repo.baseui, self.repo.root)
116 r = hg.repository(self.repo.baseui, self.repo.root)
114 self.repo = self._getview(r)
117 self.repo = self._getview(r)
115 self.maxchanges = int(self.config("web", "maxchanges", 10))
118 self.maxchanges = int(self.config("web", "maxchanges", 10))
116 self.stripecount = int(self.config("web", "stripes", 1))
119 self.stripecount = int(self.config("web", "stripes", 1))
117 self.maxshortchanges = int(self.config("web", "maxshortchanges",
120 self.maxshortchanges = int(self.config("web", "maxshortchanges",
118 60))
121 60))
119 self.maxfiles = int(self.config("web", "maxfiles", 10))
122 self.maxfiles = int(self.config("web", "maxfiles", 10))
120 self.allowpull = self.configbool("web", "allowpull", True)
123 self.allowpull = self.configbool("web", "allowpull", True)
121 encoding.encoding = self.config("web", "encoding",
124 encoding.encoding = self.config("web", "encoding",
122 encoding.encoding)
125 encoding.encoding)
123 # update these last to avoid threads seeing empty settings
126 # update these last to avoid threads seeing empty settings
127 self.repostate = repostate
128 # mtime is needed for ETag
124 self.mtime = st.st_mtime
129 self.mtime = st.st_mtime
125 self.size = st.st_size
126 if request:
130 if request:
127 self.repo.ui.environ = request.env
131 self.repo.ui.environ = request.env
128
132
129 def run(self):
133 def run(self):
130 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
134 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
131 raise RuntimeError("This function is only intended to be "
135 raise RuntimeError("This function is only intended to be "
132 "called while running as a CGI script.")
136 "called while running as a CGI script.")
133 import mercurial.hgweb.wsgicgi as wsgicgi
137 import mercurial.hgweb.wsgicgi as wsgicgi
134 wsgicgi.launch(self)
138 wsgicgi.launch(self)
135
139
136 def __call__(self, env, respond):
140 def __call__(self, env, respond):
137 req = wsgirequest(env, respond)
141 req = wsgirequest(env, respond)
138 return self.run_wsgi(req)
142 return self.run_wsgi(req)
139
143
140 def run_wsgi(self, req):
144 def run_wsgi(self, req):
141
145
142 self.refresh(req)
146 self.refresh(req)
143
147
144 # work with CGI variables to create coherent structure
148 # work with CGI variables to create coherent structure
145 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
149 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
146
150
147 req.url = req.env['SCRIPT_NAME']
151 req.url = req.env['SCRIPT_NAME']
148 if not req.url.endswith('/'):
152 if not req.url.endswith('/'):
149 req.url += '/'
153 req.url += '/'
150 if 'REPO_NAME' in req.env:
154 if 'REPO_NAME' in req.env:
151 req.url += req.env['REPO_NAME'] + '/'
155 req.url += req.env['REPO_NAME'] + '/'
152
156
153 if 'PATH_INFO' in req.env:
157 if 'PATH_INFO' in req.env:
154 parts = req.env['PATH_INFO'].strip('/').split('/')
158 parts = req.env['PATH_INFO'].strip('/').split('/')
155 repo_parts = req.env.get('REPO_NAME', '').split('/')
159 repo_parts = req.env.get('REPO_NAME', '').split('/')
156 if parts[:len(repo_parts)] == repo_parts:
160 if parts[:len(repo_parts)] == repo_parts:
157 parts = parts[len(repo_parts):]
161 parts = parts[len(repo_parts):]
158 query = '/'.join(parts)
162 query = '/'.join(parts)
159 else:
163 else:
160 query = req.env['QUERY_STRING'].split('&', 1)[0]
164 query = req.env['QUERY_STRING'].split('&', 1)[0]
161 query = query.split(';', 1)[0]
165 query = query.split(';', 1)[0]
162
166
163 # process this if it's a protocol request
167 # process this if it's a protocol request
164 # protocol bits don't need to create any URLs
168 # protocol bits don't need to create any URLs
165 # and the clients always use the old URL structure
169 # and the clients always use the old URL structure
166
170
167 cmd = req.form.get('cmd', [''])[0]
171 cmd = req.form.get('cmd', [''])[0]
168 if protocol.iscmd(cmd):
172 if protocol.iscmd(cmd):
169 try:
173 try:
170 if query:
174 if query:
171 raise ErrorResponse(HTTP_NOT_FOUND)
175 raise ErrorResponse(HTTP_NOT_FOUND)
172 if cmd in perms:
176 if cmd in perms:
173 self.check_perm(req, perms[cmd])
177 self.check_perm(req, perms[cmd])
174 return protocol.call(self.repo, req, cmd)
178 return protocol.call(self.repo, req, cmd)
175 except ErrorResponse, inst:
179 except ErrorResponse, inst:
176 # A client that sends unbundle without 100-continue will
180 # A client that sends unbundle without 100-continue will
177 # break if we respond early.
181 # break if we respond early.
178 if (cmd == 'unbundle' and
182 if (cmd == 'unbundle' and
179 (req.env.get('HTTP_EXPECT',
183 (req.env.get('HTTP_EXPECT',
180 '').lower() != '100-continue') or
184 '').lower() != '100-continue') or
181 req.env.get('X-HgHttp2', '')):
185 req.env.get('X-HgHttp2', '')):
182 req.drain()
186 req.drain()
183 else:
187 else:
184 req.headers.append(('Connection', 'Close'))
188 req.headers.append(('Connection', 'Close'))
185 req.respond(inst, protocol.HGTYPE,
189 req.respond(inst, protocol.HGTYPE,
186 body='0\n%s\n' % inst.message)
190 body='0\n%s\n' % inst.message)
187 return ''
191 return ''
188
192
189 # translate user-visible url structure to internal structure
193 # translate user-visible url structure to internal structure
190
194
191 args = query.split('/', 2)
195 args = query.split('/', 2)
192 if 'cmd' not in req.form and args and args[0]:
196 if 'cmd' not in req.form and args and args[0]:
193
197
194 cmd = args.pop(0)
198 cmd = args.pop(0)
195 style = cmd.rfind('-')
199 style = cmd.rfind('-')
196 if style != -1:
200 if style != -1:
197 req.form['style'] = [cmd[:style]]
201 req.form['style'] = [cmd[:style]]
198 cmd = cmd[style + 1:]
202 cmd = cmd[style + 1:]
199
203
200 # avoid accepting e.g. style parameter as command
204 # avoid accepting e.g. style parameter as command
201 if util.safehasattr(webcommands, cmd):
205 if util.safehasattr(webcommands, cmd):
202 req.form['cmd'] = [cmd]
206 req.form['cmd'] = [cmd]
203
207
204 if cmd == 'static':
208 if cmd == 'static':
205 req.form['file'] = ['/'.join(args)]
209 req.form['file'] = ['/'.join(args)]
206 else:
210 else:
207 if args and args[0]:
211 if args and args[0]:
208 node = args.pop(0)
212 node = args.pop(0)
209 req.form['node'] = [node]
213 req.form['node'] = [node]
210 if args:
214 if args:
211 req.form['file'] = args
215 req.form['file'] = args
212
216
213 ua = req.env.get('HTTP_USER_AGENT', '')
217 ua = req.env.get('HTTP_USER_AGENT', '')
214 if cmd == 'rev' and 'mercurial' in ua:
218 if cmd == 'rev' and 'mercurial' in ua:
215 req.form['style'] = ['raw']
219 req.form['style'] = ['raw']
216
220
217 if cmd == 'archive':
221 if cmd == 'archive':
218 fn = req.form['node'][0]
222 fn = req.form['node'][0]
219 for type_, spec in self.archive_specs.iteritems():
223 for type_, spec in self.archive_specs.iteritems():
220 ext = spec[2]
224 ext = spec[2]
221 if fn.endswith(ext):
225 if fn.endswith(ext):
222 req.form['node'] = [fn[:-len(ext)]]
226 req.form['node'] = [fn[:-len(ext)]]
223 req.form['type'] = [type_]
227 req.form['type'] = [type_]
224
228
225 # process the web interface request
229 # process the web interface request
226
230
227 try:
231 try:
228 tmpl = self.templater(req)
232 tmpl = self.templater(req)
229 ctype = tmpl('mimetype', encoding=encoding.encoding)
233 ctype = tmpl('mimetype', encoding=encoding.encoding)
230 ctype = templater.stringify(ctype)
234 ctype = templater.stringify(ctype)
231
235
232 # check read permissions non-static content
236 # check read permissions non-static content
233 if cmd != 'static':
237 if cmd != 'static':
234 self.check_perm(req, None)
238 self.check_perm(req, None)
235
239
236 if cmd == '':
240 if cmd == '':
237 req.form['cmd'] = [tmpl.cache['default']]
241 req.form['cmd'] = [tmpl.cache['default']]
238 cmd = req.form['cmd'][0]
242 cmd = req.form['cmd'][0]
239
243
240 if self.configbool('web', 'cache', True):
244 if self.configbool('web', 'cache', True):
241 caching(self, req) # sets ETag header or raises NOT_MODIFIED
245 caching(self, req) # sets ETag header or raises NOT_MODIFIED
242 if cmd not in webcommands.__all__:
246 if cmd not in webcommands.__all__:
243 msg = 'no such method: %s' % cmd
247 msg = 'no such method: %s' % cmd
244 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
248 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
245 elif cmd == 'file' and 'raw' in req.form.get('style', []):
249 elif cmd == 'file' and 'raw' in req.form.get('style', []):
246 self.ctype = ctype
250 self.ctype = ctype
247 content = webcommands.rawfile(self, req, tmpl)
251 content = webcommands.rawfile(self, req, tmpl)
248 else:
252 else:
249 content = getattr(webcommands, cmd)(self, req, tmpl)
253 content = getattr(webcommands, cmd)(self, req, tmpl)
250 req.respond(HTTP_OK, ctype)
254 req.respond(HTTP_OK, ctype)
251
255
252 return content
256 return content
253
257
254 except (error.LookupError, error.RepoLookupError), err:
258 except (error.LookupError, error.RepoLookupError), err:
255 req.respond(HTTP_NOT_FOUND, ctype)
259 req.respond(HTTP_NOT_FOUND, ctype)
256 msg = str(err)
260 msg = str(err)
257 if (util.safehasattr(err, 'name') and
261 if (util.safehasattr(err, 'name') and
258 not isinstance(err, error.ManifestLookupError)):
262 not isinstance(err, error.ManifestLookupError)):
259 msg = 'revision not found: %s' % err.name
263 msg = 'revision not found: %s' % err.name
260 return tmpl('error', error=msg)
264 return tmpl('error', error=msg)
261 except (error.RepoError, error.RevlogError), inst:
265 except (error.RepoError, error.RevlogError), inst:
262 req.respond(HTTP_SERVER_ERROR, ctype)
266 req.respond(HTTP_SERVER_ERROR, ctype)
263 return tmpl('error', error=str(inst))
267 return tmpl('error', error=str(inst))
264 except ErrorResponse, inst:
268 except ErrorResponse, inst:
265 req.respond(inst, ctype)
269 req.respond(inst, ctype)
266 if inst.code == HTTP_NOT_MODIFIED:
270 if inst.code == HTTP_NOT_MODIFIED:
267 # Not allowed to return a body on a 304
271 # Not allowed to return a body on a 304
268 return ['']
272 return ['']
269 return tmpl('error', error=inst.message)
273 return tmpl('error', error=inst.message)
270
274
271 def loadwebsub(self):
275 def loadwebsub(self):
272 websubtable = []
276 websubtable = []
273 websubdefs = self.repo.ui.configitems('websub')
277 websubdefs = self.repo.ui.configitems('websub')
274 # we must maintain interhg backwards compatibility
278 # we must maintain interhg backwards compatibility
275 websubdefs += self.repo.ui.configitems('interhg')
279 websubdefs += self.repo.ui.configitems('interhg')
276 for key, pattern in websubdefs:
280 for key, pattern in websubdefs:
277 # grab the delimiter from the character after the "s"
281 # grab the delimiter from the character after the "s"
278 unesc = pattern[1]
282 unesc = pattern[1]
279 delim = re.escape(unesc)
283 delim = re.escape(unesc)
280
284
281 # identify portions of the pattern, taking care to avoid escaped
285 # identify portions of the pattern, taking care to avoid escaped
282 # delimiters. the replace format and flags are optional, but
286 # delimiters. the replace format and flags are optional, but
283 # delimiters are required.
287 # delimiters are required.
284 match = re.match(
288 match = re.match(
285 r'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
289 r'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
286 % (delim, delim, delim), pattern)
290 % (delim, delim, delim), pattern)
287 if not match:
291 if not match:
288 self.repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
292 self.repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
289 % (key, pattern))
293 % (key, pattern))
290 continue
294 continue
291
295
292 # we need to unescape the delimiter for regexp and format
296 # we need to unescape the delimiter for regexp and format
293 delim_re = re.compile(r'(?<!\\)\\%s' % delim)
297 delim_re = re.compile(r'(?<!\\)\\%s' % delim)
294 regexp = delim_re.sub(unesc, match.group(1))
298 regexp = delim_re.sub(unesc, match.group(1))
295 format = delim_re.sub(unesc, match.group(2))
299 format = delim_re.sub(unesc, match.group(2))
296
300
297 # the pattern allows for 6 regexp flags, so set them if necessary
301 # the pattern allows for 6 regexp flags, so set them if necessary
298 flagin = match.group(3)
302 flagin = match.group(3)
299 flags = 0
303 flags = 0
300 if flagin:
304 if flagin:
301 for flag in flagin.upper():
305 for flag in flagin.upper():
302 flags |= re.__dict__[flag]
306 flags |= re.__dict__[flag]
303
307
304 try:
308 try:
305 regexp = re.compile(regexp, flags)
309 regexp = re.compile(regexp, flags)
306 websubtable.append((regexp, format))
310 websubtable.append((regexp, format))
307 except re.error:
311 except re.error:
308 self.repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
312 self.repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
309 % (key, regexp))
313 % (key, regexp))
310 return websubtable
314 return websubtable
311
315
312 def templater(self, req):
316 def templater(self, req):
313
317
314 # determine scheme, port and server name
318 # determine scheme, port and server name
315 # this is needed to create absolute urls
319 # this is needed to create absolute urls
316
320
317 proto = req.env.get('wsgi.url_scheme')
321 proto = req.env.get('wsgi.url_scheme')
318 if proto == 'https':
322 if proto == 'https':
319 proto = 'https'
323 proto = 'https'
320 default_port = "443"
324 default_port = "443"
321 else:
325 else:
322 proto = 'http'
326 proto = 'http'
323 default_port = "80"
327 default_port = "80"
324
328
325 port = req.env["SERVER_PORT"]
329 port = req.env["SERVER_PORT"]
326 port = port != default_port and (":" + port) or ""
330 port = port != default_port and (":" + port) or ""
327 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
331 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
328 logourl = self.config("web", "logourl", "http://mercurial.selenic.com/")
332 logourl = self.config("web", "logourl", "http://mercurial.selenic.com/")
329 logoimg = self.config("web", "logoimg", "hglogo.png")
333 logoimg = self.config("web", "logoimg", "hglogo.png")
330 staticurl = self.config("web", "staticurl") or req.url + 'static/'
334 staticurl = self.config("web", "staticurl") or req.url + 'static/'
331 if not staticurl.endswith('/'):
335 if not staticurl.endswith('/'):
332 staticurl += '/'
336 staticurl += '/'
333
337
334 # some functions for the templater
338 # some functions for the templater
335
339
336 def motd(**map):
340 def motd(**map):
337 yield self.config("web", "motd", "")
341 yield self.config("web", "motd", "")
338
342
339 # figure out which style to use
343 # figure out which style to use
340
344
341 vars = {}
345 vars = {}
342 styles = (
346 styles = (
343 req.form.get('style', [None])[0],
347 req.form.get('style', [None])[0],
344 self.config('web', 'style'),
348 self.config('web', 'style'),
345 'paper',
349 'paper',
346 )
350 )
347 style, mapfile = templater.stylemap(styles, self.templatepath)
351 style, mapfile = templater.stylemap(styles, self.templatepath)
348 if style == styles[0]:
352 if style == styles[0]:
349 vars['style'] = style
353 vars['style'] = style
350
354
351 start = req.url[-1] == '?' and '&' or '?'
355 start = req.url[-1] == '?' and '&' or '?'
352 sessionvars = webutil.sessionvars(vars, start)
356 sessionvars = webutil.sessionvars(vars, start)
353
357
354 if not self.reponame:
358 if not self.reponame:
355 self.reponame = (self.config("web", "name")
359 self.reponame = (self.config("web", "name")
356 or req.env.get('REPO_NAME')
360 or req.env.get('REPO_NAME')
357 or req.url.strip('/') or self.repo.root)
361 or req.url.strip('/') or self.repo.root)
358
362
359 def websubfilter(text):
363 def websubfilter(text):
360 return websub(text, self.websubtable)
364 return websub(text, self.websubtable)
361
365
362 # create the templater
366 # create the templater
363
367
364 tmpl = templater.templater(mapfile,
368 tmpl = templater.templater(mapfile,
365 filters={"websub": websubfilter},
369 filters={"websub": websubfilter},
366 defaults={"url": req.url,
370 defaults={"url": req.url,
367 "logourl": logourl,
371 "logourl": logourl,
368 "logoimg": logoimg,
372 "logoimg": logoimg,
369 "staticurl": staticurl,
373 "staticurl": staticurl,
370 "urlbase": urlbase,
374 "urlbase": urlbase,
371 "repo": self.reponame,
375 "repo": self.reponame,
372 "encoding": encoding.encoding,
376 "encoding": encoding.encoding,
373 "motd": motd,
377 "motd": motd,
374 "sessionvars": sessionvars,
378 "sessionvars": sessionvars,
375 "pathdef": makebreadcrumb(req.url),
379 "pathdef": makebreadcrumb(req.url),
376 "style": style,
380 "style": style,
377 })
381 })
378 return tmpl
382 return tmpl
379
383
380 def archivelist(self, nodeid):
384 def archivelist(self, nodeid):
381 allowed = self.configlist("web", "allow_archive")
385 allowed = self.configlist("web", "allow_archive")
382 for i, spec in self.archive_specs.iteritems():
386 for i, spec in self.archive_specs.iteritems():
383 if i in allowed or self.configbool("web", "allow" + i):
387 if i in allowed or self.configbool("web", "allow" + i):
384 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
388 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
385
389
386 archive_specs = {
390 archive_specs = {
387 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
391 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
388 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
392 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
389 'zip': ('application/zip', 'zip', '.zip', None),
393 'zip': ('application/zip', 'zip', '.zip', None),
390 }
394 }
391
395
392 def check_perm(self, req, op):
396 def check_perm(self, req, op):
393 for hook in permhooks:
397 for hook in permhooks:
394 hook(self, req, op)
398 hook(self, req, op)
@@ -1,538 +1,582 b''
1 $ "$TESTDIR/hghave" serve || exit 80
1 $ "$TESTDIR/hghave" serve || exit 80
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 serve -n test -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
13 $ hg serve -n test -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
14 $ cat hg.pid >> $DAEMON_PIDS
14 $ cat hg.pid >> $DAEMON_PIDS
15
15
16 manifest
16 manifest
17
17
18 $ ("$TESTDIR/get-with-headers.py" localhost:$HGPORT 'file/tip/?style=raw')
18 $ ("$TESTDIR/get-with-headers.py" localhost:$HGPORT 'file/tip/?style=raw')
19 200 Script output follows
19 200 Script output follows
20
20
21
21
22 drwxr-xr-x da
22 drwxr-xr-x da
23 -rw-r--r-- 4 foo
23 -rw-r--r-- 4 foo
24
24
25
25
26 $ ("$TESTDIR/get-with-headers.py" localhost:$HGPORT 'file/tip/da?style=raw')
26 $ ("$TESTDIR/get-with-headers.py" localhost:$HGPORT 'file/tip/da?style=raw')
27 200 Script output follows
27 200 Script output follows
28
28
29
29
30 -rw-r--r-- 4 foo
30 -rw-r--r-- 4 foo
31
31
32
32
33
33
34 plain file
34 plain file
35
35
36 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'file/tip/foo?style=raw'
36 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'file/tip/foo?style=raw'
37 200 Script output follows
37 200 Script output follows
38
38
39 foo
39 foo
40
40
41 should give a 404 - static file that does not exist
41 should give a 404 - static file that does not exist
42
42
43 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'static/bogus'
43 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'static/bogus'
44 404 Not Found
44 404 Not Found
45
45
46 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
46 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
47 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
47 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
48 <head>
48 <head>
49 <link rel="icon" href="/static/hgicon.png" type="image/png" />
49 <link rel="icon" href="/static/hgicon.png" type="image/png" />
50 <meta name="robots" content="index, nofollow" />
50 <meta name="robots" content="index, nofollow" />
51 <link rel="stylesheet" href="/static/style-paper.css" type="text/css" />
51 <link rel="stylesheet" href="/static/style-paper.css" type="text/css" />
52 <script type="text/javascript" src="/static/mercurial.js"></script>
52 <script type="text/javascript" src="/static/mercurial.js"></script>
53
53
54 <title>test: error</title>
54 <title>test: error</title>
55 </head>
55 </head>
56 <body>
56 <body>
57
57
58 <div class="container">
58 <div class="container">
59 <div class="menu">
59 <div class="menu">
60 <div class="logo">
60 <div class="logo">
61 <a href="http://mercurial.selenic.com/">
61 <a href="http://mercurial.selenic.com/">
62 <img src="/static/hglogo.png" width=75 height=90 border=0 alt="mercurial" /></a>
62 <img src="/static/hglogo.png" width=75 height=90 border=0 alt="mercurial" /></a>
63 </div>
63 </div>
64 <ul>
64 <ul>
65 <li><a href="/shortlog">log</a></li>
65 <li><a href="/shortlog">log</a></li>
66 <li><a href="/graph">graph</a></li>
66 <li><a href="/graph">graph</a></li>
67 <li><a href="/tags">tags</a></li>
67 <li><a href="/tags">tags</a></li>
68 <li><a href="/bookmarks">bookmarks</a></li>
68 <li><a href="/bookmarks">bookmarks</a></li>
69 <li><a href="/branches">branches</a></li>
69 <li><a href="/branches">branches</a></li>
70 </ul>
70 </ul>
71 <ul>
71 <ul>
72 <li><a href="/help">help</a></li>
72 <li><a href="/help">help</a></li>
73 </ul>
73 </ul>
74 </div>
74 </div>
75
75
76 <div class="main">
76 <div class="main">
77
77
78 <h2 class="breadcrumb"><a href="/">Mercurial</a> </h2>
78 <h2 class="breadcrumb"><a href="/">Mercurial</a> </h2>
79 <h3>error</h3>
79 <h3>error</h3>
80
80
81 <form class="search" action="/log">
81 <form class="search" action="/log">
82
82
83 <p><input name="rev" id="search1" type="text" size="30"></p>
83 <p><input name="rev" id="search1" type="text" size="30"></p>
84 <div id="hint">Find changesets by keywords (author, files, the commit message), revision
84 <div id="hint">Find changesets by keywords (author, files, the commit message), revision
85 number or hash, or <a href="/help/revsets">revset expression</a>.</div>
85 number or hash, or <a href="/help/revsets">revset expression</a>.</div>
86 </form>
86 </form>
87
87
88 <div class="description">
88 <div class="description">
89 <p>
89 <p>
90 An error occurred while processing your request:
90 An error occurred while processing your request:
91 </p>
91 </p>
92 <p>
92 <p>
93 Not Found
93 Not Found
94 </p>
94 </p>
95 </div>
95 </div>
96 </div>
96 </div>
97 </div>
97 </div>
98
98
99 <script type="text/javascript">process_dates()</script>
99 <script type="text/javascript">process_dates()</script>
100
100
101
101
102 </body>
102 </body>
103 </html>
103 </html>
104
104
105 [1]
105 [1]
106
106
107 should give a 404 - bad revision
107 should give a 404 - bad revision
108
108
109 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'file/spam/foo?style=raw'
109 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'file/spam/foo?style=raw'
110 404 Not Found
110 404 Not Found
111
111
112
112
113 error: revision not found: spam
113 error: revision not found: spam
114 [1]
114 [1]
115
115
116 should give a 400 - bad command
116 should give a 400 - bad command
117
117
118 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'file/tip/foo?cmd=spam&style=raw'
118 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'file/tip/foo?cmd=spam&style=raw'
119 400* (glob)
119 400* (glob)
120
120
121
121
122 error: no such method: spam
122 error: no such method: spam
123 [1]
123 [1]
124
124
125 $ "$TESTDIR/get-with-headers.py" --headeronly localhost:$HGPORT '?cmd=spam'
125 $ "$TESTDIR/get-with-headers.py" --headeronly localhost:$HGPORT '?cmd=spam'
126 400 no such method: spam
126 400 no such method: spam
127 [1]
127 [1]
128
128
129 should give a 400 - bad command as a part of url path (issue4071)
129 should give a 400 - bad command as a part of url path (issue4071)
130
130
131 $ "$TESTDIR/get-with-headers.py" --headeronly localhost:$HGPORT 'spam'
131 $ "$TESTDIR/get-with-headers.py" --headeronly localhost:$HGPORT 'spam'
132 400 no such method: spam
132 400 no such method: spam
133 [1]
133 [1]
134
134
135 $ "$TESTDIR/get-with-headers.py" --headeronly localhost:$HGPORT 'raw-spam'
135 $ "$TESTDIR/get-with-headers.py" --headeronly localhost:$HGPORT 'raw-spam'
136 400 no such method: spam
136 400 no such method: spam
137 [1]
137 [1]
138
138
139 $ "$TESTDIR/get-with-headers.py" --headeronly localhost:$HGPORT 'spam/tip/foo'
139 $ "$TESTDIR/get-with-headers.py" --headeronly localhost:$HGPORT 'spam/tip/foo'
140 400 no such method: spam
140 400 no such method: spam
141 [1]
141 [1]
142
142
143 should give a 404 - file does not exist
143 should give a 404 - file does not exist
144
144
145 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'file/tip/bork?style=raw'
145 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'file/tip/bork?style=raw'
146 404 Not Found
146 404 Not Found
147
147
148
148
149 error: bork@2ef0ac749a14: not found in manifest
149 error: bork@2ef0ac749a14: not found in manifest
150 [1]
150 [1]
151 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'file/tip/bork'
151 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'file/tip/bork'
152 404 Not Found
152 404 Not Found
153
153
154 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
154 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
155 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
155 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
156 <head>
156 <head>
157 <link rel="icon" href="/static/hgicon.png" type="image/png" />
157 <link rel="icon" href="/static/hgicon.png" type="image/png" />
158 <meta name="robots" content="index, nofollow" />
158 <meta name="robots" content="index, nofollow" />
159 <link rel="stylesheet" href="/static/style-paper.css" type="text/css" />
159 <link rel="stylesheet" href="/static/style-paper.css" type="text/css" />
160 <script type="text/javascript" src="/static/mercurial.js"></script>
160 <script type="text/javascript" src="/static/mercurial.js"></script>
161
161
162 <title>test: error</title>
162 <title>test: error</title>
163 </head>
163 </head>
164 <body>
164 <body>
165
165
166 <div class="container">
166 <div class="container">
167 <div class="menu">
167 <div class="menu">
168 <div class="logo">
168 <div class="logo">
169 <a href="http://mercurial.selenic.com/">
169 <a href="http://mercurial.selenic.com/">
170 <img src="/static/hglogo.png" width=75 height=90 border=0 alt="mercurial" /></a>
170 <img src="/static/hglogo.png" width=75 height=90 border=0 alt="mercurial" /></a>
171 </div>
171 </div>
172 <ul>
172 <ul>
173 <li><a href="/shortlog">log</a></li>
173 <li><a href="/shortlog">log</a></li>
174 <li><a href="/graph">graph</a></li>
174 <li><a href="/graph">graph</a></li>
175 <li><a href="/tags">tags</a></li>
175 <li><a href="/tags">tags</a></li>
176 <li><a href="/bookmarks">bookmarks</a></li>
176 <li><a href="/bookmarks">bookmarks</a></li>
177 <li><a href="/branches">branches</a></li>
177 <li><a href="/branches">branches</a></li>
178 </ul>
178 </ul>
179 <ul>
179 <ul>
180 <li><a href="/help">help</a></li>
180 <li><a href="/help">help</a></li>
181 </ul>
181 </ul>
182 </div>
182 </div>
183
183
184 <div class="main">
184 <div class="main">
185
185
186 <h2 class="breadcrumb"><a href="/">Mercurial</a> </h2>
186 <h2 class="breadcrumb"><a href="/">Mercurial</a> </h2>
187 <h3>error</h3>
187 <h3>error</h3>
188
188
189 <form class="search" action="/log">
189 <form class="search" action="/log">
190
190
191 <p><input name="rev" id="search1" type="text" size="30"></p>
191 <p><input name="rev" id="search1" type="text" size="30"></p>
192 <div id="hint">Find changesets by keywords (author, files, the commit message), revision
192 <div id="hint">Find changesets by keywords (author, files, the commit message), revision
193 number or hash, or <a href="/help/revsets">revset expression</a>.</div>
193 number or hash, or <a href="/help/revsets">revset expression</a>.</div>
194 </form>
194 </form>
195
195
196 <div class="description">
196 <div class="description">
197 <p>
197 <p>
198 An error occurred while processing your request:
198 An error occurred while processing your request:
199 </p>
199 </p>
200 <p>
200 <p>
201 bork@2ef0ac749a14: not found in manifest
201 bork@2ef0ac749a14: not found in manifest
202 </p>
202 </p>
203 </div>
203 </div>
204 </div>
204 </div>
205 </div>
205 </div>
206
206
207 <script type="text/javascript">process_dates()</script>
207 <script type="text/javascript">process_dates()</script>
208
208
209
209
210 </body>
210 </body>
211 </html>
211 </html>
212
212
213 [1]
213 [1]
214 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'diff/tip/bork?style=raw'
214 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'diff/tip/bork?style=raw'
215 404 Not Found
215 404 Not Found
216
216
217
217
218 error: bork@2ef0ac749a14: not found in manifest
218 error: bork@2ef0ac749a14: not found in manifest
219 [1]
219 [1]
220
220
221 try bad style
221 try bad style
222
222
223 $ ("$TESTDIR/get-with-headers.py" localhost:$HGPORT 'file/tip/?style=foobar')
223 $ ("$TESTDIR/get-with-headers.py" localhost:$HGPORT 'file/tip/?style=foobar')
224 200 Script output follows
224 200 Script output follows
225
225
226 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
226 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
227 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
227 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
228 <head>
228 <head>
229 <link rel="icon" href="/static/hgicon.png" type="image/png" />
229 <link rel="icon" href="/static/hgicon.png" type="image/png" />
230 <meta name="robots" content="index, nofollow" />
230 <meta name="robots" content="index, nofollow" />
231 <link rel="stylesheet" href="/static/style-paper.css" type="text/css" />
231 <link rel="stylesheet" href="/static/style-paper.css" type="text/css" />
232 <script type="text/javascript" src="/static/mercurial.js"></script>
232 <script type="text/javascript" src="/static/mercurial.js"></script>
233
233
234 <title>test: 2ef0ac749a14 /</title>
234 <title>test: 2ef0ac749a14 /</title>
235 </head>
235 </head>
236 <body>
236 <body>
237
237
238 <div class="container">
238 <div class="container">
239 <div class="menu">
239 <div class="menu">
240 <div class="logo">
240 <div class="logo">
241 <a href="http://mercurial.selenic.com/">
241 <a href="http://mercurial.selenic.com/">
242 <img src="/static/hglogo.png" alt="mercurial" /></a>
242 <img src="/static/hglogo.png" alt="mercurial" /></a>
243 </div>
243 </div>
244 <ul>
244 <ul>
245 <li><a href="/shortlog/2ef0ac749a14">log</a></li>
245 <li><a href="/shortlog/2ef0ac749a14">log</a></li>
246 <li><a href="/graph/2ef0ac749a14">graph</a></li>
246 <li><a href="/graph/2ef0ac749a14">graph</a></li>
247 <li><a href="/tags">tags</a></li>
247 <li><a href="/tags">tags</a></li>
248 <li><a href="/bookmarks">bookmarks</a></li>
248 <li><a href="/bookmarks">bookmarks</a></li>
249 <li><a href="/branches">branches</a></li>
249 <li><a href="/branches">branches</a></li>
250 </ul>
250 </ul>
251 <ul>
251 <ul>
252 <li><a href="/rev/2ef0ac749a14">changeset</a></li>
252 <li><a href="/rev/2ef0ac749a14">changeset</a></li>
253 <li class="active">browse</li>
253 <li class="active">browse</li>
254 </ul>
254 </ul>
255 <ul>
255 <ul>
256
256
257 </ul>
257 </ul>
258 <ul>
258 <ul>
259 <li><a href="/help">help</a></li>
259 <li><a href="/help">help</a></li>
260 </ul>
260 </ul>
261 </div>
261 </div>
262
262
263 <div class="main">
263 <div class="main">
264 <h2 class="breadcrumb"><a href="/">Mercurial</a> </h2>
264 <h2 class="breadcrumb"><a href="/">Mercurial</a> </h2>
265 <h3>directory / @ 0:2ef0ac749a14 <span class="tag">tip</span> </h3>
265 <h3>directory / @ 0:2ef0ac749a14 <span class="tag">tip</span> </h3>
266
266
267 <form class="search" action="/log">
267 <form class="search" action="/log">
268
268
269 <p><input name="rev" id="search1" type="text" size="30" /></p>
269 <p><input name="rev" id="search1" type="text" size="30" /></p>
270 <div id="hint">Find changesets by keywords (author, files, the commit message), revision
270 <div id="hint">Find changesets by keywords (author, files, the commit message), revision
271 number or hash, or <a href="/help/revsets">revset expression</a>.</div>
271 number or hash, or <a href="/help/revsets">revset expression</a>.</div>
272 </form>
272 </form>
273
273
274 <table class="bigtable">
274 <table class="bigtable">
275 <tr>
275 <tr>
276 <th class="name">name</th>
276 <th class="name">name</th>
277 <th class="size">size</th>
277 <th class="size">size</th>
278 <th class="permissions">permissions</th>
278 <th class="permissions">permissions</th>
279 </tr>
279 </tr>
280 <tbody class="stripes2">
280 <tbody class="stripes2">
281 <tr class="fileline">
281 <tr class="fileline">
282 <td class="name"><a href="/file/2ef0ac749a14/">[up]</a></td>
282 <td class="name"><a href="/file/2ef0ac749a14/">[up]</a></td>
283 <td class="size"></td>
283 <td class="size"></td>
284 <td class="permissions">drwxr-xr-x</td>
284 <td class="permissions">drwxr-xr-x</td>
285 </tr>
285 </tr>
286
286
287 <tr class="fileline">
287 <tr class="fileline">
288 <td class="name">
288 <td class="name">
289 <a href="/file/2ef0ac749a14/da">
289 <a href="/file/2ef0ac749a14/da">
290 <img src="/static/coal-folder.png" alt="dir."/> da/
290 <img src="/static/coal-folder.png" alt="dir."/> da/
291 </a>
291 </a>
292 <a href="/file/2ef0ac749a14/da/">
292 <a href="/file/2ef0ac749a14/da/">
293
293
294 </a>
294 </a>
295 </td>
295 </td>
296 <td class="size"></td>
296 <td class="size"></td>
297 <td class="permissions">drwxr-xr-x</td>
297 <td class="permissions">drwxr-xr-x</td>
298 </tr>
298 </tr>
299
299
300 <tr class="fileline">
300 <tr class="fileline">
301 <td class="filename">
301 <td class="filename">
302 <a href="/file/2ef0ac749a14/foo">
302 <a href="/file/2ef0ac749a14/foo">
303 <img src="/static/coal-file.png" alt="file"/> foo
303 <img src="/static/coal-file.png" alt="file"/> foo
304 </a>
304 </a>
305 </td>
305 </td>
306 <td class="size">4</td>
306 <td class="size">4</td>
307 <td class="permissions">-rw-r--r--</td>
307 <td class="permissions">-rw-r--r--</td>
308 </tr>
308 </tr>
309 </tbody>
309 </tbody>
310 </table>
310 </table>
311 </div>
311 </div>
312 </div>
312 </div>
313 <script type="text/javascript">process_dates()</script>
313 <script type="text/javascript">process_dates()</script>
314
314
315
315
316 </body>
316 </body>
317 </html>
317 </html>
318
318
319
319
320 stop and restart
320 stop and restart
321
321
322 $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS
322 $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS
323 $ hg serve -p $HGPORT -d --pid-file=hg.pid -A access.log
323 $ hg serve -p $HGPORT -d --pid-file=hg.pid -A access.log
324 $ cat hg.pid >> $DAEMON_PIDS
324 $ cat hg.pid >> $DAEMON_PIDS
325
325
326 Test the access/error files are opened in append mode
326 Test the access/error files are opened in append mode
327
327
328 $ python -c "print len(file('access.log').readlines()), 'log lines written'"
328 $ python -c "print len(file('access.log').readlines()), 'log lines written'"
329 14 log lines written
329 14 log lines written
330
330
331 static file
331 static file
332
332
333 $ "$TESTDIR/get-with-headers.py" --twice localhost:$HGPORT 'static/style-gitweb.css' - date etag server
333 $ "$TESTDIR/get-with-headers.py" --twice localhost:$HGPORT 'static/style-gitweb.css' - date etag server
334 200 Script output follows
334 200 Script output follows
335 content-length: 5262
335 content-length: 5262
336 content-type: text/css
336 content-type: text/css
337
337
338 body { font-family: sans-serif; font-size: 12px; border:solid #d9d8d1; border-width:1px; margin:10px; }
338 body { font-family: sans-serif; font-size: 12px; border:solid #d9d8d1; border-width:1px; margin:10px; }
339 a { color:#0000cc; }
339 a { color:#0000cc; }
340 a:hover, a:visited, a:active { color:#880000; }
340 a:hover, a:visited, a:active { color:#880000; }
341 div.page_header { height:25px; padding:8px; font-size:18px; font-weight:bold; background-color:#d9d8d1; }
341 div.page_header { height:25px; padding:8px; font-size:18px; font-weight:bold; background-color:#d9d8d1; }
342 div.page_header a:visited { color:#0000cc; }
342 div.page_header a:visited { color:#0000cc; }
343 div.page_header a:hover { color:#880000; }
343 div.page_header a:hover { color:#880000; }
344 div.page_nav { padding:8px; }
344 div.page_nav { padding:8px; }
345 div.page_nav a:visited { color:#0000cc; }
345 div.page_nav a:visited { color:#0000cc; }
346 div.page_path { padding:8px; border:solid #d9d8d1; border-width:0px 0px 1px}
346 div.page_path { padding:8px; border:solid #d9d8d1; border-width:0px 0px 1px}
347 div.page_footer { padding:4px 8px; background-color: #d9d8d1; }
347 div.page_footer { padding:4px 8px; background-color: #d9d8d1; }
348 div.page_footer_text { float:left; color:#555555; font-style:italic; }
348 div.page_footer_text { float:left; color:#555555; font-style:italic; }
349 div.page_body { padding:8px; }
349 div.page_body { padding:8px; }
350 div.title, a.title {
350 div.title, a.title {
351 display:block; padding:6px 8px;
351 display:block; padding:6px 8px;
352 font-weight:bold; background-color:#edece6; text-decoration:none; color:#000000;
352 font-weight:bold; background-color:#edece6; text-decoration:none; color:#000000;
353 }
353 }
354 a.title:hover { background-color: #d9d8d1; }
354 a.title:hover { background-color: #d9d8d1; }
355 div.title_text { padding:6px 0px; border: solid #d9d8d1; border-width:0px 0px 1px; }
355 div.title_text { padding:6px 0px; border: solid #d9d8d1; border-width:0px 0px 1px; }
356 div.log_body { padding:8px 8px 8px 150px; }
356 div.log_body { padding:8px 8px 8px 150px; }
357 .age { white-space:nowrap; }
357 .age { white-space:nowrap; }
358 span.age { position:relative; float:left; width:142px; font-style:italic; }
358 span.age { position:relative; float:left; width:142px; font-style:italic; }
359 div.log_link {
359 div.log_link {
360 padding:0px 8px;
360 padding:0px 8px;
361 font-size:10px; font-family:sans-serif; font-style:normal;
361 font-size:10px; font-family:sans-serif; font-style:normal;
362 position:relative; float:left; width:136px;
362 position:relative; float:left; width:136px;
363 }
363 }
364 div.list_head { padding:6px 8px 4px; border:solid #d9d8d1; border-width:1px 0px 0px; font-style:italic; }
364 div.list_head { padding:6px 8px 4px; border:solid #d9d8d1; border-width:1px 0px 0px; font-style:italic; }
365 a.list { text-decoration:none; color:#000000; }
365 a.list { text-decoration:none; color:#000000; }
366 a.list:hover { text-decoration:underline; color:#880000; }
366 a.list:hover { text-decoration:underline; color:#880000; }
367 table { padding:8px 4px; }
367 table { padding:8px 4px; }
368 th { padding:2px 5px; font-size:12px; text-align:left; }
368 th { padding:2px 5px; font-size:12px; text-align:left; }
369 tr.light:hover, .parity0:hover { background-color:#edece6; }
369 tr.light:hover, .parity0:hover { background-color:#edece6; }
370 tr.dark, .parity1 { background-color:#f6f6f0; }
370 tr.dark, .parity1 { background-color:#f6f6f0; }
371 tr.dark:hover, .parity1:hover { background-color:#edece6; }
371 tr.dark:hover, .parity1:hover { background-color:#edece6; }
372 td { padding:2px 5px; font-size:12px; vertical-align:top; }
372 td { padding:2px 5px; font-size:12px; vertical-align:top; }
373 td.closed { background-color: #99f; }
373 td.closed { background-color: #99f; }
374 td.link { padding:2px 5px; font-family:sans-serif; font-size:10px; }
374 td.link { padding:2px 5px; font-family:sans-serif; font-size:10px; }
375 td.indexlinks { white-space: nowrap; }
375 td.indexlinks { white-space: nowrap; }
376 td.indexlinks a {
376 td.indexlinks a {
377 padding: 2px 5px; line-height: 10px;
377 padding: 2px 5px; line-height: 10px;
378 border: 1px solid;
378 border: 1px solid;
379 color: #ffffff; background-color: #7777bb;
379 color: #ffffff; background-color: #7777bb;
380 border-color: #aaaadd #333366 #333366 #aaaadd;
380 border-color: #aaaadd #333366 #333366 #aaaadd;
381 font-weight: bold; text-align: center; text-decoration: none;
381 font-weight: bold; text-align: center; text-decoration: none;
382 font-size: 10px;
382 font-size: 10px;
383 }
383 }
384 td.indexlinks a:hover { background-color: #6666aa; }
384 td.indexlinks a:hover { background-color: #6666aa; }
385 div.pre { font-family:monospace; font-size:12px; white-space:pre; }
385 div.pre { font-family:monospace; font-size:12px; white-space:pre; }
386 div.diff_info { font-family:monospace; color:#000099; background-color:#edece6; font-style:italic; }
386 div.diff_info { font-family:monospace; color:#000099; background-color:#edece6; font-style:italic; }
387 div.index_include { border:solid #d9d8d1; border-width:0px 0px 1px; padding:12px 8px; }
387 div.index_include { border:solid #d9d8d1; border-width:0px 0px 1px; padding:12px 8px; }
388 div.search { margin:4px 8px; position:absolute; top:56px; right:12px }
388 div.search { margin:4px 8px; position:absolute; top:56px; right:12px }
389 .linenr { color:#999999; text-decoration:none }
389 .linenr { color:#999999; text-decoration:none }
390 div.rss_logo { float: right; white-space: nowrap; }
390 div.rss_logo { float: right; white-space: nowrap; }
391 div.rss_logo a {
391 div.rss_logo a {
392 padding:3px 6px; line-height:10px;
392 padding:3px 6px; line-height:10px;
393 border:1px solid; border-color:#fcc7a5 #7d3302 #3e1a01 #ff954e;
393 border:1px solid; border-color:#fcc7a5 #7d3302 #3e1a01 #ff954e;
394 color:#ffffff; background-color:#ff6600;
394 color:#ffffff; background-color:#ff6600;
395 font-weight:bold; font-family:sans-serif; font-size:10px;
395 font-weight:bold; font-family:sans-serif; font-size:10px;
396 text-align:center; text-decoration:none;
396 text-align:center; text-decoration:none;
397 }
397 }
398 div.rss_logo a:hover { background-color:#ee5500; }
398 div.rss_logo a:hover { background-color:#ee5500; }
399 pre { margin: 0; }
399 pre { margin: 0; }
400 span.logtags span {
400 span.logtags span {
401 padding: 0px 4px;
401 padding: 0px 4px;
402 font-size: 10px;
402 font-size: 10px;
403 font-weight: normal;
403 font-weight: normal;
404 border: 1px solid;
404 border: 1px solid;
405 background-color: #ffaaff;
405 background-color: #ffaaff;
406 border-color: #ffccff #ff00ee #ff00ee #ffccff;
406 border-color: #ffccff #ff00ee #ff00ee #ffccff;
407 }
407 }
408 span.logtags span.tagtag {
408 span.logtags span.tagtag {
409 background-color: #ffffaa;
409 background-color: #ffffaa;
410 border-color: #ffffcc #ffee00 #ffee00 #ffffcc;
410 border-color: #ffffcc #ffee00 #ffee00 #ffffcc;
411 }
411 }
412 span.logtags span.branchtag {
412 span.logtags span.branchtag {
413 background-color: #aaffaa;
413 background-color: #aaffaa;
414 border-color: #ccffcc #00cc33 #00cc33 #ccffcc;
414 border-color: #ccffcc #00cc33 #00cc33 #ccffcc;
415 }
415 }
416 span.logtags span.inbranchtag {
416 span.logtags span.inbranchtag {
417 background-color: #d5dde6;
417 background-color: #d5dde6;
418 border-color: #e3ecf4 #9398f4 #9398f4 #e3ecf4;
418 border-color: #e3ecf4 #9398f4 #9398f4 #e3ecf4;
419 }
419 }
420 span.logtags span.bookmarktag {
420 span.logtags span.bookmarktag {
421 background-color: #afdffa;
421 background-color: #afdffa;
422 border-color: #ccecff #46ace6 #46ace6 #ccecff;
422 border-color: #ccecff #46ace6 #46ace6 #ccecff;
423 }
423 }
424
424
425 /* Graph */
425 /* Graph */
426 div#wrapper {
426 div#wrapper {
427 position: relative;
427 position: relative;
428 margin: 0;
428 margin: 0;
429 padding: 0;
429 padding: 0;
430 margin-top: 3px;
430 margin-top: 3px;
431 }
431 }
432
432
433 canvas {
433 canvas {
434 position: absolute;
434 position: absolute;
435 z-index: 5;
435 z-index: 5;
436 top: -0.9em;
436 top: -0.9em;
437 margin: 0;
437 margin: 0;
438 }
438 }
439
439
440 ul#nodebgs {
440 ul#nodebgs {
441 list-style: none inside none;
441 list-style: none inside none;
442 padding: 0;
442 padding: 0;
443 margin: 0;
443 margin: 0;
444 top: -0.7em;
444 top: -0.7em;
445 }
445 }
446
446
447 ul#graphnodes li, ul#nodebgs li {
447 ul#graphnodes li, ul#nodebgs li {
448 height: 39px;
448 height: 39px;
449 }
449 }
450
450
451 ul#graphnodes {
451 ul#graphnodes {
452 position: absolute;
452 position: absolute;
453 z-index: 10;
453 z-index: 10;
454 top: -0.8em;
454 top: -0.8em;
455 list-style: none inside none;
455 list-style: none inside none;
456 padding: 0;
456 padding: 0;
457 }
457 }
458
458
459 ul#graphnodes li .info {
459 ul#graphnodes li .info {
460 display: block;
460 display: block;
461 font-size: 100%;
461 font-size: 100%;
462 position: relative;
462 position: relative;
463 top: -3px;
463 top: -3px;
464 font-style: italic;
464 font-style: italic;
465 }
465 }
466
466
467 /* Comparison */
467 /* Comparison */
468 .legend {
468 .legend {
469 padding: 1.5% 0 1.5% 0;
469 padding: 1.5% 0 1.5% 0;
470 }
470 }
471
471
472 .legendinfo {
472 .legendinfo {
473 border: 1px solid #d9d8d1;
473 border: 1px solid #d9d8d1;
474 font-size: 80%;
474 font-size: 80%;
475 text-align: center;
475 text-align: center;
476 padding: 0.5%;
476 padding: 0.5%;
477 }
477 }
478
478
479 .equal {
479 .equal {
480 background-color: #ffffff;
480 background-color: #ffffff;
481 }
481 }
482
482
483 .delete {
483 .delete {
484 background-color: #faa;
484 background-color: #faa;
485 color: #333;
485 color: #333;
486 }
486 }
487
487
488 .insert {
488 .insert {
489 background-color: #ffa;
489 background-color: #ffa;
490 }
490 }
491
491
492 .replace {
492 .replace {
493 background-color: #e8e8e8;
493 background-color: #e8e8e8;
494 }
494 }
495
495
496 .comparison {
496 .comparison {
497 overflow-x: auto;
497 overflow-x: auto;
498 }
498 }
499
499
500 .header th {
500 .header th {
501 text-align: center;
501 text-align: center;
502 }
502 }
503
503
504 .block {
504 .block {
505 border-top: 1px solid #d9d8d1;
505 border-top: 1px solid #d9d8d1;
506 }
506 }
507
507
508 .scroll-loading {
508 .scroll-loading {
509 -webkit-animation: change_color 1s linear 0s infinite alternate;
509 -webkit-animation: change_color 1s linear 0s infinite alternate;
510 -moz-animation: change_color 1s linear 0s infinite alternate;
510 -moz-animation: change_color 1s linear 0s infinite alternate;
511 -o-animation: change_color 1s linear 0s infinite alternate;
511 -o-animation: change_color 1s linear 0s infinite alternate;
512 animation: change_color 1s linear 0s infinite alternate;
512 animation: change_color 1s linear 0s infinite alternate;
513 }
513 }
514
514
515 @-webkit-keyframes change_color {
515 @-webkit-keyframes change_color {
516 from { background-color: #A0CEFF; } to { }
516 from { background-color: #A0CEFF; } to { }
517 }
517 }
518 @-moz-keyframes change_color {
518 @-moz-keyframes change_color {
519 from { background-color: #A0CEFF; } to { }
519 from { background-color: #A0CEFF; } to { }
520 }
520 }
521 @-o-keyframes change_color {
521 @-o-keyframes change_color {
522 from { background-color: #A0CEFF; } to { }
522 from { background-color: #A0CEFF; } to { }
523 }
523 }
524 @keyframes change_color {
524 @keyframes change_color {
525 from { background-color: #A0CEFF; } to { }
525 from { background-color: #A0CEFF; } to { }
526 }
526 }
527
527
528 .scroll-loading-error {
528 .scroll-loading-error {
529 background-color: #FFCCCC !important;
529 background-color: #FFCCCC !important;
530 }
530 }
531 304 Not Modified
531 304 Not Modified
532
532
533
533
534 phase changes are refreshed (issue4061)
535
536 $ echo bar >> foo
537 $ hg ci -msecret --secret
538 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'log?style=raw'
539 200 Script output follows
540
541
542 # HG changelog
543 # Node ID 2ef0ac749a14e4f57a5a822464a0902c6f7f448f
544
545 changeset: 2ef0ac749a14e4f57a5a822464a0902c6f7f448f
546 revision: 0
547 user: test
548 date: Thu, 01 Jan 1970 00:00:00 +0000
549 summary: base
550 branch: default
551 tag: tip
552
553
554 $ hg phase --draft tip
555 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT 'log?style=raw'
556 200 Script output follows
557
558
559 # HG changelog
560 # Node ID a084749e708a9c4c0a5b652a2a446322ce290e04
561
562 changeset: a084749e708a9c4c0a5b652a2a446322ce290e04
563 revision: 1
564 user: test
565 date: Thu, 01 Jan 1970 00:00:00 +0000
566 summary: secret
567 branch: default
568 tag: tip
569
570 changeset: 2ef0ac749a14e4f57a5a822464a0902c6f7f448f
571 revision: 0
572 user: test
573 date: Thu, 01 Jan 1970 00:00:00 +0000
574 summary: base
575
576
577
534 errors
578 errors
535
579
536 $ cat errors.log
580 $ cat errors.log
537
581
538 $ cd ..
582 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now