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