##// END OF EJS Templates
hgweb: Make get_mtime use repository to find store path....
Brendan Cully -
r10078:97c75ad3 default
parent child Browse files
Show More
@@ -1,157 +1,154 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, incorporated herein by reference.
7 # GNU General Public License version 2, incorporated herein by reference.
8
8
9 import errno, mimetypes, os
9 import errno, mimetypes, os
10
10
11 HTTP_OK = 200
11 HTTP_OK = 200
12 HTTP_BAD_REQUEST = 400
12 HTTP_BAD_REQUEST = 400
13 HTTP_UNAUTHORIZED = 401
13 HTTP_UNAUTHORIZED = 401
14 HTTP_FORBIDDEN = 403
14 HTTP_FORBIDDEN = 403
15 HTTP_NOT_FOUND = 404
15 HTTP_NOT_FOUND = 404
16 HTTP_METHOD_NOT_ALLOWED = 405
16 HTTP_METHOD_NOT_ALLOWED = 405
17 HTTP_SERVER_ERROR = 500
17 HTTP_SERVER_ERROR = 500
18
18
19 # Hooks for hgweb permission checks; extensions can add hooks here. Each hook
19 # Hooks for hgweb permission checks; extensions can add hooks here. Each hook
20 # is invoked like this: hook(hgweb, request, operation), where operation is
20 # is invoked like this: hook(hgweb, request, operation), where operation is
21 # either read, pull or push. Hooks should either raise an ErrorResponse
21 # either read, pull or push. Hooks should either raise an ErrorResponse
22 # exception, or just return.
22 # exception, or just return.
23 # It is possible to do both authentication and authorization through this.
23 # It is possible to do both authentication and authorization through this.
24 permhooks = []
24 permhooks = []
25
25
26 def checkauthz(hgweb, req, op):
26 def checkauthz(hgweb, req, op):
27 '''Check permission for operation based on request data (including
27 '''Check permission for operation based on request data (including
28 authentication info). Return if op allowed, else raise an ErrorResponse
28 authentication info). Return if op allowed, else raise an ErrorResponse
29 exception.'''
29 exception.'''
30
30
31 user = req.env.get('REMOTE_USER')
31 user = req.env.get('REMOTE_USER')
32
32
33 deny_read = hgweb.configlist('web', 'deny_read')
33 deny_read = hgweb.configlist('web', 'deny_read')
34 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
34 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
35 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
35 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
36
36
37 allow_read = hgweb.configlist('web', 'allow_read')
37 allow_read = hgweb.configlist('web', 'allow_read')
38 result = (not allow_read) or (allow_read == ['*'])
38 result = (not allow_read) or (allow_read == ['*'])
39 if not (result or user in allow_read):
39 if not (result or user in allow_read):
40 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
40 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
41
41
42 if op == 'pull' and not hgweb.allowpull:
42 if op == 'pull' and not hgweb.allowpull:
43 raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized')
43 raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized')
44 elif op == 'pull' or op is None: # op is None for interface requests
44 elif op == 'pull' or op is None: # op is None for interface requests
45 return
45 return
46
46
47 # enforce that you can only push using POST requests
47 # enforce that you can only push using POST requests
48 if req.env['REQUEST_METHOD'] != 'POST':
48 if req.env['REQUEST_METHOD'] != 'POST':
49 msg = 'push requires POST request'
49 msg = 'push requires POST request'
50 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
50 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
51
51
52 # require ssl by default for pushing, auth info cannot be sniffed
52 # require ssl by default for pushing, auth info cannot be sniffed
53 # and replayed
53 # and replayed
54 scheme = req.env.get('wsgi.url_scheme')
54 scheme = req.env.get('wsgi.url_scheme')
55 if hgweb.configbool('web', 'push_ssl', True) and scheme != 'https':
55 if hgweb.configbool('web', 'push_ssl', True) and scheme != 'https':
56 raise ErrorResponse(HTTP_OK, 'ssl required')
56 raise ErrorResponse(HTTP_OK, 'ssl required')
57
57
58 deny = hgweb.configlist('web', 'deny_push')
58 deny = hgweb.configlist('web', 'deny_push')
59 if deny and (not user or deny == ['*'] or user in deny):
59 if deny and (not user or deny == ['*'] or user in deny):
60 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
60 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
61
61
62 allow = hgweb.configlist('web', 'allow_push')
62 allow = hgweb.configlist('web', 'allow_push')
63 result = allow and (allow == ['*'] or user in allow)
63 result = allow and (allow == ['*'] or user in allow)
64 if not result:
64 if not result:
65 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
65 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
66
66
67 # Add the default permhook, which provides simple authorization.
67 # Add the default permhook, which provides simple authorization.
68 permhooks.append(checkauthz)
68 permhooks.append(checkauthz)
69
69
70
70
71 class ErrorResponse(Exception):
71 class ErrorResponse(Exception):
72 def __init__(self, code, message=None, headers=[]):
72 def __init__(self, code, message=None, headers=[]):
73 Exception.__init__(self)
73 Exception.__init__(self)
74 self.code = code
74 self.code = code
75 self.headers = headers
75 self.headers = headers
76 if message is not None:
76 if message is not None:
77 self.message = message
77 self.message = message
78 else:
78 else:
79 self.message = _statusmessage(code)
79 self.message = _statusmessage(code)
80
80
81 def _statusmessage(code):
81 def _statusmessage(code):
82 from BaseHTTPServer import BaseHTTPRequestHandler
82 from BaseHTTPServer import BaseHTTPRequestHandler
83 responses = BaseHTTPRequestHandler.responses
83 responses = BaseHTTPRequestHandler.responses
84 return responses.get(code, ('Error', 'Unknown error'))[0]
84 return responses.get(code, ('Error', 'Unknown error'))[0]
85
85
86 def statusmessage(code, message=None):
86 def statusmessage(code, message=None):
87 return '%d %s' % (code, message or _statusmessage(code))
87 return '%d %s' % (code, message or _statusmessage(code))
88
88
89 def get_mtime(repo_path):
89 def get_mtime(spath):
90 store_path = os.path.join(repo_path, ".hg")
90 cl_path = os.path.join(spath, "00changelog.i")
91 if not os.path.isdir(os.path.join(store_path, "data")):
92 store_path = os.path.join(store_path, "store")
93 cl_path = os.path.join(store_path, "00changelog.i")
94 if os.path.exists(cl_path):
91 if os.path.exists(cl_path):
95 return os.stat(cl_path).st_mtime
92 return os.stat(cl_path).st_mtime
96 else:
93 else:
97 return os.stat(store_path).st_mtime
94 return os.stat(spath).st_mtime
98
95
99 def staticfile(directory, fname, req):
96 def staticfile(directory, fname, req):
100 """return a file inside directory with guessed Content-Type header
97 """return a file inside directory with guessed Content-Type header
101
98
102 fname always uses '/' as directory separator and isn't allowed to
99 fname always uses '/' as directory separator and isn't allowed to
103 contain unusual path components.
100 contain unusual path components.
104 Content-Type is guessed using the mimetypes module.
101 Content-Type is guessed using the mimetypes module.
105 Return an empty string if fname is illegal or file not found.
102 Return an empty string if fname is illegal or file not found.
106
103
107 """
104 """
108 parts = fname.split('/')
105 parts = fname.split('/')
109 for part in parts:
106 for part in parts:
110 if (part in ('', os.curdir, os.pardir) or
107 if (part in ('', os.curdir, os.pardir) or
111 os.sep in part or os.altsep is not None and os.altsep in part):
108 os.sep in part or os.altsep is not None and os.altsep in part):
112 return ""
109 return ""
113 fpath = os.path.join(*parts)
110 fpath = os.path.join(*parts)
114 if isinstance(directory, str):
111 if isinstance(directory, str):
115 directory = [directory]
112 directory = [directory]
116 for d in directory:
113 for d in directory:
117 path = os.path.join(d, fpath)
114 path = os.path.join(d, fpath)
118 if os.path.exists(path):
115 if os.path.exists(path):
119 break
116 break
120 try:
117 try:
121 os.stat(path)
118 os.stat(path)
122 ct = mimetypes.guess_type(path)[0] or "text/plain"
119 ct = mimetypes.guess_type(path)[0] or "text/plain"
123 req.respond(HTTP_OK, ct, length = os.path.getsize(path))
120 req.respond(HTTP_OK, ct, length = os.path.getsize(path))
124 return open(path, 'rb').read()
121 return open(path, 'rb').read()
125 except TypeError:
122 except TypeError:
126 raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal filename')
123 raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal filename')
127 except OSError, err:
124 except OSError, err:
128 if err.errno == errno.ENOENT:
125 if err.errno == errno.ENOENT:
129 raise ErrorResponse(HTTP_NOT_FOUND)
126 raise ErrorResponse(HTTP_NOT_FOUND)
130 else:
127 else:
131 raise ErrorResponse(HTTP_SERVER_ERROR, err.strerror)
128 raise ErrorResponse(HTTP_SERVER_ERROR, err.strerror)
132
129
133 def paritygen(stripecount, offset=0):
130 def paritygen(stripecount, offset=0):
134 """count parity of horizontal stripes for easier reading"""
131 """count parity of horizontal stripes for easier reading"""
135 if stripecount and offset:
132 if stripecount and offset:
136 # account for offset, e.g. due to building the list in reverse
133 # account for offset, e.g. due to building the list in reverse
137 count = (stripecount + offset) % stripecount
134 count = (stripecount + offset) % stripecount
138 parity = (stripecount + offset) / stripecount & 1
135 parity = (stripecount + offset) / stripecount & 1
139 else:
136 else:
140 count = 0
137 count = 0
141 parity = 0
138 parity = 0
142 while True:
139 while True:
143 yield parity
140 yield parity
144 count += 1
141 count += 1
145 if stripecount and count >= stripecount:
142 if stripecount and count >= stripecount:
146 parity = 1 - parity
143 parity = 1 - parity
147 count = 0
144 count = 0
148
145
149 def get_contact(config):
146 def get_contact(config):
150 """Return repo contact information or empty string.
147 """Return repo contact information or empty string.
151
148
152 web.contact is the primary source, but if that is not set, try
149 web.contact is the primary source, but if that is not set, try
153 ui.username or $EMAIL as a fallback to display something useful.
150 ui.username or $EMAIL as a fallback to display something useful.
154 """
151 """
155 return (config("web", "contact") or
152 return (config("web", "contact") or
156 config("ui", "username") or
153 config("ui", "username") or
157 os.environ.get("EMAIL") or "")
154 os.environ.get("EMAIL") or "")
@@ -1,287 +1,287 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, incorporated herein by reference.
7 # GNU General Public License version 2, incorporated herein by reference.
8
8
9 import os
9 import os
10 from mercurial import ui, hg, hook, error, encoding, templater
10 from mercurial import ui, hg, hook, error, encoding, templater
11 from common import get_mtime, ErrorResponse, permhooks
11 from common import get_mtime, ErrorResponse, permhooks
12 from common import HTTP_OK, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
12 from common import HTTP_OK, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
13 from common import HTTP_UNAUTHORIZED, HTTP_METHOD_NOT_ALLOWED
13 from common import HTTP_UNAUTHORIZED, HTTP_METHOD_NOT_ALLOWED
14 from request import wsgirequest
14 from request import wsgirequest
15 import webcommands, protocol, webutil
15 import webcommands, protocol, webutil
16
16
17 perms = {
17 perms = {
18 'changegroup': 'pull',
18 'changegroup': 'pull',
19 'changegroupsubset': 'pull',
19 'changegroupsubset': 'pull',
20 'unbundle': 'push',
20 'unbundle': 'push',
21 'stream_out': 'pull',
21 'stream_out': 'pull',
22 }
22 }
23
23
24 class hgweb(object):
24 class hgweb(object):
25 def __init__(self, repo, name=None):
25 def __init__(self, repo, name=None):
26 if isinstance(repo, str):
26 if isinstance(repo, str):
27 u = ui.ui()
27 u = ui.ui()
28 u.setconfig('ui', 'report_untrusted', 'off')
28 u.setconfig('ui', 'report_untrusted', 'off')
29 u.setconfig('ui', 'interactive', 'off')
29 u.setconfig('ui', 'interactive', 'off')
30 self.repo = hg.repository(u, repo)
30 self.repo = hg.repository(u, repo)
31 else:
31 else:
32 self.repo = repo
32 self.repo = repo
33
33
34 hook.redirect(True)
34 hook.redirect(True)
35 self.mtime = -1
35 self.mtime = -1
36 self.reponame = name
36 self.reponame = name
37 self.archives = 'zip', 'gz', 'bz2'
37 self.archives = 'zip', 'gz', 'bz2'
38 self.stripecount = 1
38 self.stripecount = 1
39 # a repo owner may set web.templates in .hg/hgrc to get any file
39 # a repo owner may set web.templates in .hg/hgrc to get any file
40 # readable by the user running the CGI script
40 # readable by the user running the CGI script
41 self.templatepath = self.config('web', 'templates')
41 self.templatepath = self.config('web', 'templates')
42
42
43 # The CGI scripts are often run by a user different from the repo owner.
43 # The CGI scripts are often run by a user different from the repo owner.
44 # Trust the settings from the .hg/hgrc files by default.
44 # Trust the settings from the .hg/hgrc files by default.
45 def config(self, section, name, default=None, untrusted=True):
45 def config(self, section, name, default=None, untrusted=True):
46 return self.repo.ui.config(section, name, default,
46 return self.repo.ui.config(section, name, default,
47 untrusted=untrusted)
47 untrusted=untrusted)
48
48
49 def configbool(self, section, name, default=False, untrusted=True):
49 def configbool(self, section, name, default=False, untrusted=True):
50 return self.repo.ui.configbool(section, name, default,
50 return self.repo.ui.configbool(section, name, default,
51 untrusted=untrusted)
51 untrusted=untrusted)
52
52
53 def configlist(self, section, name, default=None, untrusted=True):
53 def configlist(self, section, name, default=None, untrusted=True):
54 return self.repo.ui.configlist(section, name, default,
54 return self.repo.ui.configlist(section, name, default,
55 untrusted=untrusted)
55 untrusted=untrusted)
56
56
57 def refresh(self, request=None):
57 def refresh(self, request=None):
58 if request:
58 if request:
59 self.repo.ui.environ = request.env
59 self.repo.ui.environ = request.env
60 mtime = get_mtime(self.repo.root)
60 mtime = get_mtime(self.repo.spath)
61 if mtime != self.mtime:
61 if mtime != self.mtime:
62 self.mtime = mtime
62 self.mtime = mtime
63 self.repo = hg.repository(self.repo.ui, self.repo.root)
63 self.repo = hg.repository(self.repo.ui, self.repo.root)
64 self.maxchanges = int(self.config("web", "maxchanges", 10))
64 self.maxchanges = int(self.config("web", "maxchanges", 10))
65 self.stripecount = int(self.config("web", "stripes", 1))
65 self.stripecount = int(self.config("web", "stripes", 1))
66 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
66 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
67 self.maxfiles = int(self.config("web", "maxfiles", 10))
67 self.maxfiles = int(self.config("web", "maxfiles", 10))
68 self.allowpull = self.configbool("web", "allowpull", True)
68 self.allowpull = self.configbool("web", "allowpull", True)
69 encoding.encoding = self.config("web", "encoding",
69 encoding.encoding = self.config("web", "encoding",
70 encoding.encoding)
70 encoding.encoding)
71
71
72 def run(self):
72 def run(self):
73 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
73 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
74 raise RuntimeError("This function is only intended to be "
74 raise RuntimeError("This function is only intended to be "
75 "called while running as a CGI script.")
75 "called while running as a CGI script.")
76 import mercurial.hgweb.wsgicgi as wsgicgi
76 import mercurial.hgweb.wsgicgi as wsgicgi
77 wsgicgi.launch(self)
77 wsgicgi.launch(self)
78
78
79 def __call__(self, env, respond):
79 def __call__(self, env, respond):
80 req = wsgirequest(env, respond)
80 req = wsgirequest(env, respond)
81 return self.run_wsgi(req)
81 return self.run_wsgi(req)
82
82
83 def run_wsgi(self, req):
83 def run_wsgi(self, req):
84
84
85 self.refresh(req)
85 self.refresh(req)
86
86
87 # work with CGI variables to create coherent structure
87 # work with CGI variables to create coherent structure
88 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
88 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
89
89
90 req.url = req.env['SCRIPT_NAME']
90 req.url = req.env['SCRIPT_NAME']
91 if not req.url.endswith('/'):
91 if not req.url.endswith('/'):
92 req.url += '/'
92 req.url += '/'
93 if 'REPO_NAME' in req.env:
93 if 'REPO_NAME' in req.env:
94 req.url += req.env['REPO_NAME'] + '/'
94 req.url += req.env['REPO_NAME'] + '/'
95
95
96 if 'PATH_INFO' in req.env:
96 if 'PATH_INFO' in req.env:
97 parts = req.env['PATH_INFO'].strip('/').split('/')
97 parts = req.env['PATH_INFO'].strip('/').split('/')
98 repo_parts = req.env.get('REPO_NAME', '').split('/')
98 repo_parts = req.env.get('REPO_NAME', '').split('/')
99 if parts[:len(repo_parts)] == repo_parts:
99 if parts[:len(repo_parts)] == repo_parts:
100 parts = parts[len(repo_parts):]
100 parts = parts[len(repo_parts):]
101 query = '/'.join(parts)
101 query = '/'.join(parts)
102 else:
102 else:
103 query = req.env['QUERY_STRING'].split('&', 1)[0]
103 query = req.env['QUERY_STRING'].split('&', 1)[0]
104 query = query.split(';', 1)[0]
104 query = query.split(';', 1)[0]
105
105
106 # process this if it's a protocol request
106 # process this if it's a protocol request
107 # protocol bits don't need to create any URLs
107 # protocol bits don't need to create any URLs
108 # and the clients always use the old URL structure
108 # and the clients always use the old URL structure
109
109
110 cmd = req.form.get('cmd', [''])[0]
110 cmd = req.form.get('cmd', [''])[0]
111 if cmd and cmd in protocol.__all__:
111 if cmd and cmd in protocol.__all__:
112 if query:
112 if query:
113 raise ErrorResponse(HTTP_NOT_FOUND)
113 raise ErrorResponse(HTTP_NOT_FOUND)
114 try:
114 try:
115 if cmd in perms:
115 if cmd in perms:
116 try:
116 try:
117 self.check_perm(req, perms[cmd])
117 self.check_perm(req, perms[cmd])
118 except ErrorResponse, inst:
118 except ErrorResponse, inst:
119 if cmd == 'unbundle':
119 if cmd == 'unbundle':
120 req.drain()
120 req.drain()
121 raise
121 raise
122 method = getattr(protocol, cmd)
122 method = getattr(protocol, cmd)
123 return method(self.repo, req)
123 return method(self.repo, req)
124 except ErrorResponse, inst:
124 except ErrorResponse, inst:
125 req.respond(inst, protocol.HGTYPE)
125 req.respond(inst, protocol.HGTYPE)
126 if not inst.message:
126 if not inst.message:
127 return []
127 return []
128 return '0\n%s\n' % inst.message,
128 return '0\n%s\n' % inst.message,
129
129
130 # translate user-visible url structure to internal structure
130 # translate user-visible url structure to internal structure
131
131
132 args = query.split('/', 2)
132 args = query.split('/', 2)
133 if 'cmd' not in req.form and args and args[0]:
133 if 'cmd' not in req.form and args and args[0]:
134
134
135 cmd = args.pop(0)
135 cmd = args.pop(0)
136 style = cmd.rfind('-')
136 style = cmd.rfind('-')
137 if style != -1:
137 if style != -1:
138 req.form['style'] = [cmd[:style]]
138 req.form['style'] = [cmd[:style]]
139 cmd = cmd[style+1:]
139 cmd = cmd[style+1:]
140
140
141 # avoid accepting e.g. style parameter as command
141 # avoid accepting e.g. style parameter as command
142 if hasattr(webcommands, cmd):
142 if hasattr(webcommands, cmd):
143 req.form['cmd'] = [cmd]
143 req.form['cmd'] = [cmd]
144 else:
144 else:
145 cmd = ''
145 cmd = ''
146
146
147 if cmd == 'static':
147 if cmd == 'static':
148 req.form['file'] = ['/'.join(args)]
148 req.form['file'] = ['/'.join(args)]
149 else:
149 else:
150 if args and args[0]:
150 if args and args[0]:
151 node = args.pop(0)
151 node = args.pop(0)
152 req.form['node'] = [node]
152 req.form['node'] = [node]
153 if args:
153 if args:
154 req.form['file'] = args
154 req.form['file'] = args
155
155
156 ua = req.env.get('HTTP_USER_AGENT', '')
156 ua = req.env.get('HTTP_USER_AGENT', '')
157 if cmd == 'rev' and 'mercurial' in ua:
157 if cmd == 'rev' and 'mercurial' in ua:
158 req.form['style'] = ['raw']
158 req.form['style'] = ['raw']
159
159
160 if cmd == 'archive':
160 if cmd == 'archive':
161 fn = req.form['node'][0]
161 fn = req.form['node'][0]
162 for type_, spec in self.archive_specs.iteritems():
162 for type_, spec in self.archive_specs.iteritems():
163 ext = spec[2]
163 ext = spec[2]
164 if fn.endswith(ext):
164 if fn.endswith(ext):
165 req.form['node'] = [fn[:-len(ext)]]
165 req.form['node'] = [fn[:-len(ext)]]
166 req.form['type'] = [type_]
166 req.form['type'] = [type_]
167
167
168 # process the web interface request
168 # process the web interface request
169
169
170 try:
170 try:
171 tmpl = self.templater(req)
171 tmpl = self.templater(req)
172 ctype = tmpl('mimetype', encoding=encoding.encoding)
172 ctype = tmpl('mimetype', encoding=encoding.encoding)
173 ctype = templater.stringify(ctype)
173 ctype = templater.stringify(ctype)
174
174
175 # check read permissions non-static content
175 # check read permissions non-static content
176 if cmd != 'static':
176 if cmd != 'static':
177 self.check_perm(req, None)
177 self.check_perm(req, None)
178
178
179 if cmd == '':
179 if cmd == '':
180 req.form['cmd'] = [tmpl.cache['default']]
180 req.form['cmd'] = [tmpl.cache['default']]
181 cmd = req.form['cmd'][0]
181 cmd = req.form['cmd'][0]
182
182
183 if cmd not in webcommands.__all__:
183 if cmd not in webcommands.__all__:
184 msg = 'no such method: %s' % cmd
184 msg = 'no such method: %s' % cmd
185 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
185 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
186 elif cmd == 'file' and 'raw' in req.form.get('style', []):
186 elif cmd == 'file' and 'raw' in req.form.get('style', []):
187 self.ctype = ctype
187 self.ctype = ctype
188 content = webcommands.rawfile(self, req, tmpl)
188 content = webcommands.rawfile(self, req, tmpl)
189 else:
189 else:
190 content = getattr(webcommands, cmd)(self, req, tmpl)
190 content = getattr(webcommands, cmd)(self, req, tmpl)
191 req.respond(HTTP_OK, ctype)
191 req.respond(HTTP_OK, ctype)
192
192
193 return content
193 return content
194
194
195 except error.LookupError, err:
195 except error.LookupError, err:
196 req.respond(HTTP_NOT_FOUND, ctype)
196 req.respond(HTTP_NOT_FOUND, ctype)
197 msg = str(err)
197 msg = str(err)
198 if 'manifest' not in msg:
198 if 'manifest' not in msg:
199 msg = 'revision not found: %s' % err.name
199 msg = 'revision not found: %s' % err.name
200 return tmpl('error', error=msg)
200 return tmpl('error', error=msg)
201 except (error.RepoError, error.RevlogError), inst:
201 except (error.RepoError, error.RevlogError), inst:
202 req.respond(HTTP_SERVER_ERROR, ctype)
202 req.respond(HTTP_SERVER_ERROR, ctype)
203 return tmpl('error', error=str(inst))
203 return tmpl('error', error=str(inst))
204 except ErrorResponse, inst:
204 except ErrorResponse, inst:
205 req.respond(inst, ctype)
205 req.respond(inst, ctype)
206 return tmpl('error', error=inst.message)
206 return tmpl('error', error=inst.message)
207
207
208 def templater(self, req):
208 def templater(self, req):
209
209
210 # determine scheme, port and server name
210 # determine scheme, port and server name
211 # this is needed to create absolute urls
211 # this is needed to create absolute urls
212
212
213 proto = req.env.get('wsgi.url_scheme')
213 proto = req.env.get('wsgi.url_scheme')
214 if proto == 'https':
214 if proto == 'https':
215 proto = 'https'
215 proto = 'https'
216 default_port = "443"
216 default_port = "443"
217 else:
217 else:
218 proto = 'http'
218 proto = 'http'
219 default_port = "80"
219 default_port = "80"
220
220
221 port = req.env["SERVER_PORT"]
221 port = req.env["SERVER_PORT"]
222 port = port != default_port and (":" + port) or ""
222 port = port != default_port and (":" + port) or ""
223 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
223 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
224 staticurl = self.config("web", "staticurl") or req.url + 'static/'
224 staticurl = self.config("web", "staticurl") or req.url + 'static/'
225 if not staticurl.endswith('/'):
225 if not staticurl.endswith('/'):
226 staticurl += '/'
226 staticurl += '/'
227
227
228 # some functions for the templater
228 # some functions for the templater
229
229
230 def header(**map):
230 def header(**map):
231 yield tmpl('header', encoding=encoding.encoding, **map)
231 yield tmpl('header', encoding=encoding.encoding, **map)
232
232
233 def footer(**map):
233 def footer(**map):
234 yield tmpl("footer", **map)
234 yield tmpl("footer", **map)
235
235
236 def motd(**map):
236 def motd(**map):
237 yield self.config("web", "motd", "")
237 yield self.config("web", "motd", "")
238
238
239 # figure out which style to use
239 # figure out which style to use
240
240
241 vars = {}
241 vars = {}
242 styles = (
242 styles = (
243 req.form.get('style', [None])[0],
243 req.form.get('style', [None])[0],
244 self.config('web', 'style'),
244 self.config('web', 'style'),
245 'paper',
245 'paper',
246 )
246 )
247 style, mapfile = templater.stylemap(styles, self.templatepath)
247 style, mapfile = templater.stylemap(styles, self.templatepath)
248 if style == styles[0]:
248 if style == styles[0]:
249 vars['style'] = style
249 vars['style'] = style
250
250
251 start = req.url[-1] == '?' and '&' or '?'
251 start = req.url[-1] == '?' and '&' or '?'
252 sessionvars = webutil.sessionvars(vars, start)
252 sessionvars = webutil.sessionvars(vars, start)
253
253
254 if not self.reponame:
254 if not self.reponame:
255 self.reponame = (self.config("web", "name")
255 self.reponame = (self.config("web", "name")
256 or req.env.get('REPO_NAME')
256 or req.env.get('REPO_NAME')
257 or req.url.strip('/') or self.repo.root)
257 or req.url.strip('/') or self.repo.root)
258
258
259 # create the templater
259 # create the templater
260
260
261 tmpl = templater.templater(mapfile,
261 tmpl = templater.templater(mapfile,
262 defaults={"url": req.url,
262 defaults={"url": req.url,
263 "staticurl": staticurl,
263 "staticurl": staticurl,
264 "urlbase": urlbase,
264 "urlbase": urlbase,
265 "repo": self.reponame,
265 "repo": self.reponame,
266 "header": header,
266 "header": header,
267 "footer": footer,
267 "footer": footer,
268 "motd": motd,
268 "motd": motd,
269 "sessionvars": sessionvars
269 "sessionvars": sessionvars
270 })
270 })
271 return tmpl
271 return tmpl
272
272
273 def archivelist(self, nodeid):
273 def archivelist(self, nodeid):
274 allowed = self.configlist("web", "allow_archive")
274 allowed = self.configlist("web", "allow_archive")
275 for i, spec in self.archive_specs.iteritems():
275 for i, spec in self.archive_specs.iteritems():
276 if i in allowed or self.configbool("web", "allow" + i):
276 if i in allowed or self.configbool("web", "allow" + i):
277 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
277 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
278
278
279 archive_specs = {
279 archive_specs = {
280 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
280 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
281 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
281 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
282 'zip': ('application/zip', 'zip', '.zip', None),
282 'zip': ('application/zip', 'zip', '.zip', None),
283 }
283 }
284
284
285 def check_perm(self, req, op):
285 def check_perm(self, req, op):
286 for hook in permhooks:
286 for hook in permhooks:
287 hook(self, req, op)
287 hook(self, req, op)
@@ -1,340 +1,341 b''
1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
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, incorporated herein by reference.
7 # GNU General Public License version 2, incorporated herein by reference.
8
8
9 import os, re, time
9 import os, re, time
10 from mercurial.i18n import _
10 from mercurial.i18n import _
11 from mercurial import ui, hg, util, templater
11 from mercurial import ui, hg, util, templater
12 from mercurial import error, encoding
12 from mercurial import error, encoding
13 from common import ErrorResponse, get_mtime, staticfile, paritygen,\
13 from common import ErrorResponse, get_mtime, staticfile, paritygen,\
14 get_contact, HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
14 get_contact, HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
15 from hgweb_mod import hgweb
15 from hgweb_mod import hgweb
16 from request import wsgirequest
16 from request import wsgirequest
17 import webutil
17 import webutil
18
18
19 def cleannames(items):
19 def cleannames(items):
20 return [(util.pconvert(name).strip('/'), path) for name, path in items]
20 return [(util.pconvert(name).strip('/'), path) for name, path in items]
21
21
22 def findrepos(paths):
22 def findrepos(paths):
23 repos = []
23 repos = []
24 for prefix, root in cleannames(paths):
24 for prefix, root in cleannames(paths):
25 roothead, roottail = os.path.split(root)
25 roothead, roottail = os.path.split(root)
26 # "foo = /bar/*" makes every subrepo of /bar/ to be
26 # "foo = /bar/*" makes every subrepo of /bar/ to be
27 # mounted as foo/subrepo
27 # mounted as foo/subrepo
28 # and "foo = /bar/**" also recurses into the subdirectories,
28 # and "foo = /bar/**" also recurses into the subdirectories,
29 # remember to use it without working dir.
29 # remember to use it without working dir.
30 try:
30 try:
31 recurse = {'*': False, '**': True}[roottail]
31 recurse = {'*': False, '**': True}[roottail]
32 except KeyError:
32 except KeyError:
33 repos.append((prefix, root))
33 repos.append((prefix, root))
34 continue
34 continue
35 roothead = os.path.normpath(roothead)
35 roothead = os.path.normpath(roothead)
36 for path in util.walkrepos(roothead, followsym=True, recurse=recurse):
36 for path in util.walkrepos(roothead, followsym=True, recurse=recurse):
37 path = os.path.normpath(path)
37 path = os.path.normpath(path)
38 name = util.pconvert(path[len(roothead):]).strip('/')
38 name = util.pconvert(path[len(roothead):]).strip('/')
39 if prefix:
39 if prefix:
40 name = prefix + '/' + name
40 name = prefix + '/' + name
41 repos.append((name, path))
41 repos.append((name, path))
42 return repos
42 return repos
43
43
44 class hgwebdir(object):
44 class hgwebdir(object):
45 refreshinterval = 20
45 refreshinterval = 20
46
46
47 def __init__(self, conf, baseui=None):
47 def __init__(self, conf, baseui=None):
48 self.conf = conf
48 self.conf = conf
49 self.baseui = baseui
49 self.baseui = baseui
50 self.lastrefresh = 0
50 self.lastrefresh = 0
51 self.motd = None
51 self.motd = None
52 self.refresh()
52 self.refresh()
53
53
54 def refresh(self):
54 def refresh(self):
55 if self.lastrefresh + self.refreshinterval > time.time():
55 if self.lastrefresh + self.refreshinterval > time.time():
56 return
56 return
57
57
58 if self.baseui:
58 if self.baseui:
59 self.ui = self.baseui.copy()
59 self.ui = self.baseui.copy()
60 else:
60 else:
61 self.ui = ui.ui()
61 self.ui = ui.ui()
62 self.ui.setconfig('ui', 'report_untrusted', 'off')
62 self.ui.setconfig('ui', 'report_untrusted', 'off')
63 self.ui.setconfig('ui', 'interactive', 'off')
63 self.ui.setconfig('ui', 'interactive', 'off')
64
64
65 if not isinstance(self.conf, (dict, list, tuple)):
65 if not isinstance(self.conf, (dict, list, tuple)):
66 map = {'paths': 'hgweb-paths'}
66 map = {'paths': 'hgweb-paths'}
67 self.ui.readconfig(self.conf, remap=map, trust=True)
67 self.ui.readconfig(self.conf, remap=map, trust=True)
68 paths = self.ui.configitems('hgweb-paths')
68 paths = self.ui.configitems('hgweb-paths')
69 elif isinstance(self.conf, (list, tuple)):
69 elif isinstance(self.conf, (list, tuple)):
70 paths = self.conf
70 paths = self.conf
71 elif isinstance(self.conf, dict):
71 elif isinstance(self.conf, dict):
72 paths = self.conf.items()
72 paths = self.conf.items()
73
73
74 encoding.encoding = self.ui.config('web', 'encoding',
74 encoding.encoding = self.ui.config('web', 'encoding',
75 encoding.encoding)
75 encoding.encoding)
76 self.style = self.ui.config('web', 'style', 'paper')
76 self.style = self.ui.config('web', 'style', 'paper')
77 self.stripecount = self.ui.config('web', 'stripes', 1)
77 self.stripecount = self.ui.config('web', 'stripes', 1)
78 if self.stripecount:
78 if self.stripecount:
79 self.stripecount = int(self.stripecount)
79 self.stripecount = int(self.stripecount)
80 self._baseurl = self.ui.config('web', 'baseurl')
80 self._baseurl = self.ui.config('web', 'baseurl')
81
81
82 self.repos = findrepos(paths)
82 self.repos = findrepos(paths)
83 for prefix, root in self.ui.configitems('collections'):
83 for prefix, root in self.ui.configitems('collections'):
84 prefix = util.pconvert(prefix)
84 prefix = util.pconvert(prefix)
85 for path in util.walkrepos(root, followsym=True):
85 for path in util.walkrepos(root, followsym=True):
86 repo = os.path.normpath(path)
86 repo = os.path.normpath(path)
87 name = util.pconvert(repo)
87 name = util.pconvert(repo)
88 if name.startswith(prefix):
88 if name.startswith(prefix):
89 name = name[len(prefix):]
89 name = name[len(prefix):]
90 self.repos.append((name.lstrip('/'), repo))
90 self.repos.append((name.lstrip('/'), repo))
91
91
92 self.lastrefresh = time.time()
92 self.lastrefresh = time.time()
93
93
94 def run(self):
94 def run(self):
95 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
95 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
96 raise RuntimeError("This function is only intended to be "
96 raise RuntimeError("This function is only intended to be "
97 "called while running as a CGI script.")
97 "called while running as a CGI script.")
98 import mercurial.hgweb.wsgicgi as wsgicgi
98 import mercurial.hgweb.wsgicgi as wsgicgi
99 wsgicgi.launch(self)
99 wsgicgi.launch(self)
100
100
101 def __call__(self, env, respond):
101 def __call__(self, env, respond):
102 req = wsgirequest(env, respond)
102 req = wsgirequest(env, respond)
103 return self.run_wsgi(req)
103 return self.run_wsgi(req)
104
104
105 def read_allowed(self, ui, req):
105 def read_allowed(self, ui, req):
106 """Check allow_read and deny_read config options of a repo's ui object
106 """Check allow_read and deny_read config options of a repo's ui object
107 to determine user permissions. By default, with neither option set (or
107 to determine user permissions. By default, with neither option set (or
108 both empty), allow all users to read the repo. There are two ways a
108 both empty), allow all users to read the repo. There are two ways a
109 user can be denied read access: (1) deny_read is not empty, and the
109 user can be denied read access: (1) deny_read is not empty, and the
110 user is unauthenticated or deny_read contains user (or *), and (2)
110 user is unauthenticated or deny_read contains user (or *), and (2)
111 allow_read is not empty and the user is not in allow_read. Return True
111 allow_read is not empty and the user is not in allow_read. Return True
112 if user is allowed to read the repo, else return False."""
112 if user is allowed to read the repo, else return False."""
113
113
114 user = req.env.get('REMOTE_USER')
114 user = req.env.get('REMOTE_USER')
115
115
116 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
116 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
117 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
117 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
118 return False
118 return False
119
119
120 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
120 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
121 # by default, allow reading if no allow_read option has been set
121 # by default, allow reading if no allow_read option has been set
122 if (not allow_read) or (allow_read == ['*']) or (user in allow_read):
122 if (not allow_read) or (allow_read == ['*']) or (user in allow_read):
123 return True
123 return True
124
124
125 return False
125 return False
126
126
127 def run_wsgi(self, req):
127 def run_wsgi(self, req):
128 try:
128 try:
129 try:
129 try:
130 self.refresh()
130 self.refresh()
131
131
132 virtual = req.env.get("PATH_INFO", "").strip('/')
132 virtual = req.env.get("PATH_INFO", "").strip('/')
133 tmpl = self.templater(req)
133 tmpl = self.templater(req)
134 ctype = tmpl('mimetype', encoding=encoding.encoding)
134 ctype = tmpl('mimetype', encoding=encoding.encoding)
135 ctype = templater.stringify(ctype)
135 ctype = templater.stringify(ctype)
136
136
137 # a static file
137 # a static file
138 if virtual.startswith('static/') or 'static' in req.form:
138 if virtual.startswith('static/') or 'static' in req.form:
139 if virtual.startswith('static/'):
139 if virtual.startswith('static/'):
140 fname = virtual[7:]
140 fname = virtual[7:]
141 else:
141 else:
142 fname = req.form['static'][0]
142 fname = req.form['static'][0]
143 static = templater.templatepath('static')
143 static = templater.templatepath('static')
144 return (staticfile(static, fname, req),)
144 return (staticfile(static, fname, req),)
145
145
146 # top-level index
146 # top-level index
147 elif not virtual:
147 elif not virtual:
148 req.respond(HTTP_OK, ctype)
148 req.respond(HTTP_OK, ctype)
149 return self.makeindex(req, tmpl)
149 return self.makeindex(req, tmpl)
150
150
151 # nested indexes and hgwebs
151 # nested indexes and hgwebs
152
152
153 repos = dict(self.repos)
153 repos = dict(self.repos)
154 while virtual:
154 while virtual:
155 real = repos.get(virtual)
155 real = repos.get(virtual)
156 if real:
156 if real:
157 req.env['REPO_NAME'] = virtual
157 req.env['REPO_NAME'] = virtual
158 try:
158 try:
159 repo = hg.repository(self.ui, real)
159 repo = hg.repository(self.ui, real)
160 return hgweb(repo).run_wsgi(req)
160 return hgweb(repo).run_wsgi(req)
161 except IOError, inst:
161 except IOError, inst:
162 msg = inst.strerror
162 msg = inst.strerror
163 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
163 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
164 except error.RepoError, inst:
164 except error.RepoError, inst:
165 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
165 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
166
166
167 # browse subdirectories
167 # browse subdirectories
168 subdir = virtual + '/'
168 subdir = virtual + '/'
169 if [r for r in repos if r.startswith(subdir)]:
169 if [r for r in repos if r.startswith(subdir)]:
170 req.respond(HTTP_OK, ctype)
170 req.respond(HTTP_OK, ctype)
171 return self.makeindex(req, tmpl, subdir)
171 return self.makeindex(req, tmpl, subdir)
172
172
173 up = virtual.rfind('/')
173 up = virtual.rfind('/')
174 if up < 0:
174 if up < 0:
175 break
175 break
176 virtual = virtual[:up]
176 virtual = virtual[:up]
177
177
178 # prefixes not found
178 # prefixes not found
179 req.respond(HTTP_NOT_FOUND, ctype)
179 req.respond(HTTP_NOT_FOUND, ctype)
180 return tmpl("notfound", repo=virtual)
180 return tmpl("notfound", repo=virtual)
181
181
182 except ErrorResponse, err:
182 except ErrorResponse, err:
183 req.respond(err, ctype)
183 req.respond(err, ctype)
184 return tmpl('error', error=err.message or '')
184 return tmpl('error', error=err.message or '')
185 finally:
185 finally:
186 tmpl = None
186 tmpl = None
187
187
188 def makeindex(self, req, tmpl, subdir=""):
188 def makeindex(self, req, tmpl, subdir=""):
189
189
190 def archivelist(ui, nodeid, url):
190 def archivelist(ui, nodeid, url):
191 allowed = ui.configlist("web", "allow_archive", untrusted=True)
191 allowed = ui.configlist("web", "allow_archive", untrusted=True)
192 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
192 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
193 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
193 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
194 untrusted=True):
194 untrusted=True):
195 yield {"type" : i[0], "extension": i[1],
195 yield {"type" : i[0], "extension": i[1],
196 "node": nodeid, "url": url}
196 "node": nodeid, "url": url}
197
197
198 sortdefault = None, False
198 sortdefault = None, False
199 def entries(sortcolumn="", descending=False, subdir="", **map):
199 def entries(sortcolumn="", descending=False, subdir="", **map):
200
200
201 rows = []
201 rows = []
202 parity = paritygen(self.stripecount)
202 parity = paritygen(self.stripecount)
203 descend = self.ui.configbool('web', 'descend', True)
203 descend = self.ui.configbool('web', 'descend', True)
204 for name, path in self.repos:
204 for name, path in self.repos:
205
205
206 if not name.startswith(subdir):
206 if not name.startswith(subdir):
207 continue
207 continue
208 name = name[len(subdir):]
208 name = name[len(subdir):]
209 if not descend and '/' in name:
209 if not descend and '/' in name:
210 continue
210 continue
211
211
212 u = self.ui.copy()
212 u = self.ui.copy()
213 try:
213 try:
214 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
214 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
215 except Exception, e:
215 except Exception, e:
216 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
216 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
217 continue
217 continue
218 def get(section, name, default=None):
218 def get(section, name, default=None):
219 return u.config(section, name, default, untrusted=True)
219 return u.config(section, name, default, untrusted=True)
220
220
221 if u.configbool("web", "hidden", untrusted=True):
221 if u.configbool("web", "hidden", untrusted=True):
222 continue
222 continue
223
223
224 if not self.read_allowed(u, req):
224 if not self.read_allowed(u, req):
225 continue
225 continue
226
226
227 parts = [name]
227 parts = [name]
228 if 'PATH_INFO' in req.env:
228 if 'PATH_INFO' in req.env:
229 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
229 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
230 if req.env['SCRIPT_NAME']:
230 if req.env['SCRIPT_NAME']:
231 parts.insert(0, req.env['SCRIPT_NAME'])
231 parts.insert(0, req.env['SCRIPT_NAME'])
232 m = re.match('((?:https?://)?)(.*)', '/'.join(parts))
232 m = re.match('((?:https?://)?)(.*)', '/'.join(parts))
233 # squish repeated slashes out of the path component
233 # squish repeated slashes out of the path component
234 url = m.group(1) + re.sub('/+', '/', m.group(2)) + '/'
234 url = m.group(1) + re.sub('/+', '/', m.group(2)) + '/'
235
235
236 # update time with local timezone
236 # update time with local timezone
237 try:
237 try:
238 d = (get_mtime(path), util.makedate()[1])
238 r = hg.repository(self.ui, path)
239 d = (get_mtime(r.spath), util.makedate()[1])
239 except OSError:
240 except OSError:
240 continue
241 continue
241
242
242 contact = get_contact(get)
243 contact = get_contact(get)
243 description = get("web", "description", "")
244 description = get("web", "description", "")
244 name = get("web", "name", name)
245 name = get("web", "name", name)
245 row = dict(contact=contact or "unknown",
246 row = dict(contact=contact or "unknown",
246 contact_sort=contact.upper() or "unknown",
247 contact_sort=contact.upper() or "unknown",
247 name=name,
248 name=name,
248 name_sort=name,
249 name_sort=name,
249 url=url,
250 url=url,
250 description=description or "unknown",
251 description=description or "unknown",
251 description_sort=description.upper() or "unknown",
252 description_sort=description.upper() or "unknown",
252 lastchange=d,
253 lastchange=d,
253 lastchange_sort=d[1]-d[0],
254 lastchange_sort=d[1]-d[0],
254 archives=archivelist(u, "tip", url))
255 archives=archivelist(u, "tip", url))
255 if (not sortcolumn or (sortcolumn, descending) == sortdefault):
256 if (not sortcolumn or (sortcolumn, descending) == sortdefault):
256 # fast path for unsorted output
257 # fast path for unsorted output
257 row['parity'] = parity.next()
258 row['parity'] = parity.next()
258 yield row
259 yield row
259 else:
260 else:
260 rows.append((row["%s_sort" % sortcolumn], row))
261 rows.append((row["%s_sort" % sortcolumn], row))
261 if rows:
262 if rows:
262 rows.sort()
263 rows.sort()
263 if descending:
264 if descending:
264 rows.reverse()
265 rows.reverse()
265 for key, row in rows:
266 for key, row in rows:
266 row['parity'] = parity.next()
267 row['parity'] = parity.next()
267 yield row
268 yield row
268
269
269 self.refresh()
270 self.refresh()
270 sortable = ["name", "description", "contact", "lastchange"]
271 sortable = ["name", "description", "contact", "lastchange"]
271 sortcolumn, descending = sortdefault
272 sortcolumn, descending = sortdefault
272 if 'sort' in req.form:
273 if 'sort' in req.form:
273 sortcolumn = req.form['sort'][0]
274 sortcolumn = req.form['sort'][0]
274 descending = sortcolumn.startswith('-')
275 descending = sortcolumn.startswith('-')
275 if descending:
276 if descending:
276 sortcolumn = sortcolumn[1:]
277 sortcolumn = sortcolumn[1:]
277 if sortcolumn not in sortable:
278 if sortcolumn not in sortable:
278 sortcolumn = ""
279 sortcolumn = ""
279
280
280 sort = [("sort_%s" % column,
281 sort = [("sort_%s" % column,
281 "%s%s" % ((not descending and column == sortcolumn)
282 "%s%s" % ((not descending and column == sortcolumn)
282 and "-" or "", column))
283 and "-" or "", column))
283 for column in sortable]
284 for column in sortable]
284
285
285 self.refresh()
286 self.refresh()
286 if self._baseurl is not None:
287 if self._baseurl is not None:
287 req.env['SCRIPT_NAME'] = self._baseurl
288 req.env['SCRIPT_NAME'] = self._baseurl
288
289
289 return tmpl("index", entries=entries, subdir=subdir,
290 return tmpl("index", entries=entries, subdir=subdir,
290 sortcolumn=sortcolumn, descending=descending,
291 sortcolumn=sortcolumn, descending=descending,
291 **dict(sort))
292 **dict(sort))
292
293
293 def templater(self, req):
294 def templater(self, req):
294
295
295 def header(**map):
296 def header(**map):
296 yield tmpl('header', encoding=encoding.encoding, **map)
297 yield tmpl('header', encoding=encoding.encoding, **map)
297
298
298 def footer(**map):
299 def footer(**map):
299 yield tmpl("footer", **map)
300 yield tmpl("footer", **map)
300
301
301 def motd(**map):
302 def motd(**map):
302 if self.motd is not None:
303 if self.motd is not None:
303 yield self.motd
304 yield self.motd
304 else:
305 else:
305 yield config('web', 'motd', '')
306 yield config('web', 'motd', '')
306
307
307 def config(section, name, default=None, untrusted=True):
308 def config(section, name, default=None, untrusted=True):
308 return self.ui.config(section, name, default, untrusted)
309 return self.ui.config(section, name, default, untrusted)
309
310
310 if self._baseurl is not None:
311 if self._baseurl is not None:
311 req.env['SCRIPT_NAME'] = self._baseurl
312 req.env['SCRIPT_NAME'] = self._baseurl
312
313
313 url = req.env.get('SCRIPT_NAME', '')
314 url = req.env.get('SCRIPT_NAME', '')
314 if not url.endswith('/'):
315 if not url.endswith('/'):
315 url += '/'
316 url += '/'
316
317
317 vars = {}
318 vars = {}
318 styles = (
319 styles = (
319 req.form.get('style', [None])[0],
320 req.form.get('style', [None])[0],
320 config('web', 'style'),
321 config('web', 'style'),
321 'paper'
322 'paper'
322 )
323 )
323 style, mapfile = templater.stylemap(styles)
324 style, mapfile = templater.stylemap(styles)
324 if style == styles[0]:
325 if style == styles[0]:
325 vars['style'] = style
326 vars['style'] = style
326
327
327 start = url[-1] == '?' and '&' or '?'
328 start = url[-1] == '?' and '&' or '?'
328 sessionvars = webutil.sessionvars(vars, start)
329 sessionvars = webutil.sessionvars(vars, start)
329 staticurl = config('web', 'staticurl') or url + 'static/'
330 staticurl = config('web', 'staticurl') or url + 'static/'
330 if not staticurl.endswith('/'):
331 if not staticurl.endswith('/'):
331 staticurl += '/'
332 staticurl += '/'
332
333
333 tmpl = templater.templater(mapfile,
334 tmpl = templater.templater(mapfile,
334 defaults={"header": header,
335 defaults={"header": header,
335 "footer": footer,
336 "footer": footer,
336 "motd": motd,
337 "motd": motd,
337 "url": url,
338 "url": url,
338 "staticurl": staticurl,
339 "staticurl": staticurl,
339 "sessionvars": sessionvars})
340 "sessionvars": sessionvars})
340 return tmpl
341 return tmpl
@@ -1,43 +1,48 b''
1 #!/bin/sh
1 #!/bin/sh
2
2
3 echo "[extensions]" >> $HGRCPATH
3 echo "[extensions]" >> $HGRCPATH
4 echo "share = " >> $HGRCPATH
4 echo "share = " >> $HGRCPATH
5
5
6 echo % prepare repo1
6 echo % prepare repo1
7 hg init repo1
7 hg init repo1
8 cd repo1
8 cd repo1
9 echo a > a
9 echo a > a
10 hg commit -A -m'init'
10 hg commit -A -m'init'
11
11
12 echo % share it
12 echo % share it
13 cd ..
13 cd ..
14 hg share repo1 repo2
14 hg share repo1 repo2
15
15
16 echo % contents of repo2/.hg
16 echo % contents of repo2/.hg
17 cd repo2
17 cd repo2
18 [ -d .hg/store ] \
18 [ -d .hg/store ] \
19 && echo "fail: .hg/store should not exist" \
19 && echo "fail: .hg/store should not exist" \
20 || echo "pass: .hg/store does not exist"
20 || echo "pass: .hg/store does not exist"
21 # sed appends a newline to the stream if none, GNU sed does not
21 # sed appends a newline to the stream if none, GNU sed does not
22 sed "s:$HGTMP:*HGTMP*:" .hg/sharedpath | tr -d '\n'; echo
22 sed "s:$HGTMP:*HGTMP*:" .hg/sharedpath | tr -d '\n'; echo
23
23
24 echo % commit in shared clone
24 echo % commit in shared clone
25 echo a >> a
25 echo a >> a
26 hg commit -m'change in shared clone'
26 hg commit -m'change in shared clone'
27
27
28 echo % check original
28 echo % check original
29 cd ../repo1
29 cd ../repo1
30 hg log
30 hg log
31 hg update
31 hg update
32 cat a # should be two lines of "a"
32 cat a # should be two lines of "a"
33
33
34 echo % commit in original
34 echo % commit in original
35 echo b > b
35 echo b > b
36 hg commit -A -m'another file'
36 hg commit -A -m'another file'
37
37
38 echo % check in shared clone
38 echo % check in shared clone
39 cd ../repo2
39 cd ../repo2
40 hg log
40 hg log
41 hg update
41 hg update
42 cat b # should exist with one "b"
42 cat b # should exist with one "b"
43
43
44 echo % hg serve shared clone
45 hg serve -n test -p $HGPORT -d --pid-file=hg.pid
46 cat hg.pid >> $DAEMON_PIDS
47
48 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/raw-file/'
@@ -1,45 +1,53 b''
1 % prepare repo1
1 % prepare repo1
2 adding a
2 adding a
3 % share it
3 % share it
4 updating working directory
4 updating working directory
5 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
5 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
6 % contents of repo2/.hg
6 % contents of repo2/.hg
7 pass: .hg/store does not exist
7 pass: .hg/store does not exist
8 *HGTMP*/test-share/repo1/.hg
8 *HGTMP*/test-share/repo1/.hg
9 % commit in shared clone
9 % commit in shared clone
10 % check original
10 % check original
11 changeset: 1:8af4dc49db9e
11 changeset: 1:8af4dc49db9e
12 tag: tip
12 tag: tip
13 user: test
13 user: test
14 date: Thu Jan 01 00:00:00 1970 +0000
14 date: Thu Jan 01 00:00:00 1970 +0000
15 summary: change in shared clone
15 summary: change in shared clone
16
16
17 changeset: 0:d3873e73d99e
17 changeset: 0:d3873e73d99e
18 user: test
18 user: test
19 date: Thu Jan 01 00:00:00 1970 +0000
19 date: Thu Jan 01 00:00:00 1970 +0000
20 summary: init
20 summary: init
21
21
22 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
22 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
23 a
23 a
24 a
24 a
25 % commit in original
25 % commit in original
26 adding b
26 adding b
27 % check in shared clone
27 % check in shared clone
28 changeset: 2:c2e0ac586386
28 changeset: 2:c2e0ac586386
29 tag: tip
29 tag: tip
30 user: test
30 user: test
31 date: Thu Jan 01 00:00:00 1970 +0000
31 date: Thu Jan 01 00:00:00 1970 +0000
32 summary: another file
32 summary: another file
33
33
34 changeset: 1:8af4dc49db9e
34 changeset: 1:8af4dc49db9e
35 user: test
35 user: test
36 date: Thu Jan 01 00:00:00 1970 +0000
36 date: Thu Jan 01 00:00:00 1970 +0000
37 summary: change in shared clone
37 summary: change in shared clone
38
38
39 changeset: 0:d3873e73d99e
39 changeset: 0:d3873e73d99e
40 user: test
40 user: test
41 date: Thu Jan 01 00:00:00 1970 +0000
41 date: Thu Jan 01 00:00:00 1970 +0000
42 summary: init
42 summary: init
43
43
44 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
44 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
45 b
45 b
46 % hg serve shared clone
47 200 Script output follows
48
49
50 -rw-r--r-- 4 a
51 -rw-r--r-- 2 b
52
53
General Comments 0
You need to be logged in to leave comments. Login now