##// END OF EJS Templates
hgweb: port static file handling to new response API...
Gregory Szorc -
r36889:98baf8de default
parent child Browse files
Show More
@@ -1,251 +1,253 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 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import base64
11 import base64
12 import errno
12 import errno
13 import mimetypes
13 import mimetypes
14 import os
14 import os
15 import stat
15 import stat
16
16
17 from .. import (
17 from .. import (
18 encoding,
18 encoding,
19 pycompat,
19 pycompat,
20 util,
20 util,
21 )
21 )
22
22
23 httpserver = util.httpserver
23 httpserver = util.httpserver
24
24
25 HTTP_OK = 200
25 HTTP_OK = 200
26 HTTP_NOT_MODIFIED = 304
26 HTTP_NOT_MODIFIED = 304
27 HTTP_BAD_REQUEST = 400
27 HTTP_BAD_REQUEST = 400
28 HTTP_UNAUTHORIZED = 401
28 HTTP_UNAUTHORIZED = 401
29 HTTP_FORBIDDEN = 403
29 HTTP_FORBIDDEN = 403
30 HTTP_NOT_FOUND = 404
30 HTTP_NOT_FOUND = 404
31 HTTP_METHOD_NOT_ALLOWED = 405
31 HTTP_METHOD_NOT_ALLOWED = 405
32 HTTP_SERVER_ERROR = 500
32 HTTP_SERVER_ERROR = 500
33
33
34
34
35 def ismember(ui, username, userlist):
35 def ismember(ui, username, userlist):
36 """Check if username is a member of userlist.
36 """Check if username is a member of userlist.
37
37
38 If userlist has a single '*' member, all users are considered members.
38 If userlist has a single '*' member, all users are considered members.
39 Can be overridden by extensions to provide more complex authorization
39 Can be overridden by extensions to provide more complex authorization
40 schemes.
40 schemes.
41 """
41 """
42 return userlist == ['*'] or username in userlist
42 return userlist == ['*'] or username in userlist
43
43
44 def checkauthz(hgweb, req, op):
44 def checkauthz(hgweb, req, op):
45 '''Check permission for operation based on request data (including
45 '''Check permission for operation based on request data (including
46 authentication info). Return if op allowed, else raise an ErrorResponse
46 authentication info). Return if op allowed, else raise an ErrorResponse
47 exception.'''
47 exception.'''
48
48
49 user = req.env.get(r'REMOTE_USER')
49 user = req.env.get(r'REMOTE_USER')
50
50
51 deny_read = hgweb.configlist('web', 'deny_read')
51 deny_read = hgweb.configlist('web', 'deny_read')
52 if deny_read and (not user or ismember(hgweb.repo.ui, user, deny_read)):
52 if deny_read and (not user or ismember(hgweb.repo.ui, user, deny_read)):
53 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
53 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
54
54
55 allow_read = hgweb.configlist('web', 'allow_read')
55 allow_read = hgweb.configlist('web', 'allow_read')
56 if allow_read and (not ismember(hgweb.repo.ui, user, allow_read)):
56 if allow_read and (not ismember(hgweb.repo.ui, user, allow_read)):
57 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
57 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
58
58
59 if op == 'pull' and not hgweb.allowpull:
59 if op == 'pull' and not hgweb.allowpull:
60 raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized')
60 raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized')
61 elif op == 'pull' or op is None: # op is None for interface requests
61 elif op == 'pull' or op is None: # op is None for interface requests
62 return
62 return
63
63
64 # enforce that you can only push using POST requests
64 # enforce that you can only push using POST requests
65 if req.env[r'REQUEST_METHOD'] != r'POST':
65 if req.env[r'REQUEST_METHOD'] != r'POST':
66 msg = 'push requires POST request'
66 msg = 'push requires POST request'
67 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
67 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
68
68
69 # require ssl by default for pushing, auth info cannot be sniffed
69 # require ssl by default for pushing, auth info cannot be sniffed
70 # and replayed
70 # and replayed
71 scheme = req.env.get('wsgi.url_scheme')
71 scheme = req.env.get('wsgi.url_scheme')
72 if hgweb.configbool('web', 'push_ssl') and scheme != 'https':
72 if hgweb.configbool('web', 'push_ssl') and scheme != 'https':
73 raise ErrorResponse(HTTP_FORBIDDEN, 'ssl required')
73 raise ErrorResponse(HTTP_FORBIDDEN, 'ssl required')
74
74
75 deny = hgweb.configlist('web', 'deny_push')
75 deny = hgweb.configlist('web', 'deny_push')
76 if deny and (not user or ismember(hgweb.repo.ui, user, deny)):
76 if deny and (not user or ismember(hgweb.repo.ui, user, deny)):
77 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
77 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
78
78
79 allow = hgweb.configlist('web', 'allow-push')
79 allow = hgweb.configlist('web', 'allow-push')
80 if not (allow and ismember(hgweb.repo.ui, user, allow)):
80 if not (allow and ismember(hgweb.repo.ui, user, allow)):
81 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
81 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
82
82
83 # Hooks for hgweb permission checks; extensions can add hooks here.
83 # Hooks for hgweb permission checks; extensions can add hooks here.
84 # Each hook is invoked like this: hook(hgweb, request, operation),
84 # Each hook is invoked like this: hook(hgweb, request, operation),
85 # where operation is either read, pull or push. Hooks should either
85 # where operation is either read, pull or push. Hooks should either
86 # raise an ErrorResponse exception, or just return.
86 # raise an ErrorResponse exception, or just return.
87 #
87 #
88 # It is possible to do both authentication and authorization through
88 # It is possible to do both authentication and authorization through
89 # this.
89 # this.
90 permhooks = [checkauthz]
90 permhooks = [checkauthz]
91
91
92
92
93 class ErrorResponse(Exception):
93 class ErrorResponse(Exception):
94 def __init__(self, code, message=None, headers=None):
94 def __init__(self, code, message=None, headers=None):
95 if message is None:
95 if message is None:
96 message = _statusmessage(code)
96 message = _statusmessage(code)
97 Exception.__init__(self, pycompat.sysstr(message))
97 Exception.__init__(self, pycompat.sysstr(message))
98 self.code = code
98 self.code = code
99 if headers is None:
99 if headers is None:
100 headers = []
100 headers = []
101 self.headers = headers
101 self.headers = headers
102
102
103 class continuereader(object):
103 class continuereader(object):
104 """File object wrapper to handle HTTP 100-continue.
104 """File object wrapper to handle HTTP 100-continue.
105
105
106 This is used by servers so they automatically handle Expect: 100-continue
106 This is used by servers so they automatically handle Expect: 100-continue
107 request headers. On first read of the request body, the 100 Continue
107 request headers. On first read of the request body, the 100 Continue
108 response is sent. This should trigger the client into actually sending
108 response is sent. This should trigger the client into actually sending
109 the request body.
109 the request body.
110 """
110 """
111 def __init__(self, f, write):
111 def __init__(self, f, write):
112 self.f = f
112 self.f = f
113 self._write = write
113 self._write = write
114 self.continued = False
114 self.continued = False
115
115
116 def read(self, amt=-1):
116 def read(self, amt=-1):
117 if not self.continued:
117 if not self.continued:
118 self.continued = True
118 self.continued = True
119 self._write('HTTP/1.1 100 Continue\r\n\r\n')
119 self._write('HTTP/1.1 100 Continue\r\n\r\n')
120 return self.f.read(amt)
120 return self.f.read(amt)
121
121
122 def __getattr__(self, attr):
122 def __getattr__(self, attr):
123 if attr in ('close', 'readline', 'readlines', '__iter__'):
123 if attr in ('close', 'readline', 'readlines', '__iter__'):
124 return getattr(self.f, attr)
124 return getattr(self.f, attr)
125 raise AttributeError
125 raise AttributeError
126
126
127 def _statusmessage(code):
127 def _statusmessage(code):
128 responses = httpserver.basehttprequesthandler.responses
128 responses = httpserver.basehttprequesthandler.responses
129 return responses.get(code, ('Error', 'Unknown error'))[0]
129 return responses.get(code, ('Error', 'Unknown error'))[0]
130
130
131 def statusmessage(code, message=None):
131 def statusmessage(code, message=None):
132 return '%d %s' % (code, message or _statusmessage(code))
132 return '%d %s' % (code, message or _statusmessage(code))
133
133
134 def get_stat(spath, fn):
134 def get_stat(spath, fn):
135 """stat fn if it exists, spath otherwise"""
135 """stat fn if it exists, spath otherwise"""
136 cl_path = os.path.join(spath, fn)
136 cl_path = os.path.join(spath, fn)
137 if os.path.exists(cl_path):
137 if os.path.exists(cl_path):
138 return os.stat(cl_path)
138 return os.stat(cl_path)
139 else:
139 else:
140 return os.stat(spath)
140 return os.stat(spath)
141
141
142 def get_mtime(spath):
142 def get_mtime(spath):
143 return get_stat(spath, "00changelog.i")[stat.ST_MTIME]
143 return get_stat(spath, "00changelog.i")[stat.ST_MTIME]
144
144
145 def ispathsafe(path):
145 def ispathsafe(path):
146 """Determine if a path is safe to use for filesystem access."""
146 """Determine if a path is safe to use for filesystem access."""
147 parts = path.split('/')
147 parts = path.split('/')
148 for part in parts:
148 for part in parts:
149 if (part in ('', pycompat.oscurdir, pycompat.ospardir) or
149 if (part in ('', pycompat.oscurdir, pycompat.ospardir) or
150 pycompat.ossep in part or
150 pycompat.ossep in part or
151 pycompat.osaltsep is not None and pycompat.osaltsep in part):
151 pycompat.osaltsep is not None and pycompat.osaltsep in part):
152 return False
152 return False
153
153
154 return True
154 return True
155
155
156 def staticfile(directory, fname, req):
156 def staticfile(directory, fname, res):
157 """return a file inside directory with guessed Content-Type header
157 """return a file inside directory with guessed Content-Type header
158
158
159 fname always uses '/' as directory separator and isn't allowed to
159 fname always uses '/' as directory separator and isn't allowed to
160 contain unusual path components.
160 contain unusual path components.
161 Content-Type is guessed using the mimetypes module.
161 Content-Type is guessed using the mimetypes module.
162 Return an empty string if fname is illegal or file not found.
162 Return an empty string if fname is illegal or file not found.
163
163
164 """
164 """
165 if not ispathsafe(fname):
165 if not ispathsafe(fname):
166 return
166 return
167
167
168 fpath = os.path.join(*fname.split('/'))
168 fpath = os.path.join(*fname.split('/'))
169 if isinstance(directory, str):
169 if isinstance(directory, str):
170 directory = [directory]
170 directory = [directory]
171 for d in directory:
171 for d in directory:
172 path = os.path.join(d, fpath)
172 path = os.path.join(d, fpath)
173 if os.path.exists(path):
173 if os.path.exists(path):
174 break
174 break
175 try:
175 try:
176 os.stat(path)
176 os.stat(path)
177 ct = mimetypes.guess_type(pycompat.fsdecode(path))[0] or "text/plain"
177 ct = mimetypes.guess_type(pycompat.fsdecode(path))[0] or "text/plain"
178 with open(path, 'rb') as fh:
178 with open(path, 'rb') as fh:
179 data = fh.read()
179 data = fh.read()
180
180
181 req.respond(HTTP_OK, ct, body=data)
181 res.headers['Content-Type'] = ct
182 res.setbodybytes(data)
183 return res
182 except TypeError:
184 except TypeError:
183 raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal filename')
185 raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal filename')
184 except OSError as err:
186 except OSError as err:
185 if err.errno == errno.ENOENT:
187 if err.errno == errno.ENOENT:
186 raise ErrorResponse(HTTP_NOT_FOUND)
188 raise ErrorResponse(HTTP_NOT_FOUND)
187 else:
189 else:
188 raise ErrorResponse(HTTP_SERVER_ERROR,
190 raise ErrorResponse(HTTP_SERVER_ERROR,
189 encoding.strtolocal(err.strerror))
191 encoding.strtolocal(err.strerror))
190
192
191 def paritygen(stripecount, offset=0):
193 def paritygen(stripecount, offset=0):
192 """count parity of horizontal stripes for easier reading"""
194 """count parity of horizontal stripes for easier reading"""
193 if stripecount and offset:
195 if stripecount and offset:
194 # account for offset, e.g. due to building the list in reverse
196 # account for offset, e.g. due to building the list in reverse
195 count = (stripecount + offset) % stripecount
197 count = (stripecount + offset) % stripecount
196 parity = (stripecount + offset) // stripecount & 1
198 parity = (stripecount + offset) // stripecount & 1
197 else:
199 else:
198 count = 0
200 count = 0
199 parity = 0
201 parity = 0
200 while True:
202 while True:
201 yield parity
203 yield parity
202 count += 1
204 count += 1
203 if stripecount and count >= stripecount:
205 if stripecount and count >= stripecount:
204 parity = 1 - parity
206 parity = 1 - parity
205 count = 0
207 count = 0
206
208
207 def get_contact(config):
209 def get_contact(config):
208 """Return repo contact information or empty string.
210 """Return repo contact information or empty string.
209
211
210 web.contact is the primary source, but if that is not set, try
212 web.contact is the primary source, but if that is not set, try
211 ui.username or $EMAIL as a fallback to display something useful.
213 ui.username or $EMAIL as a fallback to display something useful.
212 """
214 """
213 return (config("web", "contact") or
215 return (config("web", "contact") or
214 config("ui", "username") or
216 config("ui", "username") or
215 encoding.environ.get("EMAIL") or "")
217 encoding.environ.get("EMAIL") or "")
216
218
217 def cspvalues(ui):
219 def cspvalues(ui):
218 """Obtain the Content-Security-Policy header and nonce value.
220 """Obtain the Content-Security-Policy header and nonce value.
219
221
220 Returns a 2-tuple of the CSP header value and the nonce value.
222 Returns a 2-tuple of the CSP header value and the nonce value.
221
223
222 First value is ``None`` if CSP isn't enabled. Second value is ``None``
224 First value is ``None`` if CSP isn't enabled. Second value is ``None``
223 if CSP isn't enabled or if the CSP header doesn't need a nonce.
225 if CSP isn't enabled or if the CSP header doesn't need a nonce.
224 """
226 """
225 # Without demandimport, "import uuid" could have an immediate side-effect
227 # Without demandimport, "import uuid" could have an immediate side-effect
226 # running "ldconfig" on Linux trying to find libuuid.
228 # running "ldconfig" on Linux trying to find libuuid.
227 # With Python <= 2.7.12, that "ldconfig" is run via a shell and the shell
229 # With Python <= 2.7.12, that "ldconfig" is run via a shell and the shell
228 # may pollute the terminal with:
230 # may pollute the terminal with:
229 #
231 #
230 # shell-init: error retrieving current directory: getcwd: cannot access
232 # shell-init: error retrieving current directory: getcwd: cannot access
231 # parent directories: No such file or directory
233 # parent directories: No such file or directory
232 #
234 #
233 # Python >= 2.7.13 has fixed it by running "ldconfig" directly without a
235 # Python >= 2.7.13 has fixed it by running "ldconfig" directly without a
234 # shell (hg changeset a09ae70f3489).
236 # shell (hg changeset a09ae70f3489).
235 #
237 #
236 # Moved "import uuid" from here so it's executed after we know we have
238 # Moved "import uuid" from here so it's executed after we know we have
237 # a sane cwd (i.e. after dispatch.py cwd check).
239 # a sane cwd (i.e. after dispatch.py cwd check).
238 #
240 #
239 # We can move it back once we no longer need Python <= 2.7.12 support.
241 # We can move it back once we no longer need Python <= 2.7.12 support.
240 import uuid
242 import uuid
241
243
242 # Don't allow untrusted CSP setting since it be disable protections
244 # Don't allow untrusted CSP setting since it be disable protections
243 # from a trusted/global source.
245 # from a trusted/global source.
244 csp = ui.config('web', 'csp', untrusted=False)
246 csp = ui.config('web', 'csp', untrusted=False)
245 nonce = None
247 nonce = None
246
248
247 if csp and '%nonce%' in csp:
249 if csp and '%nonce%' in csp:
248 nonce = base64.urlsafe_b64encode(uuid.uuid4().bytes).rstrip('=')
250 nonce = base64.urlsafe_b64encode(uuid.uuid4().bytes).rstrip('=')
249 csp = csp.replace('%nonce%', nonce)
251 csp = csp.replace('%nonce%', nonce)
250
252
251 return csp, nonce
253 return csp, nonce
@@ -1,546 +1,553 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 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import os
11 import os
12 import re
12 import re
13 import time
13 import time
14
14
15 from ..i18n import _
15 from ..i18n import _
16
16
17 from .common import (
17 from .common import (
18 ErrorResponse,
18 ErrorResponse,
19 HTTP_NOT_FOUND,
19 HTTP_NOT_FOUND,
20 HTTP_OK,
20 HTTP_OK,
21 HTTP_SERVER_ERROR,
21 HTTP_SERVER_ERROR,
22 cspvalues,
22 cspvalues,
23 get_contact,
23 get_contact,
24 get_mtime,
24 get_mtime,
25 ismember,
25 ismember,
26 paritygen,
26 paritygen,
27 staticfile,
27 staticfile,
28 )
28 )
29
29
30 from .. import (
30 from .. import (
31 configitems,
31 configitems,
32 encoding,
32 encoding,
33 error,
33 error,
34 hg,
34 hg,
35 profiling,
35 profiling,
36 pycompat,
36 pycompat,
37 scmutil,
37 scmutil,
38 templater,
38 templater,
39 ui as uimod,
39 ui as uimod,
40 util,
40 util,
41 )
41 )
42
42
43 from . import (
43 from . import (
44 hgweb_mod,
44 hgweb_mod,
45 request as requestmod,
45 request as requestmod,
46 webutil,
46 webutil,
47 wsgicgi,
47 wsgicgi,
48 )
48 )
49 from ..utils import dateutil
49 from ..utils import dateutil
50
50
51 def cleannames(items):
51 def cleannames(items):
52 return [(util.pconvert(name).strip('/'), path) for name, path in items]
52 return [(util.pconvert(name).strip('/'), path) for name, path in items]
53
53
54 def findrepos(paths):
54 def findrepos(paths):
55 repos = []
55 repos = []
56 for prefix, root in cleannames(paths):
56 for prefix, root in cleannames(paths):
57 roothead, roottail = os.path.split(root)
57 roothead, roottail = os.path.split(root)
58 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
58 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
59 # /bar/ be served as as foo/N .
59 # /bar/ be served as as foo/N .
60 # '*' will not search inside dirs with .hg (except .hg/patches),
60 # '*' will not search inside dirs with .hg (except .hg/patches),
61 # '**' will search inside dirs with .hg (and thus also find subrepos).
61 # '**' will search inside dirs with .hg (and thus also find subrepos).
62 try:
62 try:
63 recurse = {'*': False, '**': True}[roottail]
63 recurse = {'*': False, '**': True}[roottail]
64 except KeyError:
64 except KeyError:
65 repos.append((prefix, root))
65 repos.append((prefix, root))
66 continue
66 continue
67 roothead = os.path.normpath(os.path.abspath(roothead))
67 roothead = os.path.normpath(os.path.abspath(roothead))
68 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
68 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
69 repos.extend(urlrepos(prefix, roothead, paths))
69 repos.extend(urlrepos(prefix, roothead, paths))
70 return repos
70 return repos
71
71
72 def urlrepos(prefix, roothead, paths):
72 def urlrepos(prefix, roothead, paths):
73 """yield url paths and filesystem paths from a list of repo paths
73 """yield url paths and filesystem paths from a list of repo paths
74
74
75 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
75 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
76 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
76 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
77 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
77 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
78 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
78 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
79 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
79 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
80 """
80 """
81 for path in paths:
81 for path in paths:
82 path = os.path.normpath(path)
82 path = os.path.normpath(path)
83 yield (prefix + '/' +
83 yield (prefix + '/' +
84 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
84 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
85
85
86 def geturlcgivars(baseurl, port):
86 def geturlcgivars(baseurl, port):
87 """
87 """
88 Extract CGI variables from baseurl
88 Extract CGI variables from baseurl
89
89
90 >>> geturlcgivars(b"http://host.org/base", b"80")
90 >>> geturlcgivars(b"http://host.org/base", b"80")
91 ('host.org', '80', '/base')
91 ('host.org', '80', '/base')
92 >>> geturlcgivars(b"http://host.org:8000/base", b"80")
92 >>> geturlcgivars(b"http://host.org:8000/base", b"80")
93 ('host.org', '8000', '/base')
93 ('host.org', '8000', '/base')
94 >>> geturlcgivars(b'/base', 8000)
94 >>> geturlcgivars(b'/base', 8000)
95 ('', '8000', '/base')
95 ('', '8000', '/base')
96 >>> geturlcgivars(b"base", b'8000')
96 >>> geturlcgivars(b"base", b'8000')
97 ('', '8000', '/base')
97 ('', '8000', '/base')
98 >>> geturlcgivars(b"http://host", b'8000')
98 >>> geturlcgivars(b"http://host", b'8000')
99 ('host', '8000', '/')
99 ('host', '8000', '/')
100 >>> geturlcgivars(b"http://host/", b'8000')
100 >>> geturlcgivars(b"http://host/", b'8000')
101 ('host', '8000', '/')
101 ('host', '8000', '/')
102 """
102 """
103 u = util.url(baseurl)
103 u = util.url(baseurl)
104 name = u.host or ''
104 name = u.host or ''
105 if u.port:
105 if u.port:
106 port = u.port
106 port = u.port
107 path = u.path or ""
107 path = u.path or ""
108 if not path.startswith('/'):
108 if not path.startswith('/'):
109 path = '/' + path
109 path = '/' + path
110
110
111 return name, pycompat.bytestr(port), path
111 return name, pycompat.bytestr(port), path
112
112
113 class hgwebdir(object):
113 class hgwebdir(object):
114 """HTTP server for multiple repositories.
114 """HTTP server for multiple repositories.
115
115
116 Given a configuration, different repositories will be served depending
116 Given a configuration, different repositories will be served depending
117 on the request path.
117 on the request path.
118
118
119 Instances are typically used as WSGI applications.
119 Instances are typically used as WSGI applications.
120 """
120 """
121 def __init__(self, conf, baseui=None):
121 def __init__(self, conf, baseui=None):
122 self.conf = conf
122 self.conf = conf
123 self.baseui = baseui
123 self.baseui = baseui
124 self.ui = None
124 self.ui = None
125 self.lastrefresh = 0
125 self.lastrefresh = 0
126 self.motd = None
126 self.motd = None
127 self.refresh()
127 self.refresh()
128
128
129 def refresh(self):
129 def refresh(self):
130 if self.ui:
130 if self.ui:
131 refreshinterval = self.ui.configint('web', 'refreshinterval')
131 refreshinterval = self.ui.configint('web', 'refreshinterval')
132 else:
132 else:
133 item = configitems.coreitems['web']['refreshinterval']
133 item = configitems.coreitems['web']['refreshinterval']
134 refreshinterval = item.default
134 refreshinterval = item.default
135
135
136 # refreshinterval <= 0 means to always refresh.
136 # refreshinterval <= 0 means to always refresh.
137 if (refreshinterval > 0 and
137 if (refreshinterval > 0 and
138 self.lastrefresh + refreshinterval > time.time()):
138 self.lastrefresh + refreshinterval > time.time()):
139 return
139 return
140
140
141 if self.baseui:
141 if self.baseui:
142 u = self.baseui.copy()
142 u = self.baseui.copy()
143 else:
143 else:
144 u = uimod.ui.load()
144 u = uimod.ui.load()
145 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
145 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
146 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
146 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
147 # displaying bundling progress bar while serving feels wrong and may
147 # displaying bundling progress bar while serving feels wrong and may
148 # break some wsgi implementations.
148 # break some wsgi implementations.
149 u.setconfig('progress', 'disable', 'true', 'hgweb')
149 u.setconfig('progress', 'disable', 'true', 'hgweb')
150
150
151 if not isinstance(self.conf, (dict, list, tuple)):
151 if not isinstance(self.conf, (dict, list, tuple)):
152 map = {'paths': 'hgweb-paths'}
152 map = {'paths': 'hgweb-paths'}
153 if not os.path.exists(self.conf):
153 if not os.path.exists(self.conf):
154 raise error.Abort(_('config file %s not found!') % self.conf)
154 raise error.Abort(_('config file %s not found!') % self.conf)
155 u.readconfig(self.conf, remap=map, trust=True)
155 u.readconfig(self.conf, remap=map, trust=True)
156 paths = []
156 paths = []
157 for name, ignored in u.configitems('hgweb-paths'):
157 for name, ignored in u.configitems('hgweb-paths'):
158 for path in u.configlist('hgweb-paths', name):
158 for path in u.configlist('hgweb-paths', name):
159 paths.append((name, path))
159 paths.append((name, path))
160 elif isinstance(self.conf, (list, tuple)):
160 elif isinstance(self.conf, (list, tuple)):
161 paths = self.conf
161 paths = self.conf
162 elif isinstance(self.conf, dict):
162 elif isinstance(self.conf, dict):
163 paths = self.conf.items()
163 paths = self.conf.items()
164
164
165 repos = findrepos(paths)
165 repos = findrepos(paths)
166 for prefix, root in u.configitems('collections'):
166 for prefix, root in u.configitems('collections'):
167 prefix = util.pconvert(prefix)
167 prefix = util.pconvert(prefix)
168 for path in scmutil.walkrepos(root, followsym=True):
168 for path in scmutil.walkrepos(root, followsym=True):
169 repo = os.path.normpath(path)
169 repo = os.path.normpath(path)
170 name = util.pconvert(repo)
170 name = util.pconvert(repo)
171 if name.startswith(prefix):
171 if name.startswith(prefix):
172 name = name[len(prefix):]
172 name = name[len(prefix):]
173 repos.append((name.lstrip('/'), repo))
173 repos.append((name.lstrip('/'), repo))
174
174
175 self.repos = repos
175 self.repos = repos
176 self.ui = u
176 self.ui = u
177 encoding.encoding = self.ui.config('web', 'encoding')
177 encoding.encoding = self.ui.config('web', 'encoding')
178 self.style = self.ui.config('web', 'style')
178 self.style = self.ui.config('web', 'style')
179 self.templatepath = self.ui.config('web', 'templates', untrusted=False)
179 self.templatepath = self.ui.config('web', 'templates', untrusted=False)
180 self.stripecount = self.ui.config('web', 'stripes')
180 self.stripecount = self.ui.config('web', 'stripes')
181 if self.stripecount:
181 if self.stripecount:
182 self.stripecount = int(self.stripecount)
182 self.stripecount = int(self.stripecount)
183 self._baseurl = self.ui.config('web', 'baseurl')
183 self._baseurl = self.ui.config('web', 'baseurl')
184 prefix = self.ui.config('web', 'prefix')
184 prefix = self.ui.config('web', 'prefix')
185 if prefix.startswith('/'):
185 if prefix.startswith('/'):
186 prefix = prefix[1:]
186 prefix = prefix[1:]
187 if prefix.endswith('/'):
187 if prefix.endswith('/'):
188 prefix = prefix[:-1]
188 prefix = prefix[:-1]
189 self.prefix = prefix
189 self.prefix = prefix
190 self.lastrefresh = time.time()
190 self.lastrefresh = time.time()
191
191
192 def run(self):
192 def run(self):
193 if not encoding.environ.get('GATEWAY_INTERFACE',
193 if not encoding.environ.get('GATEWAY_INTERFACE',
194 '').startswith("CGI/1."):
194 '').startswith("CGI/1."):
195 raise RuntimeError("This function is only intended to be "
195 raise RuntimeError("This function is only intended to be "
196 "called while running as a CGI script.")
196 "called while running as a CGI script.")
197 wsgicgi.launch(self)
197 wsgicgi.launch(self)
198
198
199 def __call__(self, env, respond):
199 def __call__(self, env, respond):
200 wsgireq = requestmod.wsgirequest(env, respond)
200 wsgireq = requestmod.wsgirequest(env, respond)
201 return self.run_wsgi(wsgireq)
201 return self.run_wsgi(wsgireq)
202
202
203 def read_allowed(self, ui, wsgireq):
203 def read_allowed(self, ui, wsgireq):
204 """Check allow_read and deny_read config options of a repo's ui object
204 """Check allow_read and deny_read config options of a repo's ui object
205 to determine user permissions. By default, with neither option set (or
205 to determine user permissions. By default, with neither option set (or
206 both empty), allow all users to read the repo. There are two ways a
206 both empty), allow all users to read the repo. There are two ways a
207 user can be denied read access: (1) deny_read is not empty, and the
207 user can be denied read access: (1) deny_read is not empty, and the
208 user is unauthenticated or deny_read contains user (or *), and (2)
208 user is unauthenticated or deny_read contains user (or *), and (2)
209 allow_read is not empty and the user is not in allow_read. Return True
209 allow_read is not empty and the user is not in allow_read. Return True
210 if user is allowed to read the repo, else return False."""
210 if user is allowed to read the repo, else return False."""
211
211
212 user = wsgireq.env.get('REMOTE_USER')
212 user = wsgireq.env.get('REMOTE_USER')
213
213
214 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
214 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
215 if deny_read and (not user or ismember(ui, user, deny_read)):
215 if deny_read and (not user or ismember(ui, user, deny_read)):
216 return False
216 return False
217
217
218 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
218 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
219 # by default, allow reading if no allow_read option has been set
219 # by default, allow reading if no allow_read option has been set
220 if (not allow_read) or ismember(ui, user, allow_read):
220 if (not allow_read) or ismember(ui, user, allow_read):
221 return True
221 return True
222
222
223 return False
223 return False
224
224
225 def run_wsgi(self, wsgireq):
225 def run_wsgi(self, wsgireq):
226 profile = self.ui.configbool('profiling', 'enabled')
226 profile = self.ui.configbool('profiling', 'enabled')
227 with profiling.profile(self.ui, enabled=profile):
227 with profiling.profile(self.ui, enabled=profile):
228 for r in self._runwsgi(wsgireq):
228 for r in self._runwsgi(wsgireq):
229 yield r
229 yield r
230
230
231 def _runwsgi(self, wsgireq):
231 def _runwsgi(self, wsgireq):
232 req = wsgireq.req
232 req = wsgireq.req
233 res = wsgireq.res
233
234
234 try:
235 try:
235 self.refresh()
236 self.refresh()
236
237
237 csp, nonce = cspvalues(self.ui)
238 csp, nonce = cspvalues(self.ui)
238 if csp:
239 if csp:
240 res.headers['Content-Security-Policy'] = csp
239 wsgireq.headers.append(('Content-Security-Policy', csp))
241 wsgireq.headers.append(('Content-Security-Policy', csp))
240
242
241 virtual = wsgireq.env.get("PATH_INFO", "").strip('/')
243 virtual = wsgireq.env.get("PATH_INFO", "").strip('/')
242 tmpl = self.templater(wsgireq, nonce)
244 tmpl = self.templater(wsgireq, nonce)
243 ctype = tmpl('mimetype', encoding=encoding.encoding)
245 ctype = tmpl('mimetype', encoding=encoding.encoding)
244 ctype = templater.stringify(ctype)
246 ctype = templater.stringify(ctype)
245
247
248 # Global defaults. These can be overridden by any handler.
249 res.status = '200 Script output follows'
250 res.headers['Content-Type'] = ctype
251
246 # a static file
252 # a static file
247 if virtual.startswith('static/') or 'static' in req.qsparams:
253 if virtual.startswith('static/') or 'static' in req.qsparams:
248 if virtual.startswith('static/'):
254 if virtual.startswith('static/'):
249 fname = virtual[7:]
255 fname = virtual[7:]
250 else:
256 else:
251 fname = req.qsparams['static']
257 fname = req.qsparams['static']
252 static = self.ui.config("web", "static", None,
258 static = self.ui.config("web", "static", None,
253 untrusted=False)
259 untrusted=False)
254 if not static:
260 if not static:
255 tp = self.templatepath or templater.templatepaths()
261 tp = self.templatepath or templater.templatepaths()
256 if isinstance(tp, str):
262 if isinstance(tp, str):
257 tp = [tp]
263 tp = [tp]
258 static = [os.path.join(p, 'static') for p in tp]
264 static = [os.path.join(p, 'static') for p in tp]
259 staticfile(static, fname, wsgireq)
265
260 return []
266 staticfile(static, fname, res)
267 return res.sendresponse()
261
268
262 # top-level index
269 # top-level index
263
270
264 repos = dict(self.repos)
271 repos = dict(self.repos)
265
272
266 if (not virtual or virtual == 'index') and virtual not in repos:
273 if (not virtual or virtual == 'index') and virtual not in repos:
267 wsgireq.respond(HTTP_OK, ctype)
274 wsgireq.respond(HTTP_OK, ctype)
268 return self.makeindex(wsgireq, tmpl)
275 return self.makeindex(wsgireq, tmpl)
269
276
270 # nested indexes and hgwebs
277 # nested indexes and hgwebs
271
278
272 if virtual.endswith('/index') and virtual not in repos:
279 if virtual.endswith('/index') and virtual not in repos:
273 subdir = virtual[:-len('index')]
280 subdir = virtual[:-len('index')]
274 if any(r.startswith(subdir) for r in repos):
281 if any(r.startswith(subdir) for r in repos):
275 wsgireq.respond(HTTP_OK, ctype)
282 wsgireq.respond(HTTP_OK, ctype)
276 return self.makeindex(wsgireq, tmpl, subdir)
283 return self.makeindex(wsgireq, tmpl, subdir)
277
284
278 def _virtualdirs():
285 def _virtualdirs():
279 # Check the full virtual path, each parent, and the root ('')
286 # Check the full virtual path, each parent, and the root ('')
280 if virtual != '':
287 if virtual != '':
281 yield virtual
288 yield virtual
282
289
283 for p in util.finddirs(virtual):
290 for p in util.finddirs(virtual):
284 yield p
291 yield p
285
292
286 yield ''
293 yield ''
287
294
288 for virtualrepo in _virtualdirs():
295 for virtualrepo in _virtualdirs():
289 real = repos.get(virtualrepo)
296 real = repos.get(virtualrepo)
290 if real:
297 if real:
291 wsgireq.env['REPO_NAME'] = virtualrepo
298 wsgireq.env['REPO_NAME'] = virtualrepo
292 # We have to re-parse because of updated environment
299 # We have to re-parse because of updated environment
293 # variable.
300 # variable.
294 # TODO this is kind of hacky and we should have a better
301 # TODO this is kind of hacky and we should have a better
295 # way of doing this than with REPO_NAME side-effects.
302 # way of doing this than with REPO_NAME side-effects.
296 wsgireq.req = requestmod.parserequestfromenv(
303 wsgireq.req = requestmod.parserequestfromenv(
297 wsgireq.env, wsgireq.req.bodyfh)
304 wsgireq.env, wsgireq.req.bodyfh)
298 try:
305 try:
299 # ensure caller gets private copy of ui
306 # ensure caller gets private copy of ui
300 repo = hg.repository(self.ui.copy(), real)
307 repo = hg.repository(self.ui.copy(), real)
301 return hgweb_mod.hgweb(repo).run_wsgi(wsgireq)
308 return hgweb_mod.hgweb(repo).run_wsgi(wsgireq)
302 except IOError as inst:
309 except IOError as inst:
303 msg = encoding.strtolocal(inst.strerror)
310 msg = encoding.strtolocal(inst.strerror)
304 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
311 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
305 except error.RepoError as inst:
312 except error.RepoError as inst:
306 raise ErrorResponse(HTTP_SERVER_ERROR, bytes(inst))
313 raise ErrorResponse(HTTP_SERVER_ERROR, bytes(inst))
307
314
308 # browse subdirectories
315 # browse subdirectories
309 subdir = virtual + '/'
316 subdir = virtual + '/'
310 if [r for r in repos if r.startswith(subdir)]:
317 if [r for r in repos if r.startswith(subdir)]:
311 wsgireq.respond(HTTP_OK, ctype)
318 wsgireq.respond(HTTP_OK, ctype)
312 return self.makeindex(wsgireq, tmpl, subdir)
319 return self.makeindex(wsgireq, tmpl, subdir)
313
320
314 # prefixes not found
321 # prefixes not found
315 wsgireq.respond(HTTP_NOT_FOUND, ctype)
322 wsgireq.respond(HTTP_NOT_FOUND, ctype)
316 return tmpl("notfound", repo=virtual)
323 return tmpl("notfound", repo=virtual)
317
324
318 except ErrorResponse as err:
325 except ErrorResponse as err:
319 wsgireq.respond(err, ctype)
326 wsgireq.respond(err, ctype)
320 return tmpl('error', error=err.message or '')
327 return tmpl('error', error=err.message or '')
321 finally:
328 finally:
322 tmpl = None
329 tmpl = None
323
330
324 def makeindex(self, wsgireq, tmpl, subdir=""):
331 def makeindex(self, wsgireq, tmpl, subdir=""):
325
332
326 def archivelist(ui, nodeid, url):
333 def archivelist(ui, nodeid, url):
327 allowed = ui.configlist("web", "allow_archive", untrusted=True)
334 allowed = ui.configlist("web", "allow_archive", untrusted=True)
328 archives = []
335 archives = []
329 for typ, spec in hgweb_mod.archivespecs.iteritems():
336 for typ, spec in hgweb_mod.archivespecs.iteritems():
330 if typ in allowed or ui.configbool("web", "allow" + typ,
337 if typ in allowed or ui.configbool("web", "allow" + typ,
331 untrusted=True):
338 untrusted=True):
332 archives.append({"type": typ, "extension": spec[2],
339 archives.append({"type": typ, "extension": spec[2],
333 "node": nodeid, "url": url})
340 "node": nodeid, "url": url})
334 return archives
341 return archives
335
342
336 def rawentries(subdir="", **map):
343 def rawentries(subdir="", **map):
337
344
338 descend = self.ui.configbool('web', 'descend')
345 descend = self.ui.configbool('web', 'descend')
339 collapse = self.ui.configbool('web', 'collapse')
346 collapse = self.ui.configbool('web', 'collapse')
340 seenrepos = set()
347 seenrepos = set()
341 seendirs = set()
348 seendirs = set()
342 for name, path in self.repos:
349 for name, path in self.repos:
343
350
344 if not name.startswith(subdir):
351 if not name.startswith(subdir):
345 continue
352 continue
346 name = name[len(subdir):]
353 name = name[len(subdir):]
347 directory = False
354 directory = False
348
355
349 if '/' in name:
356 if '/' in name:
350 if not descend:
357 if not descend:
351 continue
358 continue
352
359
353 nameparts = name.split('/')
360 nameparts = name.split('/')
354 rootname = nameparts[0]
361 rootname = nameparts[0]
355
362
356 if not collapse:
363 if not collapse:
357 pass
364 pass
358 elif rootname in seendirs:
365 elif rootname in seendirs:
359 continue
366 continue
360 elif rootname in seenrepos:
367 elif rootname in seenrepos:
361 pass
368 pass
362 else:
369 else:
363 directory = True
370 directory = True
364 name = rootname
371 name = rootname
365
372
366 # redefine the path to refer to the directory
373 # redefine the path to refer to the directory
367 discarded = '/'.join(nameparts[1:])
374 discarded = '/'.join(nameparts[1:])
368
375
369 # remove name parts plus accompanying slash
376 # remove name parts plus accompanying slash
370 path = path[:-len(discarded) - 1]
377 path = path[:-len(discarded) - 1]
371
378
372 try:
379 try:
373 r = hg.repository(self.ui, path)
380 r = hg.repository(self.ui, path)
374 directory = False
381 directory = False
375 except (IOError, error.RepoError):
382 except (IOError, error.RepoError):
376 pass
383 pass
377
384
378 parts = [name]
385 parts = [name]
379 parts.insert(0, '/' + subdir.rstrip('/'))
386 parts.insert(0, '/' + subdir.rstrip('/'))
380 if wsgireq.env['SCRIPT_NAME']:
387 if wsgireq.env['SCRIPT_NAME']:
381 parts.insert(0, wsgireq.env['SCRIPT_NAME'])
388 parts.insert(0, wsgireq.env['SCRIPT_NAME'])
382 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
389 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
383
390
384 # show either a directory entry or a repository
391 # show either a directory entry or a repository
385 if directory:
392 if directory:
386 # get the directory's time information
393 # get the directory's time information
387 try:
394 try:
388 d = (get_mtime(path), dateutil.makedate()[1])
395 d = (get_mtime(path), dateutil.makedate()[1])
389 except OSError:
396 except OSError:
390 continue
397 continue
391
398
392 # add '/' to the name to make it obvious that
399 # add '/' to the name to make it obvious that
393 # the entry is a directory, not a regular repository
400 # the entry is a directory, not a regular repository
394 row = {'contact': "",
401 row = {'contact': "",
395 'contact_sort': "",
402 'contact_sort': "",
396 'name': name + '/',
403 'name': name + '/',
397 'name_sort': name,
404 'name_sort': name,
398 'url': url,
405 'url': url,
399 'description': "",
406 'description': "",
400 'description_sort': "",
407 'description_sort': "",
401 'lastchange': d,
408 'lastchange': d,
402 'lastchange_sort': d[1]-d[0],
409 'lastchange_sort': d[1]-d[0],
403 'archives': [],
410 'archives': [],
404 'isdirectory': True,
411 'isdirectory': True,
405 'labels': [],
412 'labels': [],
406 }
413 }
407
414
408 seendirs.add(name)
415 seendirs.add(name)
409 yield row
416 yield row
410 continue
417 continue
411
418
412 u = self.ui.copy()
419 u = self.ui.copy()
413 try:
420 try:
414 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
421 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
415 except Exception as e:
422 except Exception as e:
416 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
423 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
417 continue
424 continue
418 def get(section, name, default=uimod._unset):
425 def get(section, name, default=uimod._unset):
419 return u.config(section, name, default, untrusted=True)
426 return u.config(section, name, default, untrusted=True)
420
427
421 if u.configbool("web", "hidden", untrusted=True):
428 if u.configbool("web", "hidden", untrusted=True):
422 continue
429 continue
423
430
424 if not self.read_allowed(u, wsgireq):
431 if not self.read_allowed(u, wsgireq):
425 continue
432 continue
426
433
427 # update time with local timezone
434 # update time with local timezone
428 try:
435 try:
429 r = hg.repository(self.ui, path)
436 r = hg.repository(self.ui, path)
430 except IOError:
437 except IOError:
431 u.warn(_('error accessing repository at %s\n') % path)
438 u.warn(_('error accessing repository at %s\n') % path)
432 continue
439 continue
433 except error.RepoError:
440 except error.RepoError:
434 u.warn(_('error accessing repository at %s\n') % path)
441 u.warn(_('error accessing repository at %s\n') % path)
435 continue
442 continue
436 try:
443 try:
437 d = (get_mtime(r.spath), dateutil.makedate()[1])
444 d = (get_mtime(r.spath), dateutil.makedate()[1])
438 except OSError:
445 except OSError:
439 continue
446 continue
440
447
441 contact = get_contact(get)
448 contact = get_contact(get)
442 description = get("web", "description")
449 description = get("web", "description")
443 seenrepos.add(name)
450 seenrepos.add(name)
444 name = get("web", "name", name)
451 name = get("web", "name", name)
445 row = {'contact': contact or "unknown",
452 row = {'contact': contact or "unknown",
446 'contact_sort': contact.upper() or "unknown",
453 'contact_sort': contact.upper() or "unknown",
447 'name': name,
454 'name': name,
448 'name_sort': name,
455 'name_sort': name,
449 'url': url,
456 'url': url,
450 'description': description or "unknown",
457 'description': description or "unknown",
451 'description_sort': description.upper() or "unknown",
458 'description_sort': description.upper() or "unknown",
452 'lastchange': d,
459 'lastchange': d,
453 'lastchange_sort': d[1]-d[0],
460 'lastchange_sort': d[1]-d[0],
454 'archives': archivelist(u, "tip", url),
461 'archives': archivelist(u, "tip", url),
455 'isdirectory': None,
462 'isdirectory': None,
456 'labels': u.configlist('web', 'labels', untrusted=True),
463 'labels': u.configlist('web', 'labels', untrusted=True),
457 }
464 }
458
465
459 yield row
466 yield row
460
467
461 sortdefault = None, False
468 sortdefault = None, False
462 def entries(sortcolumn="", descending=False, subdir="", **map):
469 def entries(sortcolumn="", descending=False, subdir="", **map):
463 rows = rawentries(subdir=subdir, **map)
470 rows = rawentries(subdir=subdir, **map)
464
471
465 if sortcolumn and sortdefault != (sortcolumn, descending):
472 if sortcolumn and sortdefault != (sortcolumn, descending):
466 sortkey = '%s_sort' % sortcolumn
473 sortkey = '%s_sort' % sortcolumn
467 rows = sorted(rows, key=lambda x: x[sortkey],
474 rows = sorted(rows, key=lambda x: x[sortkey],
468 reverse=descending)
475 reverse=descending)
469 for row, parity in zip(rows, paritygen(self.stripecount)):
476 for row, parity in zip(rows, paritygen(self.stripecount)):
470 row['parity'] = parity
477 row['parity'] = parity
471 yield row
478 yield row
472
479
473 self.refresh()
480 self.refresh()
474 sortable = ["name", "description", "contact", "lastchange"]
481 sortable = ["name", "description", "contact", "lastchange"]
475 sortcolumn, descending = sortdefault
482 sortcolumn, descending = sortdefault
476 if 'sort' in wsgireq.req.qsparams:
483 if 'sort' in wsgireq.req.qsparams:
477 sortcolum = wsgireq.req.qsparams['sort']
484 sortcolum = wsgireq.req.qsparams['sort']
478 descending = sortcolumn.startswith('-')
485 descending = sortcolumn.startswith('-')
479 if descending:
486 if descending:
480 sortcolumn = sortcolumn[1:]
487 sortcolumn = sortcolumn[1:]
481 if sortcolumn not in sortable:
488 if sortcolumn not in sortable:
482 sortcolumn = ""
489 sortcolumn = ""
483
490
484 sort = [("sort_%s" % column,
491 sort = [("sort_%s" % column,
485 "%s%s" % ((not descending and column == sortcolumn)
492 "%s%s" % ((not descending and column == sortcolumn)
486 and "-" or "", column))
493 and "-" or "", column))
487 for column in sortable]
494 for column in sortable]
488
495
489 self.refresh()
496 self.refresh()
490 self.updatereqenv(wsgireq.env)
497 self.updatereqenv(wsgireq.env)
491
498
492 return tmpl("index", entries=entries, subdir=subdir,
499 return tmpl("index", entries=entries, subdir=subdir,
493 pathdef=hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
500 pathdef=hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
494 sortcolumn=sortcolumn, descending=descending,
501 sortcolumn=sortcolumn, descending=descending,
495 **dict(sort))
502 **dict(sort))
496
503
497 def templater(self, wsgireq, nonce):
504 def templater(self, wsgireq, nonce):
498
505
499 def motd(**map):
506 def motd(**map):
500 if self.motd is not None:
507 if self.motd is not None:
501 yield self.motd
508 yield self.motd
502 else:
509 else:
503 yield config('web', 'motd')
510 yield config('web', 'motd')
504
511
505 def config(section, name, default=uimod._unset, untrusted=True):
512 def config(section, name, default=uimod._unset, untrusted=True):
506 return self.ui.config(section, name, default, untrusted)
513 return self.ui.config(section, name, default, untrusted)
507
514
508 self.updatereqenv(wsgireq.env)
515 self.updatereqenv(wsgireq.env)
509
516
510 url = wsgireq.env.get('SCRIPT_NAME', '')
517 url = wsgireq.env.get('SCRIPT_NAME', '')
511 if not url.endswith('/'):
518 if not url.endswith('/'):
512 url += '/'
519 url += '/'
513
520
514 vars = {}
521 vars = {}
515 styles, (style, mapfile) = hgweb_mod.getstyle(wsgireq.req, config,
522 styles, (style, mapfile) = hgweb_mod.getstyle(wsgireq.req, config,
516 self.templatepath)
523 self.templatepath)
517 if style == styles[0]:
524 if style == styles[0]:
518 vars['style'] = style
525 vars['style'] = style
519
526
520 sessionvars = webutil.sessionvars(vars, r'?')
527 sessionvars = webutil.sessionvars(vars, r'?')
521 logourl = config('web', 'logourl')
528 logourl = config('web', 'logourl')
522 logoimg = config('web', 'logoimg')
529 logoimg = config('web', 'logoimg')
523 staticurl = config('web', 'staticurl') or url + 'static/'
530 staticurl = config('web', 'staticurl') or url + 'static/'
524 if not staticurl.endswith('/'):
531 if not staticurl.endswith('/'):
525 staticurl += '/'
532 staticurl += '/'
526
533
527 defaults = {
534 defaults = {
528 "encoding": encoding.encoding,
535 "encoding": encoding.encoding,
529 "motd": motd,
536 "motd": motd,
530 "url": url,
537 "url": url,
531 "logourl": logourl,
538 "logourl": logourl,
532 "logoimg": logoimg,
539 "logoimg": logoimg,
533 "staticurl": staticurl,
540 "staticurl": staticurl,
534 "sessionvars": sessionvars,
541 "sessionvars": sessionvars,
535 "style": style,
542 "style": style,
536 "nonce": nonce,
543 "nonce": nonce,
537 }
544 }
538 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
545 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
539 return tmpl
546 return tmpl
540
547
541 def updatereqenv(self, env):
548 def updatereqenv(self, env):
542 if self._baseurl is not None:
549 if self._baseurl is not None:
543 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
550 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
544 env['SERVER_NAME'] = name
551 env['SERVER_NAME'] = name
545 env['SERVER_PORT'] = port
552 env['SERVER_PORT'] = port
546 env['SCRIPT_NAME'] = path
553 env['SCRIPT_NAME'] = path
@@ -1,1505 +1,1506 b''
1 #
1 #
2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import copy
10 import copy
11 import mimetypes
11 import mimetypes
12 import os
12 import os
13 import re
13 import re
14
14
15 from ..i18n import _
15 from ..i18n import _
16 from ..node import hex, nullid, short
16 from ..node import hex, nullid, short
17
17
18 from .common import (
18 from .common import (
19 ErrorResponse,
19 ErrorResponse,
20 HTTP_FORBIDDEN,
20 HTTP_FORBIDDEN,
21 HTTP_NOT_FOUND,
21 HTTP_NOT_FOUND,
22 HTTP_OK,
22 HTTP_OK,
23 get_contact,
23 get_contact,
24 paritygen,
24 paritygen,
25 staticfile,
25 staticfile,
26 )
26 )
27
27
28 from .. import (
28 from .. import (
29 archival,
29 archival,
30 dagop,
30 dagop,
31 encoding,
31 encoding,
32 error,
32 error,
33 graphmod,
33 graphmod,
34 pycompat,
34 pycompat,
35 revset,
35 revset,
36 revsetlang,
36 revsetlang,
37 scmutil,
37 scmutil,
38 smartset,
38 smartset,
39 templater,
39 templater,
40 util,
40 util,
41 )
41 )
42
42
43 from . import (
43 from . import (
44 webutil,
44 webutil,
45 )
45 )
46
46
47 __all__ = []
47 __all__ = []
48 commands = {}
48 commands = {}
49
49
50 class webcommand(object):
50 class webcommand(object):
51 """Decorator used to register a web command handler.
51 """Decorator used to register a web command handler.
52
52
53 The decorator takes as its positional arguments the name/path the
53 The decorator takes as its positional arguments the name/path the
54 command should be accessible under.
54 command should be accessible under.
55
55
56 When called, functions receive as arguments a ``requestcontext``,
56 When called, functions receive as arguments a ``requestcontext``,
57 ``wsgirequest``, and a templater instance for generatoring output.
57 ``wsgirequest``, and a templater instance for generatoring output.
58 The functions should populate the ``rctx.res`` object with details
58 The functions should populate the ``rctx.res`` object with details
59 about the HTTP response.
59 about the HTTP response.
60
60
61 The function can return the ``requestcontext.res`` instance to signal
61 The function can return the ``requestcontext.res`` instance to signal
62 that it wants to use this object to generate the response. If an iterable
62 that it wants to use this object to generate the response. If an iterable
63 is returned, the ``wsgirequest`` instance will be used and the returned
63 is returned, the ``wsgirequest`` instance will be used and the returned
64 content will constitute the response body.
64 content will constitute the response body.
65
65
66 Usage:
66 Usage:
67
67
68 @webcommand('mycommand')
68 @webcommand('mycommand')
69 def mycommand(web, req, tmpl):
69 def mycommand(web, req, tmpl):
70 pass
70 pass
71 """
71 """
72
72
73 def __init__(self, name):
73 def __init__(self, name):
74 self.name = name
74 self.name = name
75
75
76 def __call__(self, func):
76 def __call__(self, func):
77 __all__.append(self.name)
77 __all__.append(self.name)
78 commands[self.name] = func
78 commands[self.name] = func
79 return func
79 return func
80
80
81 @webcommand('log')
81 @webcommand('log')
82 def log(web, req, tmpl):
82 def log(web, req, tmpl):
83 """
83 """
84 /log[/{revision}[/{path}]]
84 /log[/{revision}[/{path}]]
85 --------------------------
85 --------------------------
86
86
87 Show repository or file history.
87 Show repository or file history.
88
88
89 For URLs of the form ``/log/{revision}``, a list of changesets starting at
89 For URLs of the form ``/log/{revision}``, a list of changesets starting at
90 the specified changeset identifier is shown. If ``{revision}`` is not
90 the specified changeset identifier is shown. If ``{revision}`` is not
91 defined, the default is ``tip``. This form is equivalent to the
91 defined, the default is ``tip``. This form is equivalent to the
92 ``changelog`` handler.
92 ``changelog`` handler.
93
93
94 For URLs of the form ``/log/{revision}/{file}``, the history for a specific
94 For URLs of the form ``/log/{revision}/{file}``, the history for a specific
95 file will be shown. This form is equivalent to the ``filelog`` handler.
95 file will be shown. This form is equivalent to the ``filelog`` handler.
96 """
96 """
97
97
98 if req.req.qsparams.get('file'):
98 if req.req.qsparams.get('file'):
99 return filelog(web, req, tmpl)
99 return filelog(web, req, tmpl)
100 else:
100 else:
101 return changelog(web, req, tmpl)
101 return changelog(web, req, tmpl)
102
102
103 @webcommand('rawfile')
103 @webcommand('rawfile')
104 def rawfile(web, req, tmpl):
104 def rawfile(web, req, tmpl):
105 guessmime = web.configbool('web', 'guessmime')
105 guessmime = web.configbool('web', 'guessmime')
106
106
107 path = webutil.cleanpath(web.repo, req.req.qsparams.get('file', ''))
107 path = webutil.cleanpath(web.repo, req.req.qsparams.get('file', ''))
108 if not path:
108 if not path:
109 return manifest(web, req, tmpl)
109 return manifest(web, req, tmpl)
110
110
111 try:
111 try:
112 fctx = webutil.filectx(web.repo, req)
112 fctx = webutil.filectx(web.repo, req)
113 except error.LookupError as inst:
113 except error.LookupError as inst:
114 try:
114 try:
115 return manifest(web, req, tmpl)
115 return manifest(web, req, tmpl)
116 except ErrorResponse:
116 except ErrorResponse:
117 raise inst
117 raise inst
118
118
119 path = fctx.path()
119 path = fctx.path()
120 text = fctx.data()
120 text = fctx.data()
121 mt = 'application/binary'
121 mt = 'application/binary'
122 if guessmime:
122 if guessmime:
123 mt = mimetypes.guess_type(path)[0]
123 mt = mimetypes.guess_type(path)[0]
124 if mt is None:
124 if mt is None:
125 if util.binary(text):
125 if util.binary(text):
126 mt = 'application/binary'
126 mt = 'application/binary'
127 else:
127 else:
128 mt = 'text/plain'
128 mt = 'text/plain'
129 if mt.startswith('text/'):
129 if mt.startswith('text/'):
130 mt += '; charset="%s"' % encoding.encoding
130 mt += '; charset="%s"' % encoding.encoding
131
131
132 web.res.headers['Content-Type'] = mt
132 web.res.headers['Content-Type'] = mt
133 filename = (path.rpartition('/')[-1]
133 filename = (path.rpartition('/')[-1]
134 .replace('\\', '\\\\').replace('"', '\\"'))
134 .replace('\\', '\\\\').replace('"', '\\"'))
135 web.res.headers['Content-Disposition'] = 'inline; filename="%s"' % filename
135 web.res.headers['Content-Disposition'] = 'inline; filename="%s"' % filename
136 web.res.setbodybytes(text)
136 web.res.setbodybytes(text)
137 return web.res
137 return web.res
138
138
139 def _filerevision(web, req, tmpl, fctx):
139 def _filerevision(web, req, tmpl, fctx):
140 f = fctx.path()
140 f = fctx.path()
141 text = fctx.data()
141 text = fctx.data()
142 parity = paritygen(web.stripecount)
142 parity = paritygen(web.stripecount)
143 ishead = fctx.filerev() in fctx.filelog().headrevs()
143 ishead = fctx.filerev() in fctx.filelog().headrevs()
144
144
145 if util.binary(text):
145 if util.binary(text):
146 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
146 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
147 text = '(binary:%s)' % mt
147 text = '(binary:%s)' % mt
148
148
149 def lines():
149 def lines():
150 for lineno, t in enumerate(text.splitlines(True)):
150 for lineno, t in enumerate(text.splitlines(True)):
151 yield {"line": t,
151 yield {"line": t,
152 "lineid": "l%d" % (lineno + 1),
152 "lineid": "l%d" % (lineno + 1),
153 "linenumber": "% 6d" % (lineno + 1),
153 "linenumber": "% 6d" % (lineno + 1),
154 "parity": next(parity)}
154 "parity": next(parity)}
155
155
156 web.res.setbodygen(tmpl(
156 web.res.setbodygen(tmpl(
157 'filerevision',
157 'filerevision',
158 file=f,
158 file=f,
159 path=webutil.up(f),
159 path=webutil.up(f),
160 text=lines(),
160 text=lines(),
161 symrev=webutil.symrevorshortnode(req, fctx),
161 symrev=webutil.symrevorshortnode(req, fctx),
162 rename=webutil.renamelink(fctx),
162 rename=webutil.renamelink(fctx),
163 permissions=fctx.manifest().flags(f),
163 permissions=fctx.manifest().flags(f),
164 ishead=int(ishead),
164 ishead=int(ishead),
165 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx))))
165 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx))))
166
166
167 return web.res
167 return web.res
168
168
169 @webcommand('file')
169 @webcommand('file')
170 def file(web, req, tmpl):
170 def file(web, req, tmpl):
171 """
171 """
172 /file/{revision}[/{path}]
172 /file/{revision}[/{path}]
173 -------------------------
173 -------------------------
174
174
175 Show information about a directory or file in the repository.
175 Show information about a directory or file in the repository.
176
176
177 Info about the ``path`` given as a URL parameter will be rendered.
177 Info about the ``path`` given as a URL parameter will be rendered.
178
178
179 If ``path`` is a directory, information about the entries in that
179 If ``path`` is a directory, information about the entries in that
180 directory will be rendered. This form is equivalent to the ``manifest``
180 directory will be rendered. This form is equivalent to the ``manifest``
181 handler.
181 handler.
182
182
183 If ``path`` is a file, information about that file will be shown via
183 If ``path`` is a file, information about that file will be shown via
184 the ``filerevision`` template.
184 the ``filerevision`` template.
185
185
186 If ``path`` is not defined, information about the root directory will
186 If ``path`` is not defined, information about the root directory will
187 be rendered.
187 be rendered.
188 """
188 """
189 if web.req.qsparams.get('style') == 'raw':
189 if web.req.qsparams.get('style') == 'raw':
190 return rawfile(web, req, tmpl)
190 return rawfile(web, req, tmpl)
191
191
192 path = webutil.cleanpath(web.repo, req.req.qsparams.get('file', ''))
192 path = webutil.cleanpath(web.repo, req.req.qsparams.get('file', ''))
193 if not path:
193 if not path:
194 return manifest(web, req, tmpl)
194 return manifest(web, req, tmpl)
195 try:
195 try:
196 return _filerevision(web, req, tmpl, webutil.filectx(web.repo, req))
196 return _filerevision(web, req, tmpl, webutil.filectx(web.repo, req))
197 except error.LookupError as inst:
197 except error.LookupError as inst:
198 try:
198 try:
199 return manifest(web, req, tmpl)
199 return manifest(web, req, tmpl)
200 except ErrorResponse:
200 except ErrorResponse:
201 raise inst
201 raise inst
202
202
203 def _search(web, req, tmpl):
203 def _search(web, req, tmpl):
204 MODE_REVISION = 'rev'
204 MODE_REVISION = 'rev'
205 MODE_KEYWORD = 'keyword'
205 MODE_KEYWORD = 'keyword'
206 MODE_REVSET = 'revset'
206 MODE_REVSET = 'revset'
207
207
208 def revsearch(ctx):
208 def revsearch(ctx):
209 yield ctx
209 yield ctx
210
210
211 def keywordsearch(query):
211 def keywordsearch(query):
212 lower = encoding.lower
212 lower = encoding.lower
213 qw = lower(query).split()
213 qw = lower(query).split()
214
214
215 def revgen():
215 def revgen():
216 cl = web.repo.changelog
216 cl = web.repo.changelog
217 for i in xrange(len(web.repo) - 1, 0, -100):
217 for i in xrange(len(web.repo) - 1, 0, -100):
218 l = []
218 l = []
219 for j in cl.revs(max(0, i - 99), i):
219 for j in cl.revs(max(0, i - 99), i):
220 ctx = web.repo[j]
220 ctx = web.repo[j]
221 l.append(ctx)
221 l.append(ctx)
222 l.reverse()
222 l.reverse()
223 for e in l:
223 for e in l:
224 yield e
224 yield e
225
225
226 for ctx in revgen():
226 for ctx in revgen():
227 miss = 0
227 miss = 0
228 for q in qw:
228 for q in qw:
229 if not (q in lower(ctx.user()) or
229 if not (q in lower(ctx.user()) or
230 q in lower(ctx.description()) or
230 q in lower(ctx.description()) or
231 q in lower(" ".join(ctx.files()))):
231 q in lower(" ".join(ctx.files()))):
232 miss = 1
232 miss = 1
233 break
233 break
234 if miss:
234 if miss:
235 continue
235 continue
236
236
237 yield ctx
237 yield ctx
238
238
239 def revsetsearch(revs):
239 def revsetsearch(revs):
240 for r in revs:
240 for r in revs:
241 yield web.repo[r]
241 yield web.repo[r]
242
242
243 searchfuncs = {
243 searchfuncs = {
244 MODE_REVISION: (revsearch, 'exact revision search'),
244 MODE_REVISION: (revsearch, 'exact revision search'),
245 MODE_KEYWORD: (keywordsearch, 'literal keyword search'),
245 MODE_KEYWORD: (keywordsearch, 'literal keyword search'),
246 MODE_REVSET: (revsetsearch, 'revset expression search'),
246 MODE_REVSET: (revsetsearch, 'revset expression search'),
247 }
247 }
248
248
249 def getsearchmode(query):
249 def getsearchmode(query):
250 try:
250 try:
251 ctx = web.repo[query]
251 ctx = web.repo[query]
252 except (error.RepoError, error.LookupError):
252 except (error.RepoError, error.LookupError):
253 # query is not an exact revision pointer, need to
253 # query is not an exact revision pointer, need to
254 # decide if it's a revset expression or keywords
254 # decide if it's a revset expression or keywords
255 pass
255 pass
256 else:
256 else:
257 return MODE_REVISION, ctx
257 return MODE_REVISION, ctx
258
258
259 revdef = 'reverse(%s)' % query
259 revdef = 'reverse(%s)' % query
260 try:
260 try:
261 tree = revsetlang.parse(revdef)
261 tree = revsetlang.parse(revdef)
262 except error.ParseError:
262 except error.ParseError:
263 # can't parse to a revset tree
263 # can't parse to a revset tree
264 return MODE_KEYWORD, query
264 return MODE_KEYWORD, query
265
265
266 if revsetlang.depth(tree) <= 2:
266 if revsetlang.depth(tree) <= 2:
267 # no revset syntax used
267 # no revset syntax used
268 return MODE_KEYWORD, query
268 return MODE_KEYWORD, query
269
269
270 if any((token, (value or '')[:3]) == ('string', 're:')
270 if any((token, (value or '')[:3]) == ('string', 're:')
271 for token, value, pos in revsetlang.tokenize(revdef)):
271 for token, value, pos in revsetlang.tokenize(revdef)):
272 return MODE_KEYWORD, query
272 return MODE_KEYWORD, query
273
273
274 funcsused = revsetlang.funcsused(tree)
274 funcsused = revsetlang.funcsused(tree)
275 if not funcsused.issubset(revset.safesymbols):
275 if not funcsused.issubset(revset.safesymbols):
276 return MODE_KEYWORD, query
276 return MODE_KEYWORD, query
277
277
278 mfunc = revset.match(web.repo.ui, revdef, repo=web.repo)
278 mfunc = revset.match(web.repo.ui, revdef, repo=web.repo)
279 try:
279 try:
280 revs = mfunc(web.repo)
280 revs = mfunc(web.repo)
281 return MODE_REVSET, revs
281 return MODE_REVSET, revs
282 # ParseError: wrongly placed tokens, wrongs arguments, etc
282 # ParseError: wrongly placed tokens, wrongs arguments, etc
283 # RepoLookupError: no such revision, e.g. in 'revision:'
283 # RepoLookupError: no such revision, e.g. in 'revision:'
284 # Abort: bookmark/tag not exists
284 # Abort: bookmark/tag not exists
285 # LookupError: ambiguous identifier, e.g. in '(bc)' on a large repo
285 # LookupError: ambiguous identifier, e.g. in '(bc)' on a large repo
286 except (error.ParseError, error.RepoLookupError, error.Abort,
286 except (error.ParseError, error.RepoLookupError, error.Abort,
287 LookupError):
287 LookupError):
288 return MODE_KEYWORD, query
288 return MODE_KEYWORD, query
289
289
290 def changelist(**map):
290 def changelist(**map):
291 count = 0
291 count = 0
292
292
293 for ctx in searchfunc[0](funcarg):
293 for ctx in searchfunc[0](funcarg):
294 count += 1
294 count += 1
295 n = ctx.node()
295 n = ctx.node()
296 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
296 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
297 files = webutil.listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
297 files = webutil.listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
298
298
299 yield tmpl('searchentry',
299 yield tmpl('searchentry',
300 parity=next(parity),
300 parity=next(parity),
301 changelogtag=showtags,
301 changelogtag=showtags,
302 files=files,
302 files=files,
303 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
303 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
304
304
305 if count >= revcount:
305 if count >= revcount:
306 break
306 break
307
307
308 query = req.req.qsparams['rev']
308 query = req.req.qsparams['rev']
309 revcount = web.maxchanges
309 revcount = web.maxchanges
310 if 'revcount' in req.req.qsparams:
310 if 'revcount' in req.req.qsparams:
311 try:
311 try:
312 revcount = int(req.req.qsparams.get('revcount', revcount))
312 revcount = int(req.req.qsparams.get('revcount', revcount))
313 revcount = max(revcount, 1)
313 revcount = max(revcount, 1)
314 tmpl.defaults['sessionvars']['revcount'] = revcount
314 tmpl.defaults['sessionvars']['revcount'] = revcount
315 except ValueError:
315 except ValueError:
316 pass
316 pass
317
317
318 lessvars = copy.copy(tmpl.defaults['sessionvars'])
318 lessvars = copy.copy(tmpl.defaults['sessionvars'])
319 lessvars['revcount'] = max(revcount // 2, 1)
319 lessvars['revcount'] = max(revcount // 2, 1)
320 lessvars['rev'] = query
320 lessvars['rev'] = query
321 morevars = copy.copy(tmpl.defaults['sessionvars'])
321 morevars = copy.copy(tmpl.defaults['sessionvars'])
322 morevars['revcount'] = revcount * 2
322 morevars['revcount'] = revcount * 2
323 morevars['rev'] = query
323 morevars['rev'] = query
324
324
325 mode, funcarg = getsearchmode(query)
325 mode, funcarg = getsearchmode(query)
326
326
327 if 'forcekw' in req.req.qsparams:
327 if 'forcekw' in req.req.qsparams:
328 showforcekw = ''
328 showforcekw = ''
329 showunforcekw = searchfuncs[mode][1]
329 showunforcekw = searchfuncs[mode][1]
330 mode = MODE_KEYWORD
330 mode = MODE_KEYWORD
331 funcarg = query
331 funcarg = query
332 else:
332 else:
333 if mode != MODE_KEYWORD:
333 if mode != MODE_KEYWORD:
334 showforcekw = searchfuncs[MODE_KEYWORD][1]
334 showforcekw = searchfuncs[MODE_KEYWORD][1]
335 else:
335 else:
336 showforcekw = ''
336 showforcekw = ''
337 showunforcekw = ''
337 showunforcekw = ''
338
338
339 searchfunc = searchfuncs[mode]
339 searchfunc = searchfuncs[mode]
340
340
341 tip = web.repo['tip']
341 tip = web.repo['tip']
342 parity = paritygen(web.stripecount)
342 parity = paritygen(web.stripecount)
343
343
344 web.res.setbodygen(tmpl(
344 web.res.setbodygen(tmpl(
345 'search',
345 'search',
346 query=query,
346 query=query,
347 node=tip.hex(),
347 node=tip.hex(),
348 symrev='tip',
348 symrev='tip',
349 entries=changelist,
349 entries=changelist,
350 archives=web.archivelist('tip'),
350 archives=web.archivelist('tip'),
351 morevars=morevars,
351 morevars=morevars,
352 lessvars=lessvars,
352 lessvars=lessvars,
353 modedesc=searchfunc[1],
353 modedesc=searchfunc[1],
354 showforcekw=showforcekw,
354 showforcekw=showforcekw,
355 showunforcekw=showunforcekw))
355 showunforcekw=showunforcekw))
356
356
357 return web.res
357 return web.res
358
358
359 @webcommand('changelog')
359 @webcommand('changelog')
360 def changelog(web, req, tmpl, shortlog=False):
360 def changelog(web, req, tmpl, shortlog=False):
361 """
361 """
362 /changelog[/{revision}]
362 /changelog[/{revision}]
363 -----------------------
363 -----------------------
364
364
365 Show information about multiple changesets.
365 Show information about multiple changesets.
366
366
367 If the optional ``revision`` URL argument is absent, information about
367 If the optional ``revision`` URL argument is absent, information about
368 all changesets starting at ``tip`` will be rendered. If the ``revision``
368 all changesets starting at ``tip`` will be rendered. If the ``revision``
369 argument is present, changesets will be shown starting from the specified
369 argument is present, changesets will be shown starting from the specified
370 revision.
370 revision.
371
371
372 If ``revision`` is absent, the ``rev`` query string argument may be
372 If ``revision`` is absent, the ``rev`` query string argument may be
373 defined. This will perform a search for changesets.
373 defined. This will perform a search for changesets.
374
374
375 The argument for ``rev`` can be a single revision, a revision set,
375 The argument for ``rev`` can be a single revision, a revision set,
376 or a literal keyword to search for in changeset data (equivalent to
376 or a literal keyword to search for in changeset data (equivalent to
377 :hg:`log -k`).
377 :hg:`log -k`).
378
378
379 The ``revcount`` query string argument defines the maximum numbers of
379 The ``revcount`` query string argument defines the maximum numbers of
380 changesets to render.
380 changesets to render.
381
381
382 For non-searches, the ``changelog`` template will be rendered.
382 For non-searches, the ``changelog`` template will be rendered.
383 """
383 """
384
384
385 query = ''
385 query = ''
386 if 'node' in req.req.qsparams:
386 if 'node' in req.req.qsparams:
387 ctx = webutil.changectx(web.repo, req)
387 ctx = webutil.changectx(web.repo, req)
388 symrev = webutil.symrevorshortnode(req, ctx)
388 symrev = webutil.symrevorshortnode(req, ctx)
389 elif 'rev' in req.req.qsparams:
389 elif 'rev' in req.req.qsparams:
390 return _search(web, req, tmpl)
390 return _search(web, req, tmpl)
391 else:
391 else:
392 ctx = web.repo['tip']
392 ctx = web.repo['tip']
393 symrev = 'tip'
393 symrev = 'tip'
394
394
395 def changelist():
395 def changelist():
396 revs = []
396 revs = []
397 if pos != -1:
397 if pos != -1:
398 revs = web.repo.changelog.revs(pos, 0)
398 revs = web.repo.changelog.revs(pos, 0)
399 curcount = 0
399 curcount = 0
400 for rev in revs:
400 for rev in revs:
401 curcount += 1
401 curcount += 1
402 if curcount > revcount + 1:
402 if curcount > revcount + 1:
403 break
403 break
404
404
405 entry = webutil.changelistentry(web, web.repo[rev], tmpl)
405 entry = webutil.changelistentry(web, web.repo[rev], tmpl)
406 entry['parity'] = next(parity)
406 entry['parity'] = next(parity)
407 yield entry
407 yield entry
408
408
409 if shortlog:
409 if shortlog:
410 revcount = web.maxshortchanges
410 revcount = web.maxshortchanges
411 else:
411 else:
412 revcount = web.maxchanges
412 revcount = web.maxchanges
413
413
414 if 'revcount' in req.req.qsparams:
414 if 'revcount' in req.req.qsparams:
415 try:
415 try:
416 revcount = int(req.req.qsparams.get('revcount', revcount))
416 revcount = int(req.req.qsparams.get('revcount', revcount))
417 revcount = max(revcount, 1)
417 revcount = max(revcount, 1)
418 tmpl.defaults['sessionvars']['revcount'] = revcount
418 tmpl.defaults['sessionvars']['revcount'] = revcount
419 except ValueError:
419 except ValueError:
420 pass
420 pass
421
421
422 lessvars = copy.copy(tmpl.defaults['sessionvars'])
422 lessvars = copy.copy(tmpl.defaults['sessionvars'])
423 lessvars['revcount'] = max(revcount // 2, 1)
423 lessvars['revcount'] = max(revcount // 2, 1)
424 morevars = copy.copy(tmpl.defaults['sessionvars'])
424 morevars = copy.copy(tmpl.defaults['sessionvars'])
425 morevars['revcount'] = revcount * 2
425 morevars['revcount'] = revcount * 2
426
426
427 count = len(web.repo)
427 count = len(web.repo)
428 pos = ctx.rev()
428 pos = ctx.rev()
429 parity = paritygen(web.stripecount)
429 parity = paritygen(web.stripecount)
430
430
431 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
431 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
432
432
433 entries = list(changelist())
433 entries = list(changelist())
434 latestentry = entries[:1]
434 latestentry = entries[:1]
435 if len(entries) > revcount:
435 if len(entries) > revcount:
436 nextentry = entries[-1:]
436 nextentry = entries[-1:]
437 entries = entries[:-1]
437 entries = entries[:-1]
438 else:
438 else:
439 nextentry = []
439 nextentry = []
440
440
441 web.res.setbodygen(tmpl(
441 web.res.setbodygen(tmpl(
442 'shortlog' if shortlog else 'changelog',
442 'shortlog' if shortlog else 'changelog',
443 changenav=changenav,
443 changenav=changenav,
444 node=ctx.hex(),
444 node=ctx.hex(),
445 rev=pos,
445 rev=pos,
446 symrev=symrev,
446 symrev=symrev,
447 changesets=count,
447 changesets=count,
448 entries=entries,
448 entries=entries,
449 latestentry=latestentry,
449 latestentry=latestentry,
450 nextentry=nextentry,
450 nextentry=nextentry,
451 archives=web.archivelist('tip'),
451 archives=web.archivelist('tip'),
452 revcount=revcount,
452 revcount=revcount,
453 morevars=morevars,
453 morevars=morevars,
454 lessvars=lessvars,
454 lessvars=lessvars,
455 query=query))
455 query=query))
456
456
457 return web.res
457 return web.res
458
458
459 @webcommand('shortlog')
459 @webcommand('shortlog')
460 def shortlog(web, req, tmpl):
460 def shortlog(web, req, tmpl):
461 """
461 """
462 /shortlog
462 /shortlog
463 ---------
463 ---------
464
464
465 Show basic information about a set of changesets.
465 Show basic information about a set of changesets.
466
466
467 This accepts the same parameters as the ``changelog`` handler. The only
467 This accepts the same parameters as the ``changelog`` handler. The only
468 difference is the ``shortlog`` template will be rendered instead of the
468 difference is the ``shortlog`` template will be rendered instead of the
469 ``changelog`` template.
469 ``changelog`` template.
470 """
470 """
471 return changelog(web, req, tmpl, shortlog=True)
471 return changelog(web, req, tmpl, shortlog=True)
472
472
473 @webcommand('changeset')
473 @webcommand('changeset')
474 def changeset(web, req, tmpl):
474 def changeset(web, req, tmpl):
475 """
475 """
476 /changeset[/{revision}]
476 /changeset[/{revision}]
477 -----------------------
477 -----------------------
478
478
479 Show information about a single changeset.
479 Show information about a single changeset.
480
480
481 A URL path argument is the changeset identifier to show. See ``hg help
481 A URL path argument is the changeset identifier to show. See ``hg help
482 revisions`` for possible values. If not defined, the ``tip`` changeset
482 revisions`` for possible values. If not defined, the ``tip`` changeset
483 will be shown.
483 will be shown.
484
484
485 The ``changeset`` template is rendered. Contents of the ``changesettag``,
485 The ``changeset`` template is rendered. Contents of the ``changesettag``,
486 ``changesetbookmark``, ``filenodelink``, ``filenolink``, and the many
486 ``changesetbookmark``, ``filenodelink``, ``filenolink``, and the many
487 templates related to diffs may all be used to produce the output.
487 templates related to diffs may all be used to produce the output.
488 """
488 """
489 ctx = webutil.changectx(web.repo, req)
489 ctx = webutil.changectx(web.repo, req)
490 web.res.setbodygen(tmpl('changeset',
490 web.res.setbodygen(tmpl('changeset',
491 **webutil.changesetentry(web, req, tmpl, ctx)))
491 **webutil.changesetentry(web, req, tmpl, ctx)))
492 return web.res
492 return web.res
493
493
494 rev = webcommand('rev')(changeset)
494 rev = webcommand('rev')(changeset)
495
495
496 def decodepath(path):
496 def decodepath(path):
497 """Hook for mapping a path in the repository to a path in the
497 """Hook for mapping a path in the repository to a path in the
498 working copy.
498 working copy.
499
499
500 Extensions (e.g., largefiles) can override this to remap files in
500 Extensions (e.g., largefiles) can override this to remap files in
501 the virtual file system presented by the manifest command below."""
501 the virtual file system presented by the manifest command below."""
502 return path
502 return path
503
503
504 @webcommand('manifest')
504 @webcommand('manifest')
505 def manifest(web, req, tmpl):
505 def manifest(web, req, tmpl):
506 """
506 """
507 /manifest[/{revision}[/{path}]]
507 /manifest[/{revision}[/{path}]]
508 -------------------------------
508 -------------------------------
509
509
510 Show information about a directory.
510 Show information about a directory.
511
511
512 If the URL path arguments are omitted, information about the root
512 If the URL path arguments are omitted, information about the root
513 directory for the ``tip`` changeset will be shown.
513 directory for the ``tip`` changeset will be shown.
514
514
515 Because this handler can only show information for directories, it
515 Because this handler can only show information for directories, it
516 is recommended to use the ``file`` handler instead, as it can handle both
516 is recommended to use the ``file`` handler instead, as it can handle both
517 directories and files.
517 directories and files.
518
518
519 The ``manifest`` template will be rendered for this handler.
519 The ``manifest`` template will be rendered for this handler.
520 """
520 """
521 if 'node' in req.req.qsparams:
521 if 'node' in req.req.qsparams:
522 ctx = webutil.changectx(web.repo, req)
522 ctx = webutil.changectx(web.repo, req)
523 symrev = webutil.symrevorshortnode(req, ctx)
523 symrev = webutil.symrevorshortnode(req, ctx)
524 else:
524 else:
525 ctx = web.repo['tip']
525 ctx = web.repo['tip']
526 symrev = 'tip'
526 symrev = 'tip'
527 path = webutil.cleanpath(web.repo, req.req.qsparams.get('file', ''))
527 path = webutil.cleanpath(web.repo, req.req.qsparams.get('file', ''))
528 mf = ctx.manifest()
528 mf = ctx.manifest()
529 node = ctx.node()
529 node = ctx.node()
530
530
531 files = {}
531 files = {}
532 dirs = {}
532 dirs = {}
533 parity = paritygen(web.stripecount)
533 parity = paritygen(web.stripecount)
534
534
535 if path and path[-1:] != "/":
535 if path and path[-1:] != "/":
536 path += "/"
536 path += "/"
537 l = len(path)
537 l = len(path)
538 abspath = "/" + path
538 abspath = "/" + path
539
539
540 for full, n in mf.iteritems():
540 for full, n in mf.iteritems():
541 # the virtual path (working copy path) used for the full
541 # the virtual path (working copy path) used for the full
542 # (repository) path
542 # (repository) path
543 f = decodepath(full)
543 f = decodepath(full)
544
544
545 if f[:l] != path:
545 if f[:l] != path:
546 continue
546 continue
547 remain = f[l:]
547 remain = f[l:]
548 elements = remain.split('/')
548 elements = remain.split('/')
549 if len(elements) == 1:
549 if len(elements) == 1:
550 files[remain] = full
550 files[remain] = full
551 else:
551 else:
552 h = dirs # need to retain ref to dirs (root)
552 h = dirs # need to retain ref to dirs (root)
553 for elem in elements[0:-1]:
553 for elem in elements[0:-1]:
554 if elem not in h:
554 if elem not in h:
555 h[elem] = {}
555 h[elem] = {}
556 h = h[elem]
556 h = h[elem]
557 if len(h) > 1:
557 if len(h) > 1:
558 break
558 break
559 h[None] = None # denotes files present
559 h[None] = None # denotes files present
560
560
561 if mf and not files and not dirs:
561 if mf and not files and not dirs:
562 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
562 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
563
563
564 def filelist(**map):
564 def filelist(**map):
565 for f in sorted(files):
565 for f in sorted(files):
566 full = files[f]
566 full = files[f]
567
567
568 fctx = ctx.filectx(full)
568 fctx = ctx.filectx(full)
569 yield {"file": full,
569 yield {"file": full,
570 "parity": next(parity),
570 "parity": next(parity),
571 "basename": f,
571 "basename": f,
572 "date": fctx.date(),
572 "date": fctx.date(),
573 "size": fctx.size(),
573 "size": fctx.size(),
574 "permissions": mf.flags(full)}
574 "permissions": mf.flags(full)}
575
575
576 def dirlist(**map):
576 def dirlist(**map):
577 for d in sorted(dirs):
577 for d in sorted(dirs):
578
578
579 emptydirs = []
579 emptydirs = []
580 h = dirs[d]
580 h = dirs[d]
581 while isinstance(h, dict) and len(h) == 1:
581 while isinstance(h, dict) and len(h) == 1:
582 k, v = next(iter(h.items()))
582 k, v = next(iter(h.items()))
583 if v:
583 if v:
584 emptydirs.append(k)
584 emptydirs.append(k)
585 h = v
585 h = v
586
586
587 path = "%s%s" % (abspath, d)
587 path = "%s%s" % (abspath, d)
588 yield {"parity": next(parity),
588 yield {"parity": next(parity),
589 "path": path,
589 "path": path,
590 "emptydirs": "/".join(emptydirs),
590 "emptydirs": "/".join(emptydirs),
591 "basename": d}
591 "basename": d}
592
592
593 web.res.setbodygen(tmpl(
593 web.res.setbodygen(tmpl(
594 'manifest',
594 'manifest',
595 symrev=symrev,
595 symrev=symrev,
596 path=abspath,
596 path=abspath,
597 up=webutil.up(abspath),
597 up=webutil.up(abspath),
598 upparity=next(parity),
598 upparity=next(parity),
599 fentries=filelist,
599 fentries=filelist,
600 dentries=dirlist,
600 dentries=dirlist,
601 archives=web.archivelist(hex(node)),
601 archives=web.archivelist(hex(node)),
602 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))))
602 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))))
603
603
604 return web.res
604 return web.res
605
605
606 @webcommand('tags')
606 @webcommand('tags')
607 def tags(web, req, tmpl):
607 def tags(web, req, tmpl):
608 """
608 """
609 /tags
609 /tags
610 -----
610 -----
611
611
612 Show information about tags.
612 Show information about tags.
613
613
614 No arguments are accepted.
614 No arguments are accepted.
615
615
616 The ``tags`` template is rendered.
616 The ``tags`` template is rendered.
617 """
617 """
618 i = list(reversed(web.repo.tagslist()))
618 i = list(reversed(web.repo.tagslist()))
619 parity = paritygen(web.stripecount)
619 parity = paritygen(web.stripecount)
620
620
621 def entries(notip, latestonly, **map):
621 def entries(notip, latestonly, **map):
622 t = i
622 t = i
623 if notip:
623 if notip:
624 t = [(k, n) for k, n in i if k != "tip"]
624 t = [(k, n) for k, n in i if k != "tip"]
625 if latestonly:
625 if latestonly:
626 t = t[:1]
626 t = t[:1]
627 for k, n in t:
627 for k, n in t:
628 yield {"parity": next(parity),
628 yield {"parity": next(parity),
629 "tag": k,
629 "tag": k,
630 "date": web.repo[n].date(),
630 "date": web.repo[n].date(),
631 "node": hex(n)}
631 "node": hex(n)}
632
632
633 web.res.setbodygen(tmpl(
633 web.res.setbodygen(tmpl(
634 'tags',
634 'tags',
635 node=hex(web.repo.changelog.tip()),
635 node=hex(web.repo.changelog.tip()),
636 entries=lambda **x: entries(False, False, **x),
636 entries=lambda **x: entries(False, False, **x),
637 entriesnotip=lambda **x: entries(True, False, **x),
637 entriesnotip=lambda **x: entries(True, False, **x),
638 latestentry=lambda **x: entries(True, True, **x)))
638 latestentry=lambda **x: entries(True, True, **x)))
639
639
640 return web.res
640 return web.res
641
641
642 @webcommand('bookmarks')
642 @webcommand('bookmarks')
643 def bookmarks(web, req, tmpl):
643 def bookmarks(web, req, tmpl):
644 """
644 """
645 /bookmarks
645 /bookmarks
646 ----------
646 ----------
647
647
648 Show information about bookmarks.
648 Show information about bookmarks.
649
649
650 No arguments are accepted.
650 No arguments are accepted.
651
651
652 The ``bookmarks`` template is rendered.
652 The ``bookmarks`` template is rendered.
653 """
653 """
654 i = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
654 i = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
655 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
655 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
656 i = sorted(i, key=sortkey, reverse=True)
656 i = sorted(i, key=sortkey, reverse=True)
657 parity = paritygen(web.stripecount)
657 parity = paritygen(web.stripecount)
658
658
659 def entries(latestonly, **map):
659 def entries(latestonly, **map):
660 t = i
660 t = i
661 if latestonly:
661 if latestonly:
662 t = i[:1]
662 t = i[:1]
663 for k, n in t:
663 for k, n in t:
664 yield {"parity": next(parity),
664 yield {"parity": next(parity),
665 "bookmark": k,
665 "bookmark": k,
666 "date": web.repo[n].date(),
666 "date": web.repo[n].date(),
667 "node": hex(n)}
667 "node": hex(n)}
668
668
669 if i:
669 if i:
670 latestrev = i[0][1]
670 latestrev = i[0][1]
671 else:
671 else:
672 latestrev = -1
672 latestrev = -1
673
673
674 web.res.setbodygen(tmpl(
674 web.res.setbodygen(tmpl(
675 'bookmarks',
675 'bookmarks',
676 node=hex(web.repo.changelog.tip()),
676 node=hex(web.repo.changelog.tip()),
677 lastchange=[{'date': web.repo[latestrev].date()}],
677 lastchange=[{'date': web.repo[latestrev].date()}],
678 entries=lambda **x: entries(latestonly=False, **x),
678 entries=lambda **x: entries(latestonly=False, **x),
679 latestentry=lambda **x: entries(latestonly=True, **x)))
679 latestentry=lambda **x: entries(latestonly=True, **x)))
680
680
681 return web.res
681 return web.res
682
682
683 @webcommand('branches')
683 @webcommand('branches')
684 def branches(web, req, tmpl):
684 def branches(web, req, tmpl):
685 """
685 """
686 /branches
686 /branches
687 ---------
687 ---------
688
688
689 Show information about branches.
689 Show information about branches.
690
690
691 All known branches are contained in the output, even closed branches.
691 All known branches are contained in the output, even closed branches.
692
692
693 No arguments are accepted.
693 No arguments are accepted.
694
694
695 The ``branches`` template is rendered.
695 The ``branches`` template is rendered.
696 """
696 """
697 entries = webutil.branchentries(web.repo, web.stripecount)
697 entries = webutil.branchentries(web.repo, web.stripecount)
698 latestentry = webutil.branchentries(web.repo, web.stripecount, 1)
698 latestentry = webutil.branchentries(web.repo, web.stripecount, 1)
699
699
700 web.res.setbodygen(tmpl(
700 web.res.setbodygen(tmpl(
701 'branches',
701 'branches',
702 node=hex(web.repo.changelog.tip()),
702 node=hex(web.repo.changelog.tip()),
703 entries=entries,
703 entries=entries,
704 latestentry=latestentry))
704 latestentry=latestentry))
705
705
706 return web.res
706 return web.res
707
707
708 @webcommand('summary')
708 @webcommand('summary')
709 def summary(web, req, tmpl):
709 def summary(web, req, tmpl):
710 """
710 """
711 /summary
711 /summary
712 --------
712 --------
713
713
714 Show a summary of repository state.
714 Show a summary of repository state.
715
715
716 Information about the latest changesets, bookmarks, tags, and branches
716 Information about the latest changesets, bookmarks, tags, and branches
717 is captured by this handler.
717 is captured by this handler.
718
718
719 The ``summary`` template is rendered.
719 The ``summary`` template is rendered.
720 """
720 """
721 i = reversed(web.repo.tagslist())
721 i = reversed(web.repo.tagslist())
722
722
723 def tagentries(**map):
723 def tagentries(**map):
724 parity = paritygen(web.stripecount)
724 parity = paritygen(web.stripecount)
725 count = 0
725 count = 0
726 for k, n in i:
726 for k, n in i:
727 if k == "tip": # skip tip
727 if k == "tip": # skip tip
728 continue
728 continue
729
729
730 count += 1
730 count += 1
731 if count > 10: # limit to 10 tags
731 if count > 10: # limit to 10 tags
732 break
732 break
733
733
734 yield tmpl("tagentry",
734 yield tmpl("tagentry",
735 parity=next(parity),
735 parity=next(parity),
736 tag=k,
736 tag=k,
737 node=hex(n),
737 node=hex(n),
738 date=web.repo[n].date())
738 date=web.repo[n].date())
739
739
740 def bookmarks(**map):
740 def bookmarks(**map):
741 parity = paritygen(web.stripecount)
741 parity = paritygen(web.stripecount)
742 marks = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
742 marks = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
743 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
743 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
744 marks = sorted(marks, key=sortkey, reverse=True)
744 marks = sorted(marks, key=sortkey, reverse=True)
745 for k, n in marks[:10]: # limit to 10 bookmarks
745 for k, n in marks[:10]: # limit to 10 bookmarks
746 yield {'parity': next(parity),
746 yield {'parity': next(parity),
747 'bookmark': k,
747 'bookmark': k,
748 'date': web.repo[n].date(),
748 'date': web.repo[n].date(),
749 'node': hex(n)}
749 'node': hex(n)}
750
750
751 def changelist(**map):
751 def changelist(**map):
752 parity = paritygen(web.stripecount, offset=start - end)
752 parity = paritygen(web.stripecount, offset=start - end)
753 l = [] # build a list in forward order for efficiency
753 l = [] # build a list in forward order for efficiency
754 revs = []
754 revs = []
755 if start < end:
755 if start < end:
756 revs = web.repo.changelog.revs(start, end - 1)
756 revs = web.repo.changelog.revs(start, end - 1)
757 for i in revs:
757 for i in revs:
758 ctx = web.repo[i]
758 ctx = web.repo[i]
759
759
760 l.append(tmpl(
760 l.append(tmpl(
761 'shortlogentry',
761 'shortlogentry',
762 parity=next(parity),
762 parity=next(parity),
763 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))))
763 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))))
764
764
765 for entry in reversed(l):
765 for entry in reversed(l):
766 yield entry
766 yield entry
767
767
768 tip = web.repo['tip']
768 tip = web.repo['tip']
769 count = len(web.repo)
769 count = len(web.repo)
770 start = max(0, count - web.maxchanges)
770 start = max(0, count - web.maxchanges)
771 end = min(count, start + web.maxchanges)
771 end = min(count, start + web.maxchanges)
772
772
773 desc = web.config("web", "description")
773 desc = web.config("web", "description")
774 if not desc:
774 if not desc:
775 desc = 'unknown'
775 desc = 'unknown'
776
776
777 web.res.setbodygen(tmpl(
777 web.res.setbodygen(tmpl(
778 'summary',
778 'summary',
779 desc=desc,
779 desc=desc,
780 owner=get_contact(web.config) or 'unknown',
780 owner=get_contact(web.config) or 'unknown',
781 lastchange=tip.date(),
781 lastchange=tip.date(),
782 tags=tagentries,
782 tags=tagentries,
783 bookmarks=bookmarks,
783 bookmarks=bookmarks,
784 branches=webutil.branchentries(web.repo, web.stripecount, 10),
784 branches=webutil.branchentries(web.repo, web.stripecount, 10),
785 shortlog=changelist,
785 shortlog=changelist,
786 node=tip.hex(),
786 node=tip.hex(),
787 symrev='tip',
787 symrev='tip',
788 archives=web.archivelist('tip'),
788 archives=web.archivelist('tip'),
789 labels=web.configlist('web', 'labels')))
789 labels=web.configlist('web', 'labels')))
790
790
791 return web.res
791 return web.res
792
792
793 @webcommand('filediff')
793 @webcommand('filediff')
794 def filediff(web, req, tmpl):
794 def filediff(web, req, tmpl):
795 """
795 """
796 /diff/{revision}/{path}
796 /diff/{revision}/{path}
797 -----------------------
797 -----------------------
798
798
799 Show how a file changed in a particular commit.
799 Show how a file changed in a particular commit.
800
800
801 The ``filediff`` template is rendered.
801 The ``filediff`` template is rendered.
802
802
803 This handler is registered under both the ``/diff`` and ``/filediff``
803 This handler is registered under both the ``/diff`` and ``/filediff``
804 paths. ``/diff`` is used in modern code.
804 paths. ``/diff`` is used in modern code.
805 """
805 """
806 fctx, ctx = None, None
806 fctx, ctx = None, None
807 try:
807 try:
808 fctx = webutil.filectx(web.repo, req)
808 fctx = webutil.filectx(web.repo, req)
809 except LookupError:
809 except LookupError:
810 ctx = webutil.changectx(web.repo, req)
810 ctx = webutil.changectx(web.repo, req)
811 path = webutil.cleanpath(web.repo, req.req.qsparams['file'])
811 path = webutil.cleanpath(web.repo, req.req.qsparams['file'])
812 if path not in ctx.files():
812 if path not in ctx.files():
813 raise
813 raise
814
814
815 if fctx is not None:
815 if fctx is not None:
816 path = fctx.path()
816 path = fctx.path()
817 ctx = fctx.changectx()
817 ctx = fctx.changectx()
818 basectx = ctx.p1()
818 basectx = ctx.p1()
819
819
820 style = web.config('web', 'style')
820 style = web.config('web', 'style')
821 if 'style' in req.req.qsparams:
821 if 'style' in req.req.qsparams:
822 style = req.req.qsparams['style']
822 style = req.req.qsparams['style']
823
823
824 diffs = webutil.diffs(web, tmpl, ctx, basectx, [path], style)
824 diffs = webutil.diffs(web, tmpl, ctx, basectx, [path], style)
825 if fctx is not None:
825 if fctx is not None:
826 rename = webutil.renamelink(fctx)
826 rename = webutil.renamelink(fctx)
827 ctx = fctx
827 ctx = fctx
828 else:
828 else:
829 rename = []
829 rename = []
830 ctx = ctx
830 ctx = ctx
831
831
832 web.res.setbodygen(tmpl(
832 web.res.setbodygen(tmpl(
833 'filediff',
833 'filediff',
834 file=path,
834 file=path,
835 symrev=webutil.symrevorshortnode(req, ctx),
835 symrev=webutil.symrevorshortnode(req, ctx),
836 rename=rename,
836 rename=rename,
837 diff=diffs,
837 diff=diffs,
838 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))))
838 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))))
839
839
840 return web.res
840 return web.res
841
841
842 diff = webcommand('diff')(filediff)
842 diff = webcommand('diff')(filediff)
843
843
844 @webcommand('comparison')
844 @webcommand('comparison')
845 def comparison(web, req, tmpl):
845 def comparison(web, req, tmpl):
846 """
846 """
847 /comparison/{revision}/{path}
847 /comparison/{revision}/{path}
848 -----------------------------
848 -----------------------------
849
849
850 Show a comparison between the old and new versions of a file from changes
850 Show a comparison between the old and new versions of a file from changes
851 made on a particular revision.
851 made on a particular revision.
852
852
853 This is similar to the ``diff`` handler. However, this form features
853 This is similar to the ``diff`` handler. However, this form features
854 a split or side-by-side diff rather than a unified diff.
854 a split or side-by-side diff rather than a unified diff.
855
855
856 The ``context`` query string argument can be used to control the lines of
856 The ``context`` query string argument can be used to control the lines of
857 context in the diff.
857 context in the diff.
858
858
859 The ``filecomparison`` template is rendered.
859 The ``filecomparison`` template is rendered.
860 """
860 """
861 ctx = webutil.changectx(web.repo, req)
861 ctx = webutil.changectx(web.repo, req)
862 if 'file' not in req.req.qsparams:
862 if 'file' not in req.req.qsparams:
863 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
863 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
864 path = webutil.cleanpath(web.repo, req.req.qsparams['file'])
864 path = webutil.cleanpath(web.repo, req.req.qsparams['file'])
865
865
866 parsecontext = lambda v: v == 'full' and -1 or int(v)
866 parsecontext = lambda v: v == 'full' and -1 or int(v)
867 if 'context' in req.req.qsparams:
867 if 'context' in req.req.qsparams:
868 context = parsecontext(req.req.qsparams['context'])
868 context = parsecontext(req.req.qsparams['context'])
869 else:
869 else:
870 context = parsecontext(web.config('web', 'comparisoncontext', '5'))
870 context = parsecontext(web.config('web', 'comparisoncontext', '5'))
871
871
872 def filelines(f):
872 def filelines(f):
873 if f.isbinary():
873 if f.isbinary():
874 mt = mimetypes.guess_type(f.path())[0]
874 mt = mimetypes.guess_type(f.path())[0]
875 if not mt:
875 if not mt:
876 mt = 'application/octet-stream'
876 mt = 'application/octet-stream'
877 return [_('(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
877 return [_('(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
878 return f.data().splitlines()
878 return f.data().splitlines()
879
879
880 fctx = None
880 fctx = None
881 parent = ctx.p1()
881 parent = ctx.p1()
882 leftrev = parent.rev()
882 leftrev = parent.rev()
883 leftnode = parent.node()
883 leftnode = parent.node()
884 rightrev = ctx.rev()
884 rightrev = ctx.rev()
885 rightnode = ctx.node()
885 rightnode = ctx.node()
886 if path in ctx:
886 if path in ctx:
887 fctx = ctx[path]
887 fctx = ctx[path]
888 rightlines = filelines(fctx)
888 rightlines = filelines(fctx)
889 if path not in parent:
889 if path not in parent:
890 leftlines = ()
890 leftlines = ()
891 else:
891 else:
892 pfctx = parent[path]
892 pfctx = parent[path]
893 leftlines = filelines(pfctx)
893 leftlines = filelines(pfctx)
894 else:
894 else:
895 rightlines = ()
895 rightlines = ()
896 pfctx = ctx.parents()[0][path]
896 pfctx = ctx.parents()[0][path]
897 leftlines = filelines(pfctx)
897 leftlines = filelines(pfctx)
898
898
899 comparison = webutil.compare(tmpl, context, leftlines, rightlines)
899 comparison = webutil.compare(tmpl, context, leftlines, rightlines)
900 if fctx is not None:
900 if fctx is not None:
901 rename = webutil.renamelink(fctx)
901 rename = webutil.renamelink(fctx)
902 ctx = fctx
902 ctx = fctx
903 else:
903 else:
904 rename = []
904 rename = []
905 ctx = ctx
905 ctx = ctx
906
906
907 web.res.setbodygen(tmpl(
907 web.res.setbodygen(tmpl(
908 'filecomparison',
908 'filecomparison',
909 file=path,
909 file=path,
910 symrev=webutil.symrevorshortnode(req, ctx),
910 symrev=webutil.symrevorshortnode(req, ctx),
911 rename=rename,
911 rename=rename,
912 leftrev=leftrev,
912 leftrev=leftrev,
913 leftnode=hex(leftnode),
913 leftnode=hex(leftnode),
914 rightrev=rightrev,
914 rightrev=rightrev,
915 rightnode=hex(rightnode),
915 rightnode=hex(rightnode),
916 comparison=comparison,
916 comparison=comparison,
917 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))))
917 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))))
918
918
919 return web.res
919 return web.res
920
920
921 @webcommand('annotate')
921 @webcommand('annotate')
922 def annotate(web, req, tmpl):
922 def annotate(web, req, tmpl):
923 """
923 """
924 /annotate/{revision}/{path}
924 /annotate/{revision}/{path}
925 ---------------------------
925 ---------------------------
926
926
927 Show changeset information for each line in a file.
927 Show changeset information for each line in a file.
928
928
929 The ``ignorews``, ``ignorewsamount``, ``ignorewseol``, and
929 The ``ignorews``, ``ignorewsamount``, ``ignorewseol``, and
930 ``ignoreblanklines`` query string arguments have the same meaning as
930 ``ignoreblanklines`` query string arguments have the same meaning as
931 their ``[annotate]`` config equivalents. It uses the hgrc boolean
931 their ``[annotate]`` config equivalents. It uses the hgrc boolean
932 parsing logic to interpret the value. e.g. ``0`` and ``false`` are
932 parsing logic to interpret the value. e.g. ``0`` and ``false`` are
933 false and ``1`` and ``true`` are true. If not defined, the server
933 false and ``1`` and ``true`` are true. If not defined, the server
934 default settings are used.
934 default settings are used.
935
935
936 The ``fileannotate`` template is rendered.
936 The ``fileannotate`` template is rendered.
937 """
937 """
938 fctx = webutil.filectx(web.repo, req)
938 fctx = webutil.filectx(web.repo, req)
939 f = fctx.path()
939 f = fctx.path()
940 parity = paritygen(web.stripecount)
940 parity = paritygen(web.stripecount)
941 ishead = fctx.filerev() in fctx.filelog().headrevs()
941 ishead = fctx.filerev() in fctx.filelog().headrevs()
942
942
943 # parents() is called once per line and several lines likely belong to
943 # parents() is called once per line and several lines likely belong to
944 # same revision. So it is worth caching.
944 # same revision. So it is worth caching.
945 # TODO there are still redundant operations within basefilectx.parents()
945 # TODO there are still redundant operations within basefilectx.parents()
946 # and from the fctx.annotate() call itself that could be cached.
946 # and from the fctx.annotate() call itself that could be cached.
947 parentscache = {}
947 parentscache = {}
948 def parents(f):
948 def parents(f):
949 rev = f.rev()
949 rev = f.rev()
950 if rev not in parentscache:
950 if rev not in parentscache:
951 parentscache[rev] = []
951 parentscache[rev] = []
952 for p in f.parents():
952 for p in f.parents():
953 entry = {
953 entry = {
954 'node': p.hex(),
954 'node': p.hex(),
955 'rev': p.rev(),
955 'rev': p.rev(),
956 }
956 }
957 parentscache[rev].append(entry)
957 parentscache[rev].append(entry)
958
958
959 for p in parentscache[rev]:
959 for p in parentscache[rev]:
960 yield p
960 yield p
961
961
962 def annotate(**map):
962 def annotate(**map):
963 if fctx.isbinary():
963 if fctx.isbinary():
964 mt = (mimetypes.guess_type(fctx.path())[0]
964 mt = (mimetypes.guess_type(fctx.path())[0]
965 or 'application/octet-stream')
965 or 'application/octet-stream')
966 lines = [((fctx.filectx(fctx.filerev()), 1), '(binary:%s)' % mt)]
966 lines = [((fctx.filectx(fctx.filerev()), 1), '(binary:%s)' % mt)]
967 else:
967 else:
968 lines = webutil.annotate(req, fctx, web.repo.ui)
968 lines = webutil.annotate(req, fctx, web.repo.ui)
969
969
970 previousrev = None
970 previousrev = None
971 blockparitygen = paritygen(1)
971 blockparitygen = paritygen(1)
972 for lineno, (aline, l) in enumerate(lines):
972 for lineno, (aline, l) in enumerate(lines):
973 f = aline.fctx
973 f = aline.fctx
974 rev = f.rev()
974 rev = f.rev()
975 if rev != previousrev:
975 if rev != previousrev:
976 blockhead = True
976 blockhead = True
977 blockparity = next(blockparitygen)
977 blockparity = next(blockparitygen)
978 else:
978 else:
979 blockhead = None
979 blockhead = None
980 previousrev = rev
980 previousrev = rev
981 yield {"parity": next(parity),
981 yield {"parity": next(parity),
982 "node": f.hex(),
982 "node": f.hex(),
983 "rev": rev,
983 "rev": rev,
984 "author": f.user(),
984 "author": f.user(),
985 "parents": parents(f),
985 "parents": parents(f),
986 "desc": f.description(),
986 "desc": f.description(),
987 "extra": f.extra(),
987 "extra": f.extra(),
988 "file": f.path(),
988 "file": f.path(),
989 "blockhead": blockhead,
989 "blockhead": blockhead,
990 "blockparity": blockparity,
990 "blockparity": blockparity,
991 "targetline": aline.lineno,
991 "targetline": aline.lineno,
992 "line": l,
992 "line": l,
993 "lineno": lineno + 1,
993 "lineno": lineno + 1,
994 "lineid": "l%d" % (lineno + 1),
994 "lineid": "l%d" % (lineno + 1),
995 "linenumber": "% 6d" % (lineno + 1),
995 "linenumber": "% 6d" % (lineno + 1),
996 "revdate": f.date()}
996 "revdate": f.date()}
997
997
998 diffopts = webutil.difffeatureopts(req, web.repo.ui, 'annotate')
998 diffopts = webutil.difffeatureopts(req, web.repo.ui, 'annotate')
999 diffopts = {k: getattr(diffopts, k) for k in diffopts.defaults}
999 diffopts = {k: getattr(diffopts, k) for k in diffopts.defaults}
1000
1000
1001 web.res.setbodygen(tmpl(
1001 web.res.setbodygen(tmpl(
1002 'fileannotate',
1002 'fileannotate',
1003 file=f,
1003 file=f,
1004 annotate=annotate,
1004 annotate=annotate,
1005 path=webutil.up(f),
1005 path=webutil.up(f),
1006 symrev=webutil.symrevorshortnode(req, fctx),
1006 symrev=webutil.symrevorshortnode(req, fctx),
1007 rename=webutil.renamelink(fctx),
1007 rename=webutil.renamelink(fctx),
1008 permissions=fctx.manifest().flags(f),
1008 permissions=fctx.manifest().flags(f),
1009 ishead=int(ishead),
1009 ishead=int(ishead),
1010 diffopts=diffopts,
1010 diffopts=diffopts,
1011 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx))))
1011 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx))))
1012
1012
1013 return web.res
1013 return web.res
1014
1014
1015 @webcommand('filelog')
1015 @webcommand('filelog')
1016 def filelog(web, req, tmpl):
1016 def filelog(web, req, tmpl):
1017 """
1017 """
1018 /filelog/{revision}/{path}
1018 /filelog/{revision}/{path}
1019 --------------------------
1019 --------------------------
1020
1020
1021 Show information about the history of a file in the repository.
1021 Show information about the history of a file in the repository.
1022
1022
1023 The ``revcount`` query string argument can be defined to control the
1023 The ``revcount`` query string argument can be defined to control the
1024 maximum number of entries to show.
1024 maximum number of entries to show.
1025
1025
1026 The ``filelog`` template will be rendered.
1026 The ``filelog`` template will be rendered.
1027 """
1027 """
1028
1028
1029 try:
1029 try:
1030 fctx = webutil.filectx(web.repo, req)
1030 fctx = webutil.filectx(web.repo, req)
1031 f = fctx.path()
1031 f = fctx.path()
1032 fl = fctx.filelog()
1032 fl = fctx.filelog()
1033 except error.LookupError:
1033 except error.LookupError:
1034 f = webutil.cleanpath(web.repo, req.req.qsparams['file'])
1034 f = webutil.cleanpath(web.repo, req.req.qsparams['file'])
1035 fl = web.repo.file(f)
1035 fl = web.repo.file(f)
1036 numrevs = len(fl)
1036 numrevs = len(fl)
1037 if not numrevs: # file doesn't exist at all
1037 if not numrevs: # file doesn't exist at all
1038 raise
1038 raise
1039 rev = webutil.changectx(web.repo, req).rev()
1039 rev = webutil.changectx(web.repo, req).rev()
1040 first = fl.linkrev(0)
1040 first = fl.linkrev(0)
1041 if rev < first: # current rev is from before file existed
1041 if rev < first: # current rev is from before file existed
1042 raise
1042 raise
1043 frev = numrevs - 1
1043 frev = numrevs - 1
1044 while fl.linkrev(frev) > rev:
1044 while fl.linkrev(frev) > rev:
1045 frev -= 1
1045 frev -= 1
1046 fctx = web.repo.filectx(f, fl.linkrev(frev))
1046 fctx = web.repo.filectx(f, fl.linkrev(frev))
1047
1047
1048 revcount = web.maxshortchanges
1048 revcount = web.maxshortchanges
1049 if 'revcount' in req.req.qsparams:
1049 if 'revcount' in req.req.qsparams:
1050 try:
1050 try:
1051 revcount = int(req.req.qsparams.get('revcount', revcount))
1051 revcount = int(req.req.qsparams.get('revcount', revcount))
1052 revcount = max(revcount, 1)
1052 revcount = max(revcount, 1)
1053 tmpl.defaults['sessionvars']['revcount'] = revcount
1053 tmpl.defaults['sessionvars']['revcount'] = revcount
1054 except ValueError:
1054 except ValueError:
1055 pass
1055 pass
1056
1056
1057 lrange = webutil.linerange(req)
1057 lrange = webutil.linerange(req)
1058
1058
1059 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1059 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1060 lessvars['revcount'] = max(revcount // 2, 1)
1060 lessvars['revcount'] = max(revcount // 2, 1)
1061 morevars = copy.copy(tmpl.defaults['sessionvars'])
1061 morevars = copy.copy(tmpl.defaults['sessionvars'])
1062 morevars['revcount'] = revcount * 2
1062 morevars['revcount'] = revcount * 2
1063
1063
1064 patch = 'patch' in req.req.qsparams
1064 patch = 'patch' in req.req.qsparams
1065 if patch:
1065 if patch:
1066 lessvars['patch'] = morevars['patch'] = req.req.qsparams['patch']
1066 lessvars['patch'] = morevars['patch'] = req.req.qsparams['patch']
1067 descend = 'descend' in req.req.qsparams
1067 descend = 'descend' in req.req.qsparams
1068 if descend:
1068 if descend:
1069 lessvars['descend'] = morevars['descend'] = req.req.qsparams['descend']
1069 lessvars['descend'] = morevars['descend'] = req.req.qsparams['descend']
1070
1070
1071 count = fctx.filerev() + 1
1071 count = fctx.filerev() + 1
1072 start = max(0, count - revcount) # first rev on this page
1072 start = max(0, count - revcount) # first rev on this page
1073 end = min(count, start + revcount) # last rev on this page
1073 end = min(count, start + revcount) # last rev on this page
1074 parity = paritygen(web.stripecount, offset=start - end)
1074 parity = paritygen(web.stripecount, offset=start - end)
1075
1075
1076 repo = web.repo
1076 repo = web.repo
1077 revs = fctx.filelog().revs(start, end - 1)
1077 revs = fctx.filelog().revs(start, end - 1)
1078 entries = []
1078 entries = []
1079
1079
1080 diffstyle = web.config('web', 'style')
1080 diffstyle = web.config('web', 'style')
1081 if 'style' in req.req.qsparams:
1081 if 'style' in req.req.qsparams:
1082 diffstyle = req.req.qsparams['style']
1082 diffstyle = req.req.qsparams['style']
1083
1083
1084 def diff(fctx, linerange=None):
1084 def diff(fctx, linerange=None):
1085 ctx = fctx.changectx()
1085 ctx = fctx.changectx()
1086 basectx = ctx.p1()
1086 basectx = ctx.p1()
1087 path = fctx.path()
1087 path = fctx.path()
1088 return webutil.diffs(web, tmpl, ctx, basectx, [path], diffstyle,
1088 return webutil.diffs(web, tmpl, ctx, basectx, [path], diffstyle,
1089 linerange=linerange,
1089 linerange=linerange,
1090 lineidprefix='%s-' % ctx.hex()[:12])
1090 lineidprefix='%s-' % ctx.hex()[:12])
1091
1091
1092 linerange = None
1092 linerange = None
1093 if lrange is not None:
1093 if lrange is not None:
1094 linerange = webutil.formatlinerange(*lrange)
1094 linerange = webutil.formatlinerange(*lrange)
1095 # deactivate numeric nav links when linerange is specified as this
1095 # deactivate numeric nav links when linerange is specified as this
1096 # would required a dedicated "revnav" class
1096 # would required a dedicated "revnav" class
1097 nav = None
1097 nav = None
1098 if descend:
1098 if descend:
1099 it = dagop.blockdescendants(fctx, *lrange)
1099 it = dagop.blockdescendants(fctx, *lrange)
1100 else:
1100 else:
1101 it = dagop.blockancestors(fctx, *lrange)
1101 it = dagop.blockancestors(fctx, *lrange)
1102 for i, (c, lr) in enumerate(it, 1):
1102 for i, (c, lr) in enumerate(it, 1):
1103 diffs = None
1103 diffs = None
1104 if patch:
1104 if patch:
1105 diffs = diff(c, linerange=lr)
1105 diffs = diff(c, linerange=lr)
1106 # follow renames accross filtered (not in range) revisions
1106 # follow renames accross filtered (not in range) revisions
1107 path = c.path()
1107 path = c.path()
1108 entries.append(dict(
1108 entries.append(dict(
1109 parity=next(parity),
1109 parity=next(parity),
1110 filerev=c.rev(),
1110 filerev=c.rev(),
1111 file=path,
1111 file=path,
1112 diff=diffs,
1112 diff=diffs,
1113 linerange=webutil.formatlinerange(*lr),
1113 linerange=webutil.formatlinerange(*lr),
1114 **pycompat.strkwargs(webutil.commonentry(repo, c))))
1114 **pycompat.strkwargs(webutil.commonentry(repo, c))))
1115 if i == revcount:
1115 if i == revcount:
1116 break
1116 break
1117 lessvars['linerange'] = webutil.formatlinerange(*lrange)
1117 lessvars['linerange'] = webutil.formatlinerange(*lrange)
1118 morevars['linerange'] = lessvars['linerange']
1118 morevars['linerange'] = lessvars['linerange']
1119 else:
1119 else:
1120 for i in revs:
1120 for i in revs:
1121 iterfctx = fctx.filectx(i)
1121 iterfctx = fctx.filectx(i)
1122 diffs = None
1122 diffs = None
1123 if patch:
1123 if patch:
1124 diffs = diff(iterfctx)
1124 diffs = diff(iterfctx)
1125 entries.append(dict(
1125 entries.append(dict(
1126 parity=next(parity),
1126 parity=next(parity),
1127 filerev=i,
1127 filerev=i,
1128 file=f,
1128 file=f,
1129 diff=diffs,
1129 diff=diffs,
1130 rename=webutil.renamelink(iterfctx),
1130 rename=webutil.renamelink(iterfctx),
1131 **pycompat.strkwargs(webutil.commonentry(repo, iterfctx))))
1131 **pycompat.strkwargs(webutil.commonentry(repo, iterfctx))))
1132 entries.reverse()
1132 entries.reverse()
1133 revnav = webutil.filerevnav(web.repo, fctx.path())
1133 revnav = webutil.filerevnav(web.repo, fctx.path())
1134 nav = revnav.gen(end - 1, revcount, count)
1134 nav = revnav.gen(end - 1, revcount, count)
1135
1135
1136 latestentry = entries[:1]
1136 latestentry = entries[:1]
1137
1137
1138 web.res.setbodygen(tmpl(
1138 web.res.setbodygen(tmpl(
1139 'filelog',
1139 'filelog',
1140 file=f,
1140 file=f,
1141 nav=nav,
1141 nav=nav,
1142 symrev=webutil.symrevorshortnode(req, fctx),
1142 symrev=webutil.symrevorshortnode(req, fctx),
1143 entries=entries,
1143 entries=entries,
1144 descend=descend,
1144 descend=descend,
1145 patch=patch,
1145 patch=patch,
1146 latestentry=latestentry,
1146 latestentry=latestentry,
1147 linerange=linerange,
1147 linerange=linerange,
1148 revcount=revcount,
1148 revcount=revcount,
1149 morevars=morevars,
1149 morevars=morevars,
1150 lessvars=lessvars,
1150 lessvars=lessvars,
1151 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx))))
1151 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx))))
1152
1152
1153 return web.res
1153 return web.res
1154
1154
1155 @webcommand('archive')
1155 @webcommand('archive')
1156 def archive(web, req, tmpl):
1156 def archive(web, req, tmpl):
1157 """
1157 """
1158 /archive/{revision}.{format}[/{path}]
1158 /archive/{revision}.{format}[/{path}]
1159 -------------------------------------
1159 -------------------------------------
1160
1160
1161 Obtain an archive of repository content.
1161 Obtain an archive of repository content.
1162
1162
1163 The content and type of the archive is defined by a URL path parameter.
1163 The content and type of the archive is defined by a URL path parameter.
1164 ``format`` is the file extension of the archive type to be generated. e.g.
1164 ``format`` is the file extension of the archive type to be generated. e.g.
1165 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1165 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1166 server configuration.
1166 server configuration.
1167
1167
1168 The optional ``path`` URL parameter controls content to include in the
1168 The optional ``path`` URL parameter controls content to include in the
1169 archive. If omitted, every file in the specified revision is present in the
1169 archive. If omitted, every file in the specified revision is present in the
1170 archive. If included, only the specified file or contents of the specified
1170 archive. If included, only the specified file or contents of the specified
1171 directory will be included in the archive.
1171 directory will be included in the archive.
1172
1172
1173 No template is used for this handler. Raw, binary content is generated.
1173 No template is used for this handler. Raw, binary content is generated.
1174 """
1174 """
1175
1175
1176 type_ = req.req.qsparams.get('type')
1176 type_ = req.req.qsparams.get('type')
1177 allowed = web.configlist("web", "allow_archive")
1177 allowed = web.configlist("web", "allow_archive")
1178 key = req.req.qsparams['node']
1178 key = req.req.qsparams['node']
1179
1179
1180 if type_ not in web.archivespecs:
1180 if type_ not in web.archivespecs:
1181 msg = 'Unsupported archive type: %s' % type_
1181 msg = 'Unsupported archive type: %s' % type_
1182 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1182 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1183
1183
1184 if not ((type_ in allowed or
1184 if not ((type_ in allowed or
1185 web.configbool("web", "allow" + type_))):
1185 web.configbool("web", "allow" + type_))):
1186 msg = 'Archive type not allowed: %s' % type_
1186 msg = 'Archive type not allowed: %s' % type_
1187 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1187 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1188
1188
1189 reponame = re.sub(br"\W+", "-", os.path.basename(web.reponame))
1189 reponame = re.sub(br"\W+", "-", os.path.basename(web.reponame))
1190 cnode = web.repo.lookup(key)
1190 cnode = web.repo.lookup(key)
1191 arch_version = key
1191 arch_version = key
1192 if cnode == key or key == 'tip':
1192 if cnode == key or key == 'tip':
1193 arch_version = short(cnode)
1193 arch_version = short(cnode)
1194 name = "%s-%s" % (reponame, arch_version)
1194 name = "%s-%s" % (reponame, arch_version)
1195
1195
1196 ctx = webutil.changectx(web.repo, req)
1196 ctx = webutil.changectx(web.repo, req)
1197 pats = []
1197 pats = []
1198 match = scmutil.match(ctx, [])
1198 match = scmutil.match(ctx, [])
1199 file = req.req.qsparams.get('file')
1199 file = req.req.qsparams.get('file')
1200 if file:
1200 if file:
1201 pats = ['path:' + file]
1201 pats = ['path:' + file]
1202 match = scmutil.match(ctx, pats, default='path')
1202 match = scmutil.match(ctx, pats, default='path')
1203 if pats:
1203 if pats:
1204 files = [f for f in ctx.manifest().keys() if match(f)]
1204 files = [f for f in ctx.manifest().keys() if match(f)]
1205 if not files:
1205 if not files:
1206 raise ErrorResponse(HTTP_NOT_FOUND,
1206 raise ErrorResponse(HTTP_NOT_FOUND,
1207 'file(s) not found: %s' % file)
1207 'file(s) not found: %s' % file)
1208
1208
1209 mimetype, artype, extension, encoding = web.archivespecs[type_]
1209 mimetype, artype, extension, encoding = web.archivespecs[type_]
1210 headers = [
1210 headers = [
1211 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
1211 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
1212 ]
1212 ]
1213 if encoding:
1213 if encoding:
1214 headers.append(('Content-Encoding', encoding))
1214 headers.append(('Content-Encoding', encoding))
1215 req.headers.extend(headers)
1215 req.headers.extend(headers)
1216 req.respond(HTTP_OK, mimetype)
1216 req.respond(HTTP_OK, mimetype)
1217
1217
1218 archival.archive(web.repo, req, cnode, artype, prefix=name,
1218 archival.archive(web.repo, req, cnode, artype, prefix=name,
1219 matchfn=match,
1219 matchfn=match,
1220 subrepos=web.configbool("web", "archivesubrepos"))
1220 subrepos=web.configbool("web", "archivesubrepos"))
1221 return []
1221 return []
1222
1222
1223
1223
1224 @webcommand('static')
1224 @webcommand('static')
1225 def static(web, req, tmpl):
1225 def static(web, req, tmpl):
1226 fname = req.req.qsparams['file']
1226 fname = req.req.qsparams['file']
1227 # a repo owner may set web.static in .hg/hgrc to get any file
1227 # a repo owner may set web.static in .hg/hgrc to get any file
1228 # readable by the user running the CGI script
1228 # readable by the user running the CGI script
1229 static = web.config("web", "static", None, untrusted=False)
1229 static = web.config("web", "static", None, untrusted=False)
1230 if not static:
1230 if not static:
1231 tp = web.templatepath or templater.templatepaths()
1231 tp = web.templatepath or templater.templatepaths()
1232 if isinstance(tp, str):
1232 if isinstance(tp, str):
1233 tp = [tp]
1233 tp = [tp]
1234 static = [os.path.join(p, 'static') for p in tp]
1234 static = [os.path.join(p, 'static') for p in tp]
1235 staticfile(static, fname, req)
1235
1236 return []
1236 staticfile(static, fname, web.res)
1237 return web.res
1237
1238
1238 @webcommand('graph')
1239 @webcommand('graph')
1239 def graph(web, req, tmpl):
1240 def graph(web, req, tmpl):
1240 """
1241 """
1241 /graph[/{revision}]
1242 /graph[/{revision}]
1242 -------------------
1243 -------------------
1243
1244
1244 Show information about the graphical topology of the repository.
1245 Show information about the graphical topology of the repository.
1245
1246
1246 Information rendered by this handler can be used to create visual
1247 Information rendered by this handler can be used to create visual
1247 representations of repository topology.
1248 representations of repository topology.
1248
1249
1249 The ``revision`` URL parameter controls the starting changeset. If it's
1250 The ``revision`` URL parameter controls the starting changeset. If it's
1250 absent, the default is ``tip``.
1251 absent, the default is ``tip``.
1251
1252
1252 The ``revcount`` query string argument can define the number of changesets
1253 The ``revcount`` query string argument can define the number of changesets
1253 to show information for.
1254 to show information for.
1254
1255
1255 The ``graphtop`` query string argument can specify the starting changeset
1256 The ``graphtop`` query string argument can specify the starting changeset
1256 for producing ``jsdata`` variable that is used for rendering graph in
1257 for producing ``jsdata`` variable that is used for rendering graph in
1257 JavaScript. By default it has the same value as ``revision``.
1258 JavaScript. By default it has the same value as ``revision``.
1258
1259
1259 This handler will render the ``graph`` template.
1260 This handler will render the ``graph`` template.
1260 """
1261 """
1261
1262
1262 if 'node' in req.req.qsparams:
1263 if 'node' in req.req.qsparams:
1263 ctx = webutil.changectx(web.repo, req)
1264 ctx = webutil.changectx(web.repo, req)
1264 symrev = webutil.symrevorshortnode(req, ctx)
1265 symrev = webutil.symrevorshortnode(req, ctx)
1265 else:
1266 else:
1266 ctx = web.repo['tip']
1267 ctx = web.repo['tip']
1267 symrev = 'tip'
1268 symrev = 'tip'
1268 rev = ctx.rev()
1269 rev = ctx.rev()
1269
1270
1270 bg_height = 39
1271 bg_height = 39
1271 revcount = web.maxshortchanges
1272 revcount = web.maxshortchanges
1272 if 'revcount' in req.req.qsparams:
1273 if 'revcount' in req.req.qsparams:
1273 try:
1274 try:
1274 revcount = int(req.req.qsparams.get('revcount', revcount))
1275 revcount = int(req.req.qsparams.get('revcount', revcount))
1275 revcount = max(revcount, 1)
1276 revcount = max(revcount, 1)
1276 tmpl.defaults['sessionvars']['revcount'] = revcount
1277 tmpl.defaults['sessionvars']['revcount'] = revcount
1277 except ValueError:
1278 except ValueError:
1278 pass
1279 pass
1279
1280
1280 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1281 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1281 lessvars['revcount'] = max(revcount // 2, 1)
1282 lessvars['revcount'] = max(revcount // 2, 1)
1282 morevars = copy.copy(tmpl.defaults['sessionvars'])
1283 morevars = copy.copy(tmpl.defaults['sessionvars'])
1283 morevars['revcount'] = revcount * 2
1284 morevars['revcount'] = revcount * 2
1284
1285
1285 graphtop = req.req.qsparams.get('graphtop', ctx.hex())
1286 graphtop = req.req.qsparams.get('graphtop', ctx.hex())
1286 graphvars = copy.copy(tmpl.defaults['sessionvars'])
1287 graphvars = copy.copy(tmpl.defaults['sessionvars'])
1287 graphvars['graphtop'] = graphtop
1288 graphvars['graphtop'] = graphtop
1288
1289
1289 count = len(web.repo)
1290 count = len(web.repo)
1290 pos = rev
1291 pos = rev
1291
1292
1292 uprev = min(max(0, count - 1), rev + revcount)
1293 uprev = min(max(0, count - 1), rev + revcount)
1293 downrev = max(0, rev - revcount)
1294 downrev = max(0, rev - revcount)
1294 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1295 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1295
1296
1296 tree = []
1297 tree = []
1297 nextentry = []
1298 nextentry = []
1298 lastrev = 0
1299 lastrev = 0
1299 if pos != -1:
1300 if pos != -1:
1300 allrevs = web.repo.changelog.revs(pos, 0)
1301 allrevs = web.repo.changelog.revs(pos, 0)
1301 revs = []
1302 revs = []
1302 for i in allrevs:
1303 for i in allrevs:
1303 revs.append(i)
1304 revs.append(i)
1304 if len(revs) >= revcount + 1:
1305 if len(revs) >= revcount + 1:
1305 break
1306 break
1306
1307
1307 if len(revs) > revcount:
1308 if len(revs) > revcount:
1308 nextentry = [webutil.commonentry(web.repo, web.repo[revs[-1]])]
1309 nextentry = [webutil.commonentry(web.repo, web.repo[revs[-1]])]
1309 revs = revs[:-1]
1310 revs = revs[:-1]
1310
1311
1311 lastrev = revs[-1]
1312 lastrev = revs[-1]
1312
1313
1313 # We have to feed a baseset to dagwalker as it is expecting smartset
1314 # We have to feed a baseset to dagwalker as it is expecting smartset
1314 # object. This does not have a big impact on hgweb performance itself
1315 # object. This does not have a big impact on hgweb performance itself
1315 # since hgweb graphing code is not itself lazy yet.
1316 # since hgweb graphing code is not itself lazy yet.
1316 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1317 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1317 # As we said one line above... not lazy.
1318 # As we said one line above... not lazy.
1318 tree = list(item for item in graphmod.colored(dag, web.repo)
1319 tree = list(item for item in graphmod.colored(dag, web.repo)
1319 if item[1] == graphmod.CHANGESET)
1320 if item[1] == graphmod.CHANGESET)
1320
1321
1321 def nodecurrent(ctx):
1322 def nodecurrent(ctx):
1322 wpnodes = web.repo.dirstate.parents()
1323 wpnodes = web.repo.dirstate.parents()
1323 if wpnodes[1] == nullid:
1324 if wpnodes[1] == nullid:
1324 wpnodes = wpnodes[:1]
1325 wpnodes = wpnodes[:1]
1325 if ctx.node() in wpnodes:
1326 if ctx.node() in wpnodes:
1326 return '@'
1327 return '@'
1327 return ''
1328 return ''
1328
1329
1329 def nodesymbol(ctx):
1330 def nodesymbol(ctx):
1330 if ctx.obsolete():
1331 if ctx.obsolete():
1331 return 'x'
1332 return 'x'
1332 elif ctx.isunstable():
1333 elif ctx.isunstable():
1333 return '*'
1334 return '*'
1334 elif ctx.closesbranch():
1335 elif ctx.closesbranch():
1335 return '_'
1336 return '_'
1336 else:
1337 else:
1337 return 'o'
1338 return 'o'
1338
1339
1339 def fulltree():
1340 def fulltree():
1340 pos = web.repo[graphtop].rev()
1341 pos = web.repo[graphtop].rev()
1341 tree = []
1342 tree = []
1342 if pos != -1:
1343 if pos != -1:
1343 revs = web.repo.changelog.revs(pos, lastrev)
1344 revs = web.repo.changelog.revs(pos, lastrev)
1344 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1345 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1345 tree = list(item for item in graphmod.colored(dag, web.repo)
1346 tree = list(item for item in graphmod.colored(dag, web.repo)
1346 if item[1] == graphmod.CHANGESET)
1347 if item[1] == graphmod.CHANGESET)
1347 return tree
1348 return tree
1348
1349
1349 def jsdata():
1350 def jsdata():
1350 return [{'node': pycompat.bytestr(ctx),
1351 return [{'node': pycompat.bytestr(ctx),
1351 'graphnode': nodecurrent(ctx) + nodesymbol(ctx),
1352 'graphnode': nodecurrent(ctx) + nodesymbol(ctx),
1352 'vertex': vtx,
1353 'vertex': vtx,
1353 'edges': edges}
1354 'edges': edges}
1354 for (id, type, ctx, vtx, edges) in fulltree()]
1355 for (id, type, ctx, vtx, edges) in fulltree()]
1355
1356
1356 def nodes():
1357 def nodes():
1357 parity = paritygen(web.stripecount)
1358 parity = paritygen(web.stripecount)
1358 for row, (id, type, ctx, vtx, edges) in enumerate(tree):
1359 for row, (id, type, ctx, vtx, edges) in enumerate(tree):
1359 entry = webutil.commonentry(web.repo, ctx)
1360 entry = webutil.commonentry(web.repo, ctx)
1360 edgedata = [{'col': edge[0],
1361 edgedata = [{'col': edge[0],
1361 'nextcol': edge[1],
1362 'nextcol': edge[1],
1362 'color': (edge[2] - 1) % 6 + 1,
1363 'color': (edge[2] - 1) % 6 + 1,
1363 'width': edge[3],
1364 'width': edge[3],
1364 'bcolor': edge[4]}
1365 'bcolor': edge[4]}
1365 for edge in edges]
1366 for edge in edges]
1366
1367
1367 entry.update({'col': vtx[0],
1368 entry.update({'col': vtx[0],
1368 'color': (vtx[1] - 1) % 6 + 1,
1369 'color': (vtx[1] - 1) % 6 + 1,
1369 'parity': next(parity),
1370 'parity': next(parity),
1370 'edges': edgedata,
1371 'edges': edgedata,
1371 'row': row,
1372 'row': row,
1372 'nextrow': row + 1})
1373 'nextrow': row + 1})
1373
1374
1374 yield entry
1375 yield entry
1375
1376
1376 rows = len(tree)
1377 rows = len(tree)
1377
1378
1378 web.res.setbodygen(tmpl(
1379 web.res.setbodygen(tmpl(
1379 'graph',
1380 'graph',
1380 rev=rev,
1381 rev=rev,
1381 symrev=symrev,
1382 symrev=symrev,
1382 revcount=revcount,
1383 revcount=revcount,
1383 uprev=uprev,
1384 uprev=uprev,
1384 lessvars=lessvars,
1385 lessvars=lessvars,
1385 morevars=morevars,
1386 morevars=morevars,
1386 downrev=downrev,
1387 downrev=downrev,
1387 graphvars=graphvars,
1388 graphvars=graphvars,
1388 rows=rows,
1389 rows=rows,
1389 bg_height=bg_height,
1390 bg_height=bg_height,
1390 changesets=count,
1391 changesets=count,
1391 nextentry=nextentry,
1392 nextentry=nextentry,
1392 jsdata=lambda **x: jsdata(),
1393 jsdata=lambda **x: jsdata(),
1393 nodes=lambda **x: nodes(),
1394 nodes=lambda **x: nodes(),
1394 node=ctx.hex(),
1395 node=ctx.hex(),
1395 changenav=changenav))
1396 changenav=changenav))
1396
1397
1397 return web.res
1398 return web.res
1398
1399
1399 def _getdoc(e):
1400 def _getdoc(e):
1400 doc = e[0].__doc__
1401 doc = e[0].__doc__
1401 if doc:
1402 if doc:
1402 doc = _(doc).partition('\n')[0]
1403 doc = _(doc).partition('\n')[0]
1403 else:
1404 else:
1404 doc = _('(no help text available)')
1405 doc = _('(no help text available)')
1405 return doc
1406 return doc
1406
1407
1407 @webcommand('help')
1408 @webcommand('help')
1408 def help(web, req, tmpl):
1409 def help(web, req, tmpl):
1409 """
1410 """
1410 /help[/{topic}]
1411 /help[/{topic}]
1411 ---------------
1412 ---------------
1412
1413
1413 Render help documentation.
1414 Render help documentation.
1414
1415
1415 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1416 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1416 is defined, that help topic will be rendered. If not, an index of
1417 is defined, that help topic will be rendered. If not, an index of
1417 available help topics will be rendered.
1418 available help topics will be rendered.
1418
1419
1419 The ``help`` template will be rendered when requesting help for a topic.
1420 The ``help`` template will be rendered when requesting help for a topic.
1420 ``helptopics`` will be rendered for the index of help topics.
1421 ``helptopics`` will be rendered for the index of help topics.
1421 """
1422 """
1422 from .. import commands, help as helpmod # avoid cycle
1423 from .. import commands, help as helpmod # avoid cycle
1423
1424
1424 topicname = req.req.qsparams.get('node')
1425 topicname = req.req.qsparams.get('node')
1425 if not topicname:
1426 if not topicname:
1426 def topics(**map):
1427 def topics(**map):
1427 for entries, summary, _doc in helpmod.helptable:
1428 for entries, summary, _doc in helpmod.helptable:
1428 yield {'topic': entries[0], 'summary': summary}
1429 yield {'topic': entries[0], 'summary': summary}
1429
1430
1430 early, other = [], []
1431 early, other = [], []
1431 primary = lambda s: s.partition('|')[0]
1432 primary = lambda s: s.partition('|')[0]
1432 for c, e in commands.table.iteritems():
1433 for c, e in commands.table.iteritems():
1433 doc = _getdoc(e)
1434 doc = _getdoc(e)
1434 if 'DEPRECATED' in doc or c.startswith('debug'):
1435 if 'DEPRECATED' in doc or c.startswith('debug'):
1435 continue
1436 continue
1436 cmd = primary(c)
1437 cmd = primary(c)
1437 if cmd.startswith('^'):
1438 if cmd.startswith('^'):
1438 early.append((cmd[1:], doc))
1439 early.append((cmd[1:], doc))
1439 else:
1440 else:
1440 other.append((cmd, doc))
1441 other.append((cmd, doc))
1441
1442
1442 early.sort()
1443 early.sort()
1443 other.sort()
1444 other.sort()
1444
1445
1445 def earlycommands(**map):
1446 def earlycommands(**map):
1446 for c, doc in early:
1447 for c, doc in early:
1447 yield {'topic': c, 'summary': doc}
1448 yield {'topic': c, 'summary': doc}
1448
1449
1449 def othercommands(**map):
1450 def othercommands(**map):
1450 for c, doc in other:
1451 for c, doc in other:
1451 yield {'topic': c, 'summary': doc}
1452 yield {'topic': c, 'summary': doc}
1452
1453
1453 web.res.setbodygen(tmpl(
1454 web.res.setbodygen(tmpl(
1454 'helptopics',
1455 'helptopics',
1455 topics=topics,
1456 topics=topics,
1456 earlycommands=earlycommands,
1457 earlycommands=earlycommands,
1457 othercommands=othercommands,
1458 othercommands=othercommands,
1458 title='Index'))
1459 title='Index'))
1459 return web.res
1460 return web.res
1460
1461
1461 # Render an index of sub-topics.
1462 # Render an index of sub-topics.
1462 if topicname in helpmod.subtopics:
1463 if topicname in helpmod.subtopics:
1463 topics = []
1464 topics = []
1464 for entries, summary, _doc in helpmod.subtopics[topicname]:
1465 for entries, summary, _doc in helpmod.subtopics[topicname]:
1465 topics.append({
1466 topics.append({
1466 'topic': '%s.%s' % (topicname, entries[0]),
1467 'topic': '%s.%s' % (topicname, entries[0]),
1467 'basename': entries[0],
1468 'basename': entries[0],
1468 'summary': summary,
1469 'summary': summary,
1469 })
1470 })
1470
1471
1471 web.res.setbodygen(tmpl(
1472 web.res.setbodygen(tmpl(
1472 'helptopics',
1473 'helptopics',
1473 topics=topics,
1474 topics=topics,
1474 title=topicname,
1475 title=topicname,
1475 subindex=True))
1476 subindex=True))
1476 return web.res
1477 return web.res
1477
1478
1478 u = webutil.wsgiui.load()
1479 u = webutil.wsgiui.load()
1479 u.verbose = True
1480 u.verbose = True
1480
1481
1481 # Render a page from a sub-topic.
1482 # Render a page from a sub-topic.
1482 if '.' in topicname:
1483 if '.' in topicname:
1483 # TODO implement support for rendering sections, like
1484 # TODO implement support for rendering sections, like
1484 # `hg help` works.
1485 # `hg help` works.
1485 topic, subtopic = topicname.split('.', 1)
1486 topic, subtopic = topicname.split('.', 1)
1486 if topic not in helpmod.subtopics:
1487 if topic not in helpmod.subtopics:
1487 raise ErrorResponse(HTTP_NOT_FOUND)
1488 raise ErrorResponse(HTTP_NOT_FOUND)
1488 else:
1489 else:
1489 topic = topicname
1490 topic = topicname
1490 subtopic = None
1491 subtopic = None
1491
1492
1492 try:
1493 try:
1493 doc = helpmod.help_(u, commands, topic, subtopic=subtopic)
1494 doc = helpmod.help_(u, commands, topic, subtopic=subtopic)
1494 except error.Abort:
1495 except error.Abort:
1495 raise ErrorResponse(HTTP_NOT_FOUND)
1496 raise ErrorResponse(HTTP_NOT_FOUND)
1496
1497
1497 web.res.setbodygen(tmpl(
1498 web.res.setbodygen(tmpl(
1498 'help',
1499 'help',
1499 topic=topicname,
1500 topic=topicname,
1500 doc=doc))
1501 doc=doc))
1501
1502
1502 return web.res
1503 return web.res
1503
1504
1504 # tell hggettext to extract docstrings from these functions:
1505 # tell hggettext to extract docstrings from these functions:
1505 i18nfunctions = commands.values()
1506 i18nfunctions = commands.values()
General Comments 0
You need to be logged in to leave comments. Login now