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