##// END OF EJS Templates
hgweb: fix a few `str` type conditional for py3...
Matt Harbison -
r44180:6ff1a0d1 stable
parent child Browse files
Show More
@@ -1,289 +1,289 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(
146 return pycompat.bytesurl(
147 responses.get(code, (r'Error', r'Unknown error'))[0]
147 responses.get(code, (r'Error', r'Unknown error'))[0]
148 )
148 )
149
149
150
150
151 def statusmessage(code, message=None):
151 def statusmessage(code, message=None):
152 return b'%d %s' % (code, message or _statusmessage(code))
152 return b'%d %s' % (code, message or _statusmessage(code))
153
153
154
154
155 def get_stat(spath, fn):
155 def get_stat(spath, fn):
156 """stat fn if it exists, spath otherwise"""
156 """stat fn if it exists, spath otherwise"""
157 cl_path = os.path.join(spath, fn)
157 cl_path = os.path.join(spath, fn)
158 if os.path.exists(cl_path):
158 if os.path.exists(cl_path):
159 return os.stat(cl_path)
159 return os.stat(cl_path)
160 else:
160 else:
161 return os.stat(spath)
161 return os.stat(spath)
162
162
163
163
164 def get_mtime(spath):
164 def get_mtime(spath):
165 return get_stat(spath, b"00changelog.i")[stat.ST_MTIME]
165 return get_stat(spath, b"00changelog.i")[stat.ST_MTIME]
166
166
167
167
168 def ispathsafe(path):
168 def ispathsafe(path):
169 """Determine if a path is safe to use for filesystem access."""
169 """Determine if a path is safe to use for filesystem access."""
170 parts = path.split(b'/')
170 parts = path.split(b'/')
171 for part in parts:
171 for part in parts:
172 if (
172 if (
173 part in (b'', pycompat.oscurdir, pycompat.ospardir)
173 part in (b'', pycompat.oscurdir, pycompat.ospardir)
174 or pycompat.ossep in part
174 or pycompat.ossep in part
175 or pycompat.osaltsep is not None
175 or pycompat.osaltsep is not None
176 and pycompat.osaltsep in part
176 and pycompat.osaltsep in part
177 ):
177 ):
178 return False
178 return False
179
179
180 return True
180 return True
181
181
182
182
183 def staticfile(directory, fname, res):
183 def staticfile(directory, fname, res):
184 """return a file inside directory with guessed Content-Type header
184 """return a file inside directory with guessed Content-Type header
185
185
186 fname always uses '/' as directory separator and isn't allowed to
186 fname always uses '/' as directory separator and isn't allowed to
187 contain unusual path components.
187 contain unusual path components.
188 Content-Type is guessed using the mimetypes module.
188 Content-Type is guessed using the mimetypes module.
189 Return an empty string if fname is illegal or file not found.
189 Return an empty string if fname is illegal or file not found.
190
190
191 """
191 """
192 if not ispathsafe(fname):
192 if not ispathsafe(fname):
193 return
193 return
194
194
195 fpath = os.path.join(*fname.split(b'/'))
195 fpath = os.path.join(*fname.split(b'/'))
196 if isinstance(directory, str):
196 if isinstance(directory, bytes):
197 directory = [directory]
197 directory = [directory]
198 for d in directory:
198 for d in directory:
199 path = os.path.join(d, fpath)
199 path = os.path.join(d, fpath)
200 if os.path.exists(path):
200 if os.path.exists(path):
201 break
201 break
202 try:
202 try:
203 os.stat(path)
203 os.stat(path)
204 ct = pycompat.sysbytes(
204 ct = pycompat.sysbytes(
205 mimetypes.guess_type(pycompat.fsdecode(path))[0] or r"text/plain"
205 mimetypes.guess_type(pycompat.fsdecode(path))[0] or r"text/plain"
206 )
206 )
207 with open(path, b'rb') as fh:
207 with open(path, b'rb') as fh:
208 data = fh.read()
208 data = fh.read()
209
209
210 res.headers[b'Content-Type'] = ct
210 res.headers[b'Content-Type'] = ct
211 res.setbodybytes(data)
211 res.setbodybytes(data)
212 return res
212 return res
213 except TypeError:
213 except TypeError:
214 raise ErrorResponse(HTTP_SERVER_ERROR, b'illegal filename')
214 raise ErrorResponse(HTTP_SERVER_ERROR, b'illegal filename')
215 except OSError as err:
215 except OSError as err:
216 if err.errno == errno.ENOENT:
216 if err.errno == errno.ENOENT:
217 raise ErrorResponse(HTTP_NOT_FOUND)
217 raise ErrorResponse(HTTP_NOT_FOUND)
218 else:
218 else:
219 raise ErrorResponse(
219 raise ErrorResponse(
220 HTTP_SERVER_ERROR, encoding.strtolocal(err.strerror)
220 HTTP_SERVER_ERROR, encoding.strtolocal(err.strerror)
221 )
221 )
222
222
223
223
224 def paritygen(stripecount, offset=0):
224 def paritygen(stripecount, offset=0):
225 """count parity of horizontal stripes for easier reading"""
225 """count parity of horizontal stripes for easier reading"""
226 if stripecount and offset:
226 if stripecount and offset:
227 # account for offset, e.g. due to building the list in reverse
227 # account for offset, e.g. due to building the list in reverse
228 count = (stripecount + offset) % stripecount
228 count = (stripecount + offset) % stripecount
229 parity = (stripecount + offset) // stripecount & 1
229 parity = (stripecount + offset) // stripecount & 1
230 else:
230 else:
231 count = 0
231 count = 0
232 parity = 0
232 parity = 0
233 while True:
233 while True:
234 yield parity
234 yield parity
235 count += 1
235 count += 1
236 if stripecount and count >= stripecount:
236 if stripecount and count >= stripecount:
237 parity = 1 - parity
237 parity = 1 - parity
238 count = 0
238 count = 0
239
239
240
240
241 def get_contact(config):
241 def get_contact(config):
242 """Return repo contact information or empty string.
242 """Return repo contact information or empty string.
243
243
244 web.contact is the primary source, but if that is not set, try
244 web.contact is the primary source, but if that is not set, try
245 ui.username or $EMAIL as a fallback to display something useful.
245 ui.username or $EMAIL as a fallback to display something useful.
246 """
246 """
247 return (
247 return (
248 config(b"web", b"contact")
248 config(b"web", b"contact")
249 or config(b"ui", b"username")
249 or config(b"ui", b"username")
250 or encoding.environ.get(b"EMAIL")
250 or encoding.environ.get(b"EMAIL")
251 or b""
251 or b""
252 )
252 )
253
253
254
254
255 def cspvalues(ui):
255 def cspvalues(ui):
256 """Obtain the Content-Security-Policy header and nonce value.
256 """Obtain the Content-Security-Policy header and nonce value.
257
257
258 Returns a 2-tuple of the CSP header value and the nonce value.
258 Returns a 2-tuple of the CSP header value and the nonce value.
259
259
260 First value is ``None`` if CSP isn't enabled. Second value is ``None``
260 First value is ``None`` if CSP isn't enabled. Second value is ``None``
261 if CSP isn't enabled or if the CSP header doesn't need a nonce.
261 if CSP isn't enabled or if the CSP header doesn't need a nonce.
262 """
262 """
263 # Without demandimport, "import uuid" could have an immediate side-effect
263 # Without demandimport, "import uuid" could have an immediate side-effect
264 # running "ldconfig" on Linux trying to find libuuid.
264 # running "ldconfig" on Linux trying to find libuuid.
265 # With Python <= 2.7.12, that "ldconfig" is run via a shell and the shell
265 # With Python <= 2.7.12, that "ldconfig" is run via a shell and the shell
266 # may pollute the terminal with:
266 # may pollute the terminal with:
267 #
267 #
268 # shell-init: error retrieving current directory: getcwd: cannot access
268 # shell-init: error retrieving current directory: getcwd: cannot access
269 # parent directories: No such file or directory
269 # parent directories: No such file or directory
270 #
270 #
271 # Python >= 2.7.13 has fixed it by running "ldconfig" directly without a
271 # Python >= 2.7.13 has fixed it by running "ldconfig" directly without a
272 # shell (hg changeset a09ae70f3489).
272 # shell (hg changeset a09ae70f3489).
273 #
273 #
274 # Moved "import uuid" from here so it's executed after we know we have
274 # Moved "import uuid" from here so it's executed after we know we have
275 # a sane cwd (i.e. after dispatch.py cwd check).
275 # a sane cwd (i.e. after dispatch.py cwd check).
276 #
276 #
277 # We can move it back once we no longer need Python <= 2.7.12 support.
277 # We can move it back once we no longer need Python <= 2.7.12 support.
278 import uuid
278 import uuid
279
279
280 # Don't allow untrusted CSP setting since it be disable protections
280 # Don't allow untrusted CSP setting since it be disable protections
281 # from a trusted/global source.
281 # from a trusted/global source.
282 csp = ui.config(b'web', b'csp', untrusted=False)
282 csp = ui.config(b'web', b'csp', untrusted=False)
283 nonce = None
283 nonce = None
284
284
285 if csp and b'%nonce%' in csp:
285 if csp and b'%nonce%' in csp:
286 nonce = base64.urlsafe_b64encode(uuid.uuid4().bytes).rstrip(b'=')
286 nonce = base64.urlsafe_b64encode(uuid.uuid4().bytes).rstrip(b'=')
287 csp = csp.replace(b'%nonce%', nonce)
287 csp = csp.replace(b'%nonce%', nonce)
288
288
289 return csp, nonce
289 return csp, nonce
@@ -1,578 +1,578 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 profiling,
35 profiling,
36 pycompat,
36 pycompat,
37 registrar,
37 registrar,
38 scmutil,
38 scmutil,
39 templater,
39 templater,
40 templateutil,
40 templateutil,
41 ui as uimod,
41 ui as uimod,
42 util,
42 util,
43 )
43 )
44
44
45 from . import (
45 from . import (
46 hgweb_mod,
46 hgweb_mod,
47 request as requestmod,
47 request as requestmod,
48 webutil,
48 webutil,
49 wsgicgi,
49 wsgicgi,
50 )
50 )
51 from ..utils import dateutil
51 from ..utils import dateutil
52
52
53
53
54 def cleannames(items):
54 def cleannames(items):
55 return [(util.pconvert(name).strip(b'/'), path) for name, path in items]
55 return [(util.pconvert(name).strip(b'/'), path) for name, path in items]
56
56
57
57
58 def findrepos(paths):
58 def findrepos(paths):
59 repos = []
59 repos = []
60 for prefix, root in cleannames(paths):
60 for prefix, root in cleannames(paths):
61 roothead, roottail = os.path.split(root)
61 roothead, roottail = os.path.split(root)
62 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
62 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
63 # /bar/ be served as as foo/N .
63 # /bar/ be served as as foo/N .
64 # '*' will not search inside dirs with .hg (except .hg/patches),
64 # '*' will not search inside dirs with .hg (except .hg/patches),
65 # '**' will search inside dirs with .hg (and thus also find subrepos).
65 # '**' will search inside dirs with .hg (and thus also find subrepos).
66 try:
66 try:
67 recurse = {b'*': False, b'**': True}[roottail]
67 recurse = {b'*': False, b'**': True}[roottail]
68 except KeyError:
68 except KeyError:
69 repos.append((prefix, root))
69 repos.append((prefix, root))
70 continue
70 continue
71 roothead = os.path.normpath(os.path.abspath(roothead))
71 roothead = os.path.normpath(os.path.abspath(roothead))
72 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
72 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
73 repos.extend(urlrepos(prefix, roothead, paths))
73 repos.extend(urlrepos(prefix, roothead, paths))
74 return repos
74 return repos
75
75
76
76
77 def urlrepos(prefix, roothead, paths):
77 def urlrepos(prefix, roothead, paths):
78 """yield url paths and filesystem paths from a list of repo paths
78 """yield url paths and filesystem paths from a list of repo paths
79
79
80 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
80 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
81 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
81 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
82 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
82 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
83 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
83 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
84 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
84 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
85 """
85 """
86 for path in paths:
86 for path in paths:
87 path = os.path.normpath(path)
87 path = os.path.normpath(path)
88 yield (
88 yield (
89 prefix + b'/' + util.pconvert(path[len(roothead) :]).lstrip(b'/')
89 prefix + b'/' + util.pconvert(path[len(roothead) :]).lstrip(b'/')
90 ).strip(b'/'), path
90 ).strip(b'/'), path
91
91
92
92
93 def readallowed(ui, req):
93 def readallowed(ui, req):
94 """Check allow_read and deny_read config options of a repo's ui object
94 """Check allow_read and deny_read config options of a repo's ui object
95 to determine user permissions. By default, with neither option set (or
95 to determine user permissions. By default, with neither option set (or
96 both empty), allow all users to read the repo. There are two ways a
96 both empty), allow all users to read the repo. There are two ways a
97 user can be denied read access: (1) deny_read is not empty, and the
97 user can be denied read access: (1) deny_read is not empty, and the
98 user is unauthenticated or deny_read contains user (or *), and (2)
98 user is unauthenticated or deny_read contains user (or *), and (2)
99 allow_read is not empty and the user is not in allow_read. Return True
99 allow_read is not empty and the user is not in allow_read. Return True
100 if user is allowed to read the repo, else return False."""
100 if user is allowed to read the repo, else return False."""
101
101
102 user = req.remoteuser
102 user = req.remoteuser
103
103
104 deny_read = ui.configlist(b'web', b'deny_read', untrusted=True)
104 deny_read = ui.configlist(b'web', b'deny_read', untrusted=True)
105 if deny_read and (not user or ismember(ui, user, deny_read)):
105 if deny_read and (not user or ismember(ui, user, deny_read)):
106 return False
106 return False
107
107
108 allow_read = ui.configlist(b'web', b'allow_read', untrusted=True)
108 allow_read = ui.configlist(b'web', b'allow_read', untrusted=True)
109 # by default, allow reading if no allow_read option has been set
109 # by default, allow reading if no allow_read option has been set
110 if not allow_read or ismember(ui, user, allow_read):
110 if not allow_read or ismember(ui, user, allow_read):
111 return True
111 return True
112
112
113 return False
113 return False
114
114
115
115
116 def rawindexentries(ui, repos, req, subdir=b''):
116 def rawindexentries(ui, repos, req, subdir=b''):
117 descend = ui.configbool(b'web', b'descend')
117 descend = ui.configbool(b'web', b'descend')
118 collapse = ui.configbool(b'web', b'collapse')
118 collapse = ui.configbool(b'web', b'collapse')
119 seenrepos = set()
119 seenrepos = set()
120 seendirs = set()
120 seendirs = set()
121 for name, path in repos:
121 for name, path in repos:
122
122
123 if not name.startswith(subdir):
123 if not name.startswith(subdir):
124 continue
124 continue
125 name = name[len(subdir) :]
125 name = name[len(subdir) :]
126 directory = False
126 directory = False
127
127
128 if b'/' in name:
128 if b'/' in name:
129 if not descend:
129 if not descend:
130 continue
130 continue
131
131
132 nameparts = name.split(b'/')
132 nameparts = name.split(b'/')
133 rootname = nameparts[0]
133 rootname = nameparts[0]
134
134
135 if not collapse:
135 if not collapse:
136 pass
136 pass
137 elif rootname in seendirs:
137 elif rootname in seendirs:
138 continue
138 continue
139 elif rootname in seenrepos:
139 elif rootname in seenrepos:
140 pass
140 pass
141 else:
141 else:
142 directory = True
142 directory = True
143 name = rootname
143 name = rootname
144
144
145 # redefine the path to refer to the directory
145 # redefine the path to refer to the directory
146 discarded = b'/'.join(nameparts[1:])
146 discarded = b'/'.join(nameparts[1:])
147
147
148 # remove name parts plus accompanying slash
148 # remove name parts plus accompanying slash
149 path = path[: -len(discarded) - 1]
149 path = path[: -len(discarded) - 1]
150
150
151 try:
151 try:
152 hg.repository(ui, path)
152 hg.repository(ui, path)
153 directory = False
153 directory = False
154 except (IOError, error.RepoError):
154 except (IOError, error.RepoError):
155 pass
155 pass
156
156
157 parts = [
157 parts = [
158 req.apppath.strip(b'/'),
158 req.apppath.strip(b'/'),
159 subdir.strip(b'/'),
159 subdir.strip(b'/'),
160 name.strip(b'/'),
160 name.strip(b'/'),
161 ]
161 ]
162 url = b'/' + b'/'.join(p for p in parts if p) + b'/'
162 url = b'/' + b'/'.join(p for p in parts if p) + b'/'
163
163
164 # show either a directory entry or a repository
164 # show either a directory entry or a repository
165 if directory:
165 if directory:
166 # get the directory's time information
166 # get the directory's time information
167 try:
167 try:
168 d = (get_mtime(path), dateutil.makedate()[1])
168 d = (get_mtime(path), dateutil.makedate()[1])
169 except OSError:
169 except OSError:
170 continue
170 continue
171
171
172 # add '/' to the name to make it obvious that
172 # add '/' to the name to make it obvious that
173 # the entry is a directory, not a regular repository
173 # the entry is a directory, not a regular repository
174 row = {
174 row = {
175 b'contact': b"",
175 b'contact': b"",
176 b'contact_sort': b"",
176 b'contact_sort': b"",
177 b'name': name + b'/',
177 b'name': name + b'/',
178 b'name_sort': name,
178 b'name_sort': name,
179 b'url': url,
179 b'url': url,
180 b'description': b"",
180 b'description': b"",
181 b'description_sort': b"",
181 b'description_sort': b"",
182 b'lastchange': d,
182 b'lastchange': d,
183 b'lastchange_sort': d[1] - d[0],
183 b'lastchange_sort': d[1] - d[0],
184 b'archives': templateutil.mappinglist([]),
184 b'archives': templateutil.mappinglist([]),
185 b'isdirectory': True,
185 b'isdirectory': True,
186 b'labels': templateutil.hybridlist([], name=b'label'),
186 b'labels': templateutil.hybridlist([], name=b'label'),
187 }
187 }
188
188
189 seendirs.add(name)
189 seendirs.add(name)
190 yield row
190 yield row
191 continue
191 continue
192
192
193 u = ui.copy()
193 u = ui.copy()
194 try:
194 try:
195 u.readconfig(os.path.join(path, b'.hg', b'hgrc'))
195 u.readconfig(os.path.join(path, b'.hg', b'hgrc'))
196 except Exception as e:
196 except Exception as e:
197 u.warn(_(b'error reading %s/.hg/hgrc: %s\n') % (path, e))
197 u.warn(_(b'error reading %s/.hg/hgrc: %s\n') % (path, e))
198 continue
198 continue
199
199
200 def get(section, name, default=uimod._unset):
200 def get(section, name, default=uimod._unset):
201 return u.config(section, name, default, untrusted=True)
201 return u.config(section, name, default, untrusted=True)
202
202
203 if u.configbool(b"web", b"hidden", untrusted=True):
203 if u.configbool(b"web", b"hidden", untrusted=True):
204 continue
204 continue
205
205
206 if not readallowed(u, req):
206 if not readallowed(u, req):
207 continue
207 continue
208
208
209 # update time with local timezone
209 # update time with local timezone
210 try:
210 try:
211 r = hg.repository(ui, path)
211 r = hg.repository(ui, path)
212 except IOError:
212 except IOError:
213 u.warn(_(b'error accessing repository at %s\n') % path)
213 u.warn(_(b'error accessing repository at %s\n') % path)
214 continue
214 continue
215 except error.RepoError:
215 except error.RepoError:
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 try:
218 try:
219 d = (get_mtime(r.spath), dateutil.makedate()[1])
219 d = (get_mtime(r.spath), dateutil.makedate()[1])
220 except OSError:
220 except OSError:
221 continue
221 continue
222
222
223 contact = get_contact(get)
223 contact = get_contact(get)
224 description = get(b"web", b"description")
224 description = get(b"web", b"description")
225 seenrepos.add(name)
225 seenrepos.add(name)
226 name = get(b"web", b"name", name)
226 name = get(b"web", b"name", name)
227 labels = u.configlist(b'web', b'labels', untrusted=True)
227 labels = u.configlist(b'web', b'labels', untrusted=True)
228 row = {
228 row = {
229 b'contact': contact or b"unknown",
229 b'contact': contact or b"unknown",
230 b'contact_sort': contact.upper() or b"unknown",
230 b'contact_sort': contact.upper() or b"unknown",
231 b'name': name,
231 b'name': name,
232 b'name_sort': name,
232 b'name_sort': name,
233 b'url': url,
233 b'url': url,
234 b'description': description or b"unknown",
234 b'description': description or b"unknown",
235 b'description_sort': description.upper() or b"unknown",
235 b'description_sort': description.upper() or b"unknown",
236 b'lastchange': d,
236 b'lastchange': d,
237 b'lastchange_sort': d[1] - d[0],
237 b'lastchange_sort': d[1] - d[0],
238 b'archives': webutil.archivelist(u, b"tip", url),
238 b'archives': webutil.archivelist(u, b"tip", url),
239 b'isdirectory': None,
239 b'isdirectory': None,
240 b'labels': templateutil.hybridlist(labels, name=b'label'),
240 b'labels': templateutil.hybridlist(labels, name=b'label'),
241 }
241 }
242
242
243 yield row
243 yield row
244
244
245
245
246 def _indexentriesgen(
246 def _indexentriesgen(
247 context, ui, repos, req, stripecount, sortcolumn, descending, subdir
247 context, ui, repos, req, stripecount, sortcolumn, descending, subdir
248 ):
248 ):
249 rows = rawindexentries(ui, repos, req, subdir=subdir)
249 rows = rawindexentries(ui, repos, req, subdir=subdir)
250
250
251 sortdefault = None, False
251 sortdefault = None, False
252
252
253 if sortcolumn and sortdefault != (sortcolumn, descending):
253 if sortcolumn and sortdefault != (sortcolumn, descending):
254 sortkey = b'%s_sort' % sortcolumn
254 sortkey = b'%s_sort' % sortcolumn
255 rows = sorted(rows, key=lambda x: x[sortkey], reverse=descending)
255 rows = sorted(rows, key=lambda x: x[sortkey], reverse=descending)
256
256
257 for row, parity in zip(rows, paritygen(stripecount)):
257 for row, parity in zip(rows, paritygen(stripecount)):
258 row[b'parity'] = parity
258 row[b'parity'] = parity
259 yield row
259 yield row
260
260
261
261
262 def indexentries(
262 def indexentries(
263 ui, repos, req, stripecount, sortcolumn=b'', descending=False, subdir=b''
263 ui, repos, req, stripecount, sortcolumn=b'', descending=False, subdir=b''
264 ):
264 ):
265 args = (ui, repos, req, stripecount, sortcolumn, descending, subdir)
265 args = (ui, repos, req, stripecount, sortcolumn, descending, subdir)
266 return templateutil.mappinggenerator(_indexentriesgen, args=args)
266 return templateutil.mappinggenerator(_indexentriesgen, args=args)
267
267
268
268
269 class hgwebdir(object):
269 class hgwebdir(object):
270 """HTTP server for multiple repositories.
270 """HTTP server for multiple repositories.
271
271
272 Given a configuration, different repositories will be served depending
272 Given a configuration, different repositories will be served depending
273 on the request path.
273 on the request path.
274
274
275 Instances are typically used as WSGI applications.
275 Instances are typically used as WSGI applications.
276 """
276 """
277
277
278 def __init__(self, conf, baseui=None):
278 def __init__(self, conf, baseui=None):
279 self.conf = conf
279 self.conf = conf
280 self.baseui = baseui
280 self.baseui = baseui
281 self.ui = None
281 self.ui = None
282 self.lastrefresh = 0
282 self.lastrefresh = 0
283 self.motd = None
283 self.motd = None
284 self.refresh()
284 self.refresh()
285 if not baseui:
285 if not baseui:
286 # set up environment for new ui
286 # set up environment for new ui
287 extensions.loadall(self.ui)
287 extensions.loadall(self.ui)
288 extensions.populateui(self.ui)
288 extensions.populateui(self.ui)
289
289
290 def refresh(self):
290 def refresh(self):
291 if self.ui:
291 if self.ui:
292 refreshinterval = self.ui.configint(b'web', b'refreshinterval')
292 refreshinterval = self.ui.configint(b'web', b'refreshinterval')
293 else:
293 else:
294 item = configitems.coreitems[b'web'][b'refreshinterval']
294 item = configitems.coreitems[b'web'][b'refreshinterval']
295 refreshinterval = item.default
295 refreshinterval = item.default
296
296
297 # refreshinterval <= 0 means to always refresh.
297 # refreshinterval <= 0 means to always refresh.
298 if (
298 if (
299 refreshinterval > 0
299 refreshinterval > 0
300 and self.lastrefresh + refreshinterval > time.time()
300 and self.lastrefresh + refreshinterval > time.time()
301 ):
301 ):
302 return
302 return
303
303
304 if self.baseui:
304 if self.baseui:
305 u = self.baseui.copy()
305 u = self.baseui.copy()
306 else:
306 else:
307 u = uimod.ui.load()
307 u = uimod.ui.load()
308 u.setconfig(b'ui', b'report_untrusted', b'off', b'hgwebdir')
308 u.setconfig(b'ui', b'report_untrusted', b'off', b'hgwebdir')
309 u.setconfig(b'ui', b'nontty', b'true', b'hgwebdir')
309 u.setconfig(b'ui', b'nontty', b'true', b'hgwebdir')
310 # displaying bundling progress bar while serving feels wrong and may
310 # displaying bundling progress bar while serving feels wrong and may
311 # break some wsgi implementations.
311 # break some wsgi implementations.
312 u.setconfig(b'progress', b'disable', b'true', b'hgweb')
312 u.setconfig(b'progress', b'disable', b'true', b'hgweb')
313
313
314 if not isinstance(self.conf, (dict, list, tuple)):
314 if not isinstance(self.conf, (dict, list, tuple)):
315 map = {b'paths': b'hgweb-paths'}
315 map = {b'paths': b'hgweb-paths'}
316 if not os.path.exists(self.conf):
316 if not os.path.exists(self.conf):
317 raise error.Abort(_(b'config file %s not found!') % self.conf)
317 raise error.Abort(_(b'config file %s not found!') % self.conf)
318 u.readconfig(self.conf, remap=map, trust=True)
318 u.readconfig(self.conf, remap=map, trust=True)
319 paths = []
319 paths = []
320 for name, ignored in u.configitems(b'hgweb-paths'):
320 for name, ignored in u.configitems(b'hgweb-paths'):
321 for path in u.configlist(b'hgweb-paths', name):
321 for path in u.configlist(b'hgweb-paths', name):
322 paths.append((name, path))
322 paths.append((name, path))
323 elif isinstance(self.conf, (list, tuple)):
323 elif isinstance(self.conf, (list, tuple)):
324 paths = self.conf
324 paths = self.conf
325 elif isinstance(self.conf, dict):
325 elif isinstance(self.conf, dict):
326 paths = self.conf.items()
326 paths = self.conf.items()
327 extensions.populateui(u)
327 extensions.populateui(u)
328
328
329 repos = findrepos(paths)
329 repos = findrepos(paths)
330 for prefix, root in u.configitems(b'collections'):
330 for prefix, root in u.configitems(b'collections'):
331 prefix = util.pconvert(prefix)
331 prefix = util.pconvert(prefix)
332 for path in scmutil.walkrepos(root, followsym=True):
332 for path in scmutil.walkrepos(root, followsym=True):
333 repo = os.path.normpath(path)
333 repo = os.path.normpath(path)
334 name = util.pconvert(repo)
334 name = util.pconvert(repo)
335 if name.startswith(prefix):
335 if name.startswith(prefix):
336 name = name[len(prefix) :]
336 name = name[len(prefix) :]
337 repos.append((name.lstrip(b'/'), repo))
337 repos.append((name.lstrip(b'/'), repo))
338
338
339 self.repos = repos
339 self.repos = repos
340 self.ui = u
340 self.ui = u
341 encoding.encoding = self.ui.config(b'web', b'encoding')
341 encoding.encoding = self.ui.config(b'web', b'encoding')
342 self.style = self.ui.config(b'web', b'style')
342 self.style = self.ui.config(b'web', b'style')
343 self.templatepath = self.ui.config(
343 self.templatepath = self.ui.config(
344 b'web', b'templates', untrusted=False
344 b'web', b'templates', untrusted=False
345 )
345 )
346 self.stripecount = self.ui.config(b'web', b'stripes')
346 self.stripecount = self.ui.config(b'web', b'stripes')
347 if self.stripecount:
347 if self.stripecount:
348 self.stripecount = int(self.stripecount)
348 self.stripecount = int(self.stripecount)
349 prefix = self.ui.config(b'web', b'prefix')
349 prefix = self.ui.config(b'web', b'prefix')
350 if prefix.startswith(b'/'):
350 if prefix.startswith(b'/'):
351 prefix = prefix[1:]
351 prefix = prefix[1:]
352 if prefix.endswith(b'/'):
352 if prefix.endswith(b'/'):
353 prefix = prefix[:-1]
353 prefix = prefix[:-1]
354 self.prefix = prefix
354 self.prefix = prefix
355 self.lastrefresh = time.time()
355 self.lastrefresh = time.time()
356
356
357 def run(self):
357 def run(self):
358 if not encoding.environ.get(b'GATEWAY_INTERFACE', b'').startswith(
358 if not encoding.environ.get(b'GATEWAY_INTERFACE', b'').startswith(
359 b"CGI/1."
359 b"CGI/1."
360 ):
360 ):
361 raise RuntimeError(
361 raise RuntimeError(
362 b"This function is only intended to be "
362 b"This function is only intended to be "
363 b"called while running as a CGI script."
363 b"called while running as a CGI script."
364 )
364 )
365 wsgicgi.launch(self)
365 wsgicgi.launch(self)
366
366
367 def __call__(self, env, respond):
367 def __call__(self, env, respond):
368 baseurl = self.ui.config(b'web', b'baseurl')
368 baseurl = self.ui.config(b'web', b'baseurl')
369 req = requestmod.parserequestfromenv(env, altbaseurl=baseurl)
369 req = requestmod.parserequestfromenv(env, altbaseurl=baseurl)
370 res = requestmod.wsgiresponse(req, respond)
370 res = requestmod.wsgiresponse(req, respond)
371
371
372 return self.run_wsgi(req, res)
372 return self.run_wsgi(req, res)
373
373
374 def run_wsgi(self, req, res):
374 def run_wsgi(self, req, res):
375 profile = self.ui.configbool(b'profiling', b'enabled')
375 profile = self.ui.configbool(b'profiling', b'enabled')
376 with profiling.profile(self.ui, enabled=profile):
376 with profiling.profile(self.ui, enabled=profile):
377 try:
377 try:
378 for r in self._runwsgi(req, res):
378 for r in self._runwsgi(req, res):
379 yield r
379 yield r
380 finally:
380 finally:
381 # There are known cycles in localrepository that prevent
381 # There are known cycles in localrepository that prevent
382 # those objects (and tons of held references) from being
382 # those objects (and tons of held references) from being
383 # collected through normal refcounting. We mitigate those
383 # collected through normal refcounting. We mitigate those
384 # leaks by performing an explicit GC on every request.
384 # leaks by performing an explicit GC on every request.
385 # TODO remove this once leaks are fixed.
385 # TODO remove this once leaks are fixed.
386 # TODO only run this on requests that create localrepository
386 # TODO only run this on requests that create localrepository
387 # instances instead of every request.
387 # instances instead of every request.
388 gc.collect()
388 gc.collect()
389
389
390 def _runwsgi(self, req, res):
390 def _runwsgi(self, req, res):
391 try:
391 try:
392 self.refresh()
392 self.refresh()
393
393
394 csp, nonce = cspvalues(self.ui)
394 csp, nonce = cspvalues(self.ui)
395 if csp:
395 if csp:
396 res.headers[b'Content-Security-Policy'] = csp
396 res.headers[b'Content-Security-Policy'] = csp
397
397
398 virtual = req.dispatchpath.strip(b'/')
398 virtual = req.dispatchpath.strip(b'/')
399 tmpl = self.templater(req, nonce)
399 tmpl = self.templater(req, nonce)
400 ctype = tmpl.render(b'mimetype', {b'encoding': encoding.encoding})
400 ctype = tmpl.render(b'mimetype', {b'encoding': encoding.encoding})
401
401
402 # Global defaults. These can be overridden by any handler.
402 # Global defaults. These can be overridden by any handler.
403 res.status = b'200 Script output follows'
403 res.status = b'200 Script output follows'
404 res.headers[b'Content-Type'] = ctype
404 res.headers[b'Content-Type'] = ctype
405
405
406 # a static file
406 # a static file
407 if virtual.startswith(b'static/') or b'static' in req.qsparams:
407 if virtual.startswith(b'static/') or b'static' in req.qsparams:
408 if virtual.startswith(b'static/'):
408 if virtual.startswith(b'static/'):
409 fname = virtual[7:]
409 fname = virtual[7:]
410 else:
410 else:
411 fname = req.qsparams[b'static']
411 fname = req.qsparams[b'static']
412 static = self.ui.config(b"web", b"static", untrusted=False)
412 static = self.ui.config(b"web", b"static", untrusted=False)
413 if not static:
413 if not static:
414 tp = self.templatepath or templater.templatepaths()
414 tp = self.templatepath or templater.templatepaths()
415 if isinstance(tp, str):
415 if isinstance(tp, bytes):
416 tp = [tp]
416 tp = [tp]
417 static = [os.path.join(p, b'static') for p in tp]
417 static = [os.path.join(p, b'static') for p in tp]
418
418
419 staticfile(static, fname, res)
419 staticfile(static, fname, res)
420 return res.sendresponse()
420 return res.sendresponse()
421
421
422 # top-level index
422 # top-level index
423
423
424 repos = dict(self.repos)
424 repos = dict(self.repos)
425
425
426 if (not virtual or virtual == b'index') and virtual not in repos:
426 if (not virtual or virtual == b'index') and virtual not in repos:
427 return self.makeindex(req, res, tmpl)
427 return self.makeindex(req, res, tmpl)
428
428
429 # nested indexes and hgwebs
429 # nested indexes and hgwebs
430
430
431 if virtual.endswith(b'/index') and virtual not in repos:
431 if virtual.endswith(b'/index') and virtual not in repos:
432 subdir = virtual[: -len(b'index')]
432 subdir = virtual[: -len(b'index')]
433 if any(r.startswith(subdir) for r in repos):
433 if any(r.startswith(subdir) for r in repos):
434 return self.makeindex(req, res, tmpl, subdir)
434 return self.makeindex(req, res, tmpl, subdir)
435
435
436 def _virtualdirs():
436 def _virtualdirs():
437 # Check the full virtual path, and each parent
437 # Check the full virtual path, and each parent
438 yield virtual
438 yield virtual
439 for p in util.finddirs(virtual):
439 for p in util.finddirs(virtual):
440 yield p
440 yield p
441
441
442 for virtualrepo in _virtualdirs():
442 for virtualrepo in _virtualdirs():
443 real = repos.get(virtualrepo)
443 real = repos.get(virtualrepo)
444 if real:
444 if real:
445 # Re-parse the WSGI environment to take into account our
445 # Re-parse the WSGI environment to take into account our
446 # repository path component.
446 # repository path component.
447 uenv = req.rawenv
447 uenv = req.rawenv
448 if pycompat.ispy3:
448 if pycompat.ispy3:
449 uenv = {
449 uenv = {
450 k.decode('latin1'): v
450 k.decode('latin1'): v
451 for k, v in pycompat.iteritems(uenv)
451 for k, v in pycompat.iteritems(uenv)
452 }
452 }
453 req = requestmod.parserequestfromenv(
453 req = requestmod.parserequestfromenv(
454 uenv,
454 uenv,
455 reponame=virtualrepo,
455 reponame=virtualrepo,
456 altbaseurl=self.ui.config(b'web', b'baseurl'),
456 altbaseurl=self.ui.config(b'web', b'baseurl'),
457 # Reuse wrapped body file object otherwise state
457 # Reuse wrapped body file object otherwise state
458 # tracking can get confused.
458 # tracking can get confused.
459 bodyfh=req.bodyfh,
459 bodyfh=req.bodyfh,
460 )
460 )
461 try:
461 try:
462 # ensure caller gets private copy of ui
462 # ensure caller gets private copy of ui
463 repo = hg.repository(self.ui.copy(), real)
463 repo = hg.repository(self.ui.copy(), real)
464 return hgweb_mod.hgweb(repo).run_wsgi(req, res)
464 return hgweb_mod.hgweb(repo).run_wsgi(req, res)
465 except IOError as inst:
465 except IOError as inst:
466 msg = encoding.strtolocal(inst.strerror)
466 msg = encoding.strtolocal(inst.strerror)
467 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
467 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
468 except error.RepoError as inst:
468 except error.RepoError as inst:
469 raise ErrorResponse(HTTP_SERVER_ERROR, bytes(inst))
469 raise ErrorResponse(HTTP_SERVER_ERROR, bytes(inst))
470
470
471 # browse subdirectories
471 # browse subdirectories
472 subdir = virtual + b'/'
472 subdir = virtual + b'/'
473 if [r for r in repos if r.startswith(subdir)]:
473 if [r for r in repos if r.startswith(subdir)]:
474 return self.makeindex(req, res, tmpl, subdir)
474 return self.makeindex(req, res, tmpl, subdir)
475
475
476 # prefixes not found
476 # prefixes not found
477 res.status = b'404 Not Found'
477 res.status = b'404 Not Found'
478 res.setbodygen(tmpl.generate(b'notfound', {b'repo': virtual}))
478 res.setbodygen(tmpl.generate(b'notfound', {b'repo': virtual}))
479 return res.sendresponse()
479 return res.sendresponse()
480
480
481 except ErrorResponse as e:
481 except ErrorResponse as e:
482 res.status = statusmessage(e.code, pycompat.bytestr(e))
482 res.status = statusmessage(e.code, pycompat.bytestr(e))
483 res.setbodygen(
483 res.setbodygen(
484 tmpl.generate(b'error', {b'error': e.message or b''})
484 tmpl.generate(b'error', {b'error': e.message or b''})
485 )
485 )
486 return res.sendresponse()
486 return res.sendresponse()
487 finally:
487 finally:
488 tmpl = None
488 tmpl = None
489
489
490 def makeindex(self, req, res, tmpl, subdir=b""):
490 def makeindex(self, req, res, tmpl, subdir=b""):
491 self.refresh()
491 self.refresh()
492 sortable = [b"name", b"description", b"contact", b"lastchange"]
492 sortable = [b"name", b"description", b"contact", b"lastchange"]
493 sortcolumn, descending = None, False
493 sortcolumn, descending = None, False
494 if b'sort' in req.qsparams:
494 if b'sort' in req.qsparams:
495 sortcolumn = req.qsparams[b'sort']
495 sortcolumn = req.qsparams[b'sort']
496 descending = sortcolumn.startswith(b'-')
496 descending = sortcolumn.startswith(b'-')
497 if descending:
497 if descending:
498 sortcolumn = sortcolumn[1:]
498 sortcolumn = sortcolumn[1:]
499 if sortcolumn not in sortable:
499 if sortcolumn not in sortable:
500 sortcolumn = b""
500 sortcolumn = b""
501
501
502 sort = [
502 sort = [
503 (
503 (
504 b"sort_%s" % column,
504 b"sort_%s" % column,
505 b"%s%s"
505 b"%s%s"
506 % (
506 % (
507 (not descending and column == sortcolumn) and b"-" or b"",
507 (not descending and column == sortcolumn) and b"-" or b"",
508 column,
508 column,
509 ),
509 ),
510 )
510 )
511 for column in sortable
511 for column in sortable
512 ]
512 ]
513
513
514 self.refresh()
514 self.refresh()
515
515
516 entries = indexentries(
516 entries = indexentries(
517 self.ui,
517 self.ui,
518 self.repos,
518 self.repos,
519 req,
519 req,
520 self.stripecount,
520 self.stripecount,
521 sortcolumn=sortcolumn,
521 sortcolumn=sortcolumn,
522 descending=descending,
522 descending=descending,
523 subdir=subdir,
523 subdir=subdir,
524 )
524 )
525
525
526 mapping = {
526 mapping = {
527 b'entries': entries,
527 b'entries': entries,
528 b'subdir': subdir,
528 b'subdir': subdir,
529 b'pathdef': hgweb_mod.makebreadcrumb(b'/' + subdir, self.prefix),
529 b'pathdef': hgweb_mod.makebreadcrumb(b'/' + subdir, self.prefix),
530 b'sortcolumn': sortcolumn,
530 b'sortcolumn': sortcolumn,
531 b'descending': descending,
531 b'descending': descending,
532 }
532 }
533 mapping.update(sort)
533 mapping.update(sort)
534 res.setbodygen(tmpl.generate(b'index', mapping))
534 res.setbodygen(tmpl.generate(b'index', mapping))
535 return res.sendresponse()
535 return res.sendresponse()
536
536
537 def templater(self, req, nonce):
537 def templater(self, req, nonce):
538 def config(section, name, default=uimod._unset, untrusted=True):
538 def config(section, name, default=uimod._unset, untrusted=True):
539 return self.ui.config(section, name, default, untrusted)
539 return self.ui.config(section, name, default, untrusted)
540
540
541 vars = {}
541 vars = {}
542 styles, (style, mapfile) = hgweb_mod.getstyle(
542 styles, (style, mapfile) = hgweb_mod.getstyle(
543 req, config, self.templatepath
543 req, config, self.templatepath
544 )
544 )
545 if style == styles[0]:
545 if style == styles[0]:
546 vars[b'style'] = style
546 vars[b'style'] = style
547
547
548 sessionvars = webutil.sessionvars(vars, b'?')
548 sessionvars = webutil.sessionvars(vars, b'?')
549 logourl = config(b'web', b'logourl')
549 logourl = config(b'web', b'logourl')
550 logoimg = config(b'web', b'logoimg')
550 logoimg = config(b'web', b'logoimg')
551 staticurl = (
551 staticurl = (
552 config(b'web', b'staticurl')
552 config(b'web', b'staticurl')
553 or req.apppath.rstrip(b'/') + b'/static/'
553 or req.apppath.rstrip(b'/') + b'/static/'
554 )
554 )
555 if not staticurl.endswith(b'/'):
555 if not staticurl.endswith(b'/'):
556 staticurl += b'/'
556 staticurl += b'/'
557
557
558 defaults = {
558 defaults = {
559 b"encoding": encoding.encoding,
559 b"encoding": encoding.encoding,
560 b"url": req.apppath + b'/',
560 b"url": req.apppath + b'/',
561 b"logourl": logourl,
561 b"logourl": logourl,
562 b"logoimg": logoimg,
562 b"logoimg": logoimg,
563 b"staticurl": staticurl,
563 b"staticurl": staticurl,
564 b"sessionvars": sessionvars,
564 b"sessionvars": sessionvars,
565 b"style": style,
565 b"style": style,
566 b"nonce": nonce,
566 b"nonce": nonce,
567 }
567 }
568 templatekeyword = registrar.templatekeyword(defaults)
568 templatekeyword = registrar.templatekeyword(defaults)
569
569
570 @templatekeyword(b'motd', requires=())
570 @templatekeyword(b'motd', requires=())
571 def motd(context, mapping):
571 def motd(context, mapping):
572 if self.motd is not None:
572 if self.motd is not None:
573 yield self.motd
573 yield self.motd
574 else:
574 else:
575 yield config(b'web', b'motd')
575 yield config(b'web', b'motd')
576
576
577 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
577 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
578 return tmpl
578 return tmpl
@@ -1,1599 +1,1599 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 pats = []
1271 pats = []
1272 match = scmutil.match(ctx, [])
1272 match = scmutil.match(ctx, [])
1273 file = web.req.qsparams.get(b'file')
1273 file = web.req.qsparams.get(b'file')
1274 if file:
1274 if file:
1275 pats = [b'path:' + file]
1275 pats = [b'path:' + file]
1276 match = scmutil.match(ctx, pats, default=b'path')
1276 match = scmutil.match(ctx, pats, default=b'path')
1277 if pats:
1277 if pats:
1278 files = [f for f in ctx.manifest().keys() if match(f)]
1278 files = [f for f in ctx.manifest().keys() if match(f)]
1279 if not files:
1279 if not files:
1280 raise ErrorResponse(
1280 raise ErrorResponse(
1281 HTTP_NOT_FOUND, b'file(s) not found: %s' % file
1281 HTTP_NOT_FOUND, b'file(s) not found: %s' % file
1282 )
1282 )
1283
1283
1284 mimetype, artype, extension, encoding = webutil.archivespecs[type_]
1284 mimetype, artype, extension, encoding = webutil.archivespecs[type_]
1285
1285
1286 web.res.headers[b'Content-Type'] = mimetype
1286 web.res.headers[b'Content-Type'] = mimetype
1287 web.res.headers[b'Content-Disposition'] = b'attachment; filename=%s%s' % (
1287 web.res.headers[b'Content-Disposition'] = b'attachment; filename=%s%s' % (
1288 name,
1288 name,
1289 extension,
1289 extension,
1290 )
1290 )
1291
1291
1292 if encoding:
1292 if encoding:
1293 web.res.headers[b'Content-Encoding'] = encoding
1293 web.res.headers[b'Content-Encoding'] = encoding
1294
1294
1295 web.res.setbodywillwrite()
1295 web.res.setbodywillwrite()
1296 if list(web.res.sendresponse()):
1296 if list(web.res.sendresponse()):
1297 raise error.ProgrammingError(
1297 raise error.ProgrammingError(
1298 b'sendresponse() should not emit data if writing later'
1298 b'sendresponse() should not emit data if writing later'
1299 )
1299 )
1300
1300
1301 bodyfh = web.res.getbodyfile()
1301 bodyfh = web.res.getbodyfile()
1302
1302
1303 archival.archive(
1303 archival.archive(
1304 web.repo,
1304 web.repo,
1305 bodyfh,
1305 bodyfh,
1306 cnode,
1306 cnode,
1307 artype,
1307 artype,
1308 prefix=name,
1308 prefix=name,
1309 match=match,
1309 match=match,
1310 subrepos=web.configbool(b"web", b"archivesubrepos"),
1310 subrepos=web.configbool(b"web", b"archivesubrepos"),
1311 )
1311 )
1312
1312
1313 return []
1313 return []
1314
1314
1315
1315
1316 @webcommand(b'static')
1316 @webcommand(b'static')
1317 def static(web):
1317 def static(web):
1318 fname = web.req.qsparams[b'file']
1318 fname = web.req.qsparams[b'file']
1319 # a repo owner may set web.static in .hg/hgrc to get any file
1319 # a repo owner may set web.static in .hg/hgrc to get any file
1320 # readable by the user running the CGI script
1320 # readable by the user running the CGI script
1321 static = web.config(b"web", b"static", untrusted=False)
1321 static = web.config(b"web", b"static", untrusted=False)
1322 if not static:
1322 if not static:
1323 tp = web.templatepath or templater.templatepaths()
1323 tp = web.templatepath or templater.templatepaths()
1324 if isinstance(tp, str):
1324 if isinstance(tp, bytes):
1325 tp = [tp]
1325 tp = [tp]
1326 static = [os.path.join(p, b'static') for p in tp]
1326 static = [os.path.join(p, b'static') for p in tp]
1327
1327
1328 staticfile(static, fname, web.res)
1328 staticfile(static, fname, web.res)
1329 return web.res.sendresponse()
1329 return web.res.sendresponse()
1330
1330
1331
1331
1332 @webcommand(b'graph')
1332 @webcommand(b'graph')
1333 def graph(web):
1333 def graph(web):
1334 """
1334 """
1335 /graph[/{revision}]
1335 /graph[/{revision}]
1336 -------------------
1336 -------------------
1337
1337
1338 Show information about the graphical topology of the repository.
1338 Show information about the graphical topology of the repository.
1339
1339
1340 Information rendered by this handler can be used to create visual
1340 Information rendered by this handler can be used to create visual
1341 representations of repository topology.
1341 representations of repository topology.
1342
1342
1343 The ``revision`` URL parameter controls the starting changeset. If it's
1343 The ``revision`` URL parameter controls the starting changeset. If it's
1344 absent, the default is ``tip``.
1344 absent, the default is ``tip``.
1345
1345
1346 The ``revcount`` query string argument can define the number of changesets
1346 The ``revcount`` query string argument can define the number of changesets
1347 to show information for.
1347 to show information for.
1348
1348
1349 The ``graphtop`` query string argument can specify the starting changeset
1349 The ``graphtop`` query string argument can specify the starting changeset
1350 for producing ``jsdata`` variable that is used for rendering graph in
1350 for producing ``jsdata`` variable that is used for rendering graph in
1351 JavaScript. By default it has the same value as ``revision``.
1351 JavaScript. By default it has the same value as ``revision``.
1352
1352
1353 This handler will render the ``graph`` template.
1353 This handler will render the ``graph`` template.
1354 """
1354 """
1355
1355
1356 if b'node' in web.req.qsparams:
1356 if b'node' in web.req.qsparams:
1357 ctx = webutil.changectx(web.repo, web.req)
1357 ctx = webutil.changectx(web.repo, web.req)
1358 symrev = webutil.symrevorshortnode(web.req, ctx)
1358 symrev = webutil.symrevorshortnode(web.req, ctx)
1359 else:
1359 else:
1360 ctx = web.repo[b'tip']
1360 ctx = web.repo[b'tip']
1361 symrev = b'tip'
1361 symrev = b'tip'
1362 rev = ctx.rev()
1362 rev = ctx.rev()
1363
1363
1364 bg_height = 39
1364 bg_height = 39
1365 revcount = web.maxshortchanges
1365 revcount = web.maxshortchanges
1366 if b'revcount' in web.req.qsparams:
1366 if b'revcount' in web.req.qsparams:
1367 try:
1367 try:
1368 revcount = int(web.req.qsparams.get(b'revcount', revcount))
1368 revcount = int(web.req.qsparams.get(b'revcount', revcount))
1369 revcount = max(revcount, 1)
1369 revcount = max(revcount, 1)
1370 web.tmpl.defaults[b'sessionvars'][b'revcount'] = revcount
1370 web.tmpl.defaults[b'sessionvars'][b'revcount'] = revcount
1371 except ValueError:
1371 except ValueError:
1372 pass
1372 pass
1373
1373
1374 lessvars = copy.copy(web.tmpl.defaults[b'sessionvars'])
1374 lessvars = copy.copy(web.tmpl.defaults[b'sessionvars'])
1375 lessvars[b'revcount'] = max(revcount // 2, 1)
1375 lessvars[b'revcount'] = max(revcount // 2, 1)
1376 morevars = copy.copy(web.tmpl.defaults[b'sessionvars'])
1376 morevars = copy.copy(web.tmpl.defaults[b'sessionvars'])
1377 morevars[b'revcount'] = revcount * 2
1377 morevars[b'revcount'] = revcount * 2
1378
1378
1379 graphtop = web.req.qsparams.get(b'graphtop', ctx.hex())
1379 graphtop = web.req.qsparams.get(b'graphtop', ctx.hex())
1380 graphvars = copy.copy(web.tmpl.defaults[b'sessionvars'])
1380 graphvars = copy.copy(web.tmpl.defaults[b'sessionvars'])
1381 graphvars[b'graphtop'] = graphtop
1381 graphvars[b'graphtop'] = graphtop
1382
1382
1383 count = len(web.repo)
1383 count = len(web.repo)
1384 pos = rev
1384 pos = rev
1385
1385
1386 uprev = min(max(0, count - 1), rev + revcount)
1386 uprev = min(max(0, count - 1), rev + revcount)
1387 downrev = max(0, rev - revcount)
1387 downrev = max(0, rev - revcount)
1388 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1388 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1389
1389
1390 tree = []
1390 tree = []
1391 nextentry = []
1391 nextentry = []
1392 lastrev = 0
1392 lastrev = 0
1393 if pos != -1:
1393 if pos != -1:
1394 allrevs = web.repo.changelog.revs(pos, 0)
1394 allrevs = web.repo.changelog.revs(pos, 0)
1395 revs = []
1395 revs = []
1396 for i in allrevs:
1396 for i in allrevs:
1397 revs.append(i)
1397 revs.append(i)
1398 if len(revs) >= revcount + 1:
1398 if len(revs) >= revcount + 1:
1399 break
1399 break
1400
1400
1401 if len(revs) > revcount:
1401 if len(revs) > revcount:
1402 nextentry = [webutil.commonentry(web.repo, web.repo[revs[-1]])]
1402 nextentry = [webutil.commonentry(web.repo, web.repo[revs[-1]])]
1403 revs = revs[:-1]
1403 revs = revs[:-1]
1404
1404
1405 lastrev = revs[-1]
1405 lastrev = revs[-1]
1406
1406
1407 # We have to feed a baseset to dagwalker as it is expecting smartset
1407 # We have to feed a baseset to dagwalker as it is expecting smartset
1408 # object. This does not have a big impact on hgweb performance itself
1408 # object. This does not have a big impact on hgweb performance itself
1409 # since hgweb graphing code is not itself lazy yet.
1409 # since hgweb graphing code is not itself lazy yet.
1410 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1410 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1411 # As we said one line above... not lazy.
1411 # As we said one line above... not lazy.
1412 tree = list(
1412 tree = list(
1413 item
1413 item
1414 for item in graphmod.colored(dag, web.repo)
1414 for item in graphmod.colored(dag, web.repo)
1415 if item[1] == graphmod.CHANGESET
1415 if item[1] == graphmod.CHANGESET
1416 )
1416 )
1417
1417
1418 def fulltree():
1418 def fulltree():
1419 pos = web.repo[graphtop].rev()
1419 pos = web.repo[graphtop].rev()
1420 tree = []
1420 tree = []
1421 if pos != -1:
1421 if pos != -1:
1422 revs = web.repo.changelog.revs(pos, lastrev)
1422 revs = web.repo.changelog.revs(pos, lastrev)
1423 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1423 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1424 tree = list(
1424 tree = list(
1425 item
1425 item
1426 for item in graphmod.colored(dag, web.repo)
1426 for item in graphmod.colored(dag, web.repo)
1427 if item[1] == graphmod.CHANGESET
1427 if item[1] == graphmod.CHANGESET
1428 )
1428 )
1429 return tree
1429 return tree
1430
1430
1431 def jsdata(context):
1431 def jsdata(context):
1432 for (id, type, ctx, vtx, edges) in fulltree():
1432 for (id, type, ctx, vtx, edges) in fulltree():
1433 yield {
1433 yield {
1434 b'node': pycompat.bytestr(ctx),
1434 b'node': pycompat.bytestr(ctx),
1435 b'graphnode': webutil.getgraphnode(web.repo, ctx),
1435 b'graphnode': webutil.getgraphnode(web.repo, ctx),
1436 b'vertex': vtx,
1436 b'vertex': vtx,
1437 b'edges': edges,
1437 b'edges': edges,
1438 }
1438 }
1439
1439
1440 def nodes(context):
1440 def nodes(context):
1441 parity = paritygen(web.stripecount)
1441 parity = paritygen(web.stripecount)
1442 for row, (id, type, ctx, vtx, edges) in enumerate(tree):
1442 for row, (id, type, ctx, vtx, edges) in enumerate(tree):
1443 entry = webutil.commonentry(web.repo, ctx)
1443 entry = webutil.commonentry(web.repo, ctx)
1444 edgedata = [
1444 edgedata = [
1445 {
1445 {
1446 b'col': edge[0],
1446 b'col': edge[0],
1447 b'nextcol': edge[1],
1447 b'nextcol': edge[1],
1448 b'color': (edge[2] - 1) % 6 + 1,
1448 b'color': (edge[2] - 1) % 6 + 1,
1449 b'width': edge[3],
1449 b'width': edge[3],
1450 b'bcolor': edge[4],
1450 b'bcolor': edge[4],
1451 }
1451 }
1452 for edge in edges
1452 for edge in edges
1453 ]
1453 ]
1454
1454
1455 entry.update(
1455 entry.update(
1456 {
1456 {
1457 b'col': vtx[0],
1457 b'col': vtx[0],
1458 b'color': (vtx[1] - 1) % 6 + 1,
1458 b'color': (vtx[1] - 1) % 6 + 1,
1459 b'parity': next(parity),
1459 b'parity': next(parity),
1460 b'edges': templateutil.mappinglist(edgedata),
1460 b'edges': templateutil.mappinglist(edgedata),
1461 b'row': row,
1461 b'row': row,
1462 b'nextrow': row + 1,
1462 b'nextrow': row + 1,
1463 }
1463 }
1464 )
1464 )
1465
1465
1466 yield entry
1466 yield entry
1467
1467
1468 rows = len(tree)
1468 rows = len(tree)
1469
1469
1470 return web.sendtemplate(
1470 return web.sendtemplate(
1471 b'graph',
1471 b'graph',
1472 rev=rev,
1472 rev=rev,
1473 symrev=symrev,
1473 symrev=symrev,
1474 revcount=revcount,
1474 revcount=revcount,
1475 uprev=uprev,
1475 uprev=uprev,
1476 lessvars=lessvars,
1476 lessvars=lessvars,
1477 morevars=morevars,
1477 morevars=morevars,
1478 downrev=downrev,
1478 downrev=downrev,
1479 graphvars=graphvars,
1479 graphvars=graphvars,
1480 rows=rows,
1480 rows=rows,
1481 bg_height=bg_height,
1481 bg_height=bg_height,
1482 changesets=count,
1482 changesets=count,
1483 nextentry=templateutil.mappinglist(nextentry),
1483 nextentry=templateutil.mappinglist(nextentry),
1484 jsdata=templateutil.mappinggenerator(jsdata),
1484 jsdata=templateutil.mappinggenerator(jsdata),
1485 nodes=templateutil.mappinggenerator(nodes),
1485 nodes=templateutil.mappinggenerator(nodes),
1486 node=ctx.hex(),
1486 node=ctx.hex(),
1487 archives=web.archivelist(b'tip'),
1487 archives=web.archivelist(b'tip'),
1488 changenav=changenav,
1488 changenav=changenav,
1489 )
1489 )
1490
1490
1491
1491
1492 def _getdoc(e):
1492 def _getdoc(e):
1493 doc = e[0].__doc__
1493 doc = e[0].__doc__
1494 if doc:
1494 if doc:
1495 doc = _(doc).partition(b'\n')[0]
1495 doc = _(doc).partition(b'\n')[0]
1496 else:
1496 else:
1497 doc = _(b'(no help text available)')
1497 doc = _(b'(no help text available)')
1498 return doc
1498 return doc
1499
1499
1500
1500
1501 @webcommand(b'help')
1501 @webcommand(b'help')
1502 def help(web):
1502 def help(web):
1503 """
1503 """
1504 /help[/{topic}]
1504 /help[/{topic}]
1505 ---------------
1505 ---------------
1506
1506
1507 Render help documentation.
1507 Render help documentation.
1508
1508
1509 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1509 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1510 is defined, that help topic will be rendered. If not, an index of
1510 is defined, that help topic will be rendered. If not, an index of
1511 available help topics will be rendered.
1511 available help topics will be rendered.
1512
1512
1513 The ``help`` template will be rendered when requesting help for a topic.
1513 The ``help`` template will be rendered when requesting help for a topic.
1514 ``helptopics`` will be rendered for the index of help topics.
1514 ``helptopics`` will be rendered for the index of help topics.
1515 """
1515 """
1516 from .. import commands, help as helpmod # avoid cycle
1516 from .. import commands, help as helpmod # avoid cycle
1517
1517
1518 topicname = web.req.qsparams.get(b'node')
1518 topicname = web.req.qsparams.get(b'node')
1519 if not topicname:
1519 if not topicname:
1520
1520
1521 def topics(context):
1521 def topics(context):
1522 for h in helpmod.helptable:
1522 for h in helpmod.helptable:
1523 entries, summary, _doc = h[0:3]
1523 entries, summary, _doc = h[0:3]
1524 yield {b'topic': entries[0], b'summary': summary}
1524 yield {b'topic': entries[0], b'summary': summary}
1525
1525
1526 early, other = [], []
1526 early, other = [], []
1527 primary = lambda s: s.partition(b'|')[0]
1527 primary = lambda s: s.partition(b'|')[0]
1528 for c, e in pycompat.iteritems(commands.table):
1528 for c, e in pycompat.iteritems(commands.table):
1529 doc = _getdoc(e)
1529 doc = _getdoc(e)
1530 if b'DEPRECATED' in doc or c.startswith(b'debug'):
1530 if b'DEPRECATED' in doc or c.startswith(b'debug'):
1531 continue
1531 continue
1532 cmd = primary(c)
1532 cmd = primary(c)
1533 if getattr(e[0], 'helpbasic', False):
1533 if getattr(e[0], 'helpbasic', False):
1534 early.append((cmd, doc))
1534 early.append((cmd, doc))
1535 else:
1535 else:
1536 other.append((cmd, doc))
1536 other.append((cmd, doc))
1537
1537
1538 early.sort()
1538 early.sort()
1539 other.sort()
1539 other.sort()
1540
1540
1541 def earlycommands(context):
1541 def earlycommands(context):
1542 for c, doc in early:
1542 for c, doc in early:
1543 yield {b'topic': c, b'summary': doc}
1543 yield {b'topic': c, b'summary': doc}
1544
1544
1545 def othercommands(context):
1545 def othercommands(context):
1546 for c, doc in other:
1546 for c, doc in other:
1547 yield {b'topic': c, b'summary': doc}
1547 yield {b'topic': c, b'summary': doc}
1548
1548
1549 return web.sendtemplate(
1549 return web.sendtemplate(
1550 b'helptopics',
1550 b'helptopics',
1551 topics=templateutil.mappinggenerator(topics),
1551 topics=templateutil.mappinggenerator(topics),
1552 earlycommands=templateutil.mappinggenerator(earlycommands),
1552 earlycommands=templateutil.mappinggenerator(earlycommands),
1553 othercommands=templateutil.mappinggenerator(othercommands),
1553 othercommands=templateutil.mappinggenerator(othercommands),
1554 title=b'Index',
1554 title=b'Index',
1555 )
1555 )
1556
1556
1557 # Render an index of sub-topics.
1557 # Render an index of sub-topics.
1558 if topicname in helpmod.subtopics:
1558 if topicname in helpmod.subtopics:
1559 topics = []
1559 topics = []
1560 for entries, summary, _doc in helpmod.subtopics[topicname]:
1560 for entries, summary, _doc in helpmod.subtopics[topicname]:
1561 topics.append(
1561 topics.append(
1562 {
1562 {
1563 b'topic': b'%s.%s' % (topicname, entries[0]),
1563 b'topic': b'%s.%s' % (topicname, entries[0]),
1564 b'basename': entries[0],
1564 b'basename': entries[0],
1565 b'summary': summary,
1565 b'summary': summary,
1566 }
1566 }
1567 )
1567 )
1568
1568
1569 return web.sendtemplate(
1569 return web.sendtemplate(
1570 b'helptopics',
1570 b'helptopics',
1571 topics=templateutil.mappinglist(topics),
1571 topics=templateutil.mappinglist(topics),
1572 title=topicname,
1572 title=topicname,
1573 subindex=True,
1573 subindex=True,
1574 )
1574 )
1575
1575
1576 u = webutil.wsgiui.load()
1576 u = webutil.wsgiui.load()
1577 u.verbose = True
1577 u.verbose = True
1578
1578
1579 # Render a page from a sub-topic.
1579 # Render a page from a sub-topic.
1580 if b'.' in topicname:
1580 if b'.' in topicname:
1581 # TODO implement support for rendering sections, like
1581 # TODO implement support for rendering sections, like
1582 # `hg help` works.
1582 # `hg help` works.
1583 topic, subtopic = topicname.split(b'.', 1)
1583 topic, subtopic = topicname.split(b'.', 1)
1584 if topic not in helpmod.subtopics:
1584 if topic not in helpmod.subtopics:
1585 raise ErrorResponse(HTTP_NOT_FOUND)
1585 raise ErrorResponse(HTTP_NOT_FOUND)
1586 else:
1586 else:
1587 topic = topicname
1587 topic = topicname
1588 subtopic = None
1588 subtopic = None
1589
1589
1590 try:
1590 try:
1591 doc = helpmod.help_(u, commands, topic, subtopic=subtopic)
1591 doc = helpmod.help_(u, commands, topic, subtopic=subtopic)
1592 except error.Abort:
1592 except error.Abort:
1593 raise ErrorResponse(HTTP_NOT_FOUND)
1593 raise ErrorResponse(HTTP_NOT_FOUND)
1594
1594
1595 return web.sendtemplate(b'help', topic=topicname, doc=doc)
1595 return web.sendtemplate(b'help', topic=topicname, doc=doc)
1596
1596
1597
1597
1598 # tell hggettext to extract docstrings from these functions:
1598 # tell hggettext to extract docstrings from these functions:
1599 i18nfunctions = commands.values()
1599 i18nfunctions = commands.values()
General Comments 0
You need to be logged in to leave comments. Login now