##// END OF EJS Templates
py3: make os.pardir a bytes
Yuya Nishihara -
r36665:052351e3 default
parent child Browse files
Show More
@@ -1,249 +1,249 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
15
16 from .. import (
16 from .. import (
17 encoding,
17 encoding,
18 pycompat,
18 pycompat,
19 util,
19 util,
20 )
20 )
21
21
22 httpserver = util.httpserver
22 httpserver = util.httpserver
23
23
24 HTTP_OK = 200
24 HTTP_OK = 200
25 HTTP_NOT_MODIFIED = 304
25 HTTP_NOT_MODIFIED = 304
26 HTTP_BAD_REQUEST = 400
26 HTTP_BAD_REQUEST = 400
27 HTTP_UNAUTHORIZED = 401
27 HTTP_UNAUTHORIZED = 401
28 HTTP_FORBIDDEN = 403
28 HTTP_FORBIDDEN = 403
29 HTTP_NOT_FOUND = 404
29 HTTP_NOT_FOUND = 404
30 HTTP_METHOD_NOT_ALLOWED = 405
30 HTTP_METHOD_NOT_ALLOWED = 405
31 HTTP_SERVER_ERROR = 500
31 HTTP_SERVER_ERROR = 500
32
32
33
33
34 def ismember(ui, username, userlist):
34 def ismember(ui, username, userlist):
35 """Check if username is a member of userlist.
35 """Check if username is a member of userlist.
36
36
37 If userlist has a single '*' member, all users are considered members.
37 If userlist has a single '*' member, all users are considered members.
38 Can be overridden by extensions to provide more complex authorization
38 Can be overridden by extensions to provide more complex authorization
39 schemes.
39 schemes.
40 """
40 """
41 return userlist == ['*'] or username in userlist
41 return userlist == ['*'] or username in userlist
42
42
43 def checkauthz(hgweb, req, op):
43 def checkauthz(hgweb, req, op):
44 '''Check permission for operation based on request data (including
44 '''Check permission for operation based on request data (including
45 authentication info). Return if op allowed, else raise an ErrorResponse
45 authentication info). Return if op allowed, else raise an ErrorResponse
46 exception.'''
46 exception.'''
47
47
48 user = req.env.get(r'REMOTE_USER')
48 user = req.env.get(r'REMOTE_USER')
49
49
50 deny_read = hgweb.configlist('web', 'deny_read')
50 deny_read = hgweb.configlist('web', 'deny_read')
51 if deny_read and (not user or ismember(hgweb.repo.ui, user, deny_read)):
51 if deny_read and (not user or ismember(hgweb.repo.ui, user, deny_read)):
52 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
52 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
53
53
54 allow_read = hgweb.configlist('web', 'allow_read')
54 allow_read = hgweb.configlist('web', 'allow_read')
55 if allow_read and (not ismember(hgweb.repo.ui, user, allow_read)):
55 if allow_read and (not ismember(hgweb.repo.ui, user, allow_read)):
56 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
56 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
57
57
58 if op == 'pull' and not hgweb.allowpull:
58 if op == 'pull' and not hgweb.allowpull:
59 raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized')
59 raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized')
60 elif op == 'pull' or op is None: # op is None for interface requests
60 elif op == 'pull' or op is None: # op is None for interface requests
61 return
61 return
62
62
63 # enforce that you can only push using POST requests
63 # enforce that you can only push using POST requests
64 if req.env[r'REQUEST_METHOD'] != r'POST':
64 if req.env[r'REQUEST_METHOD'] != r'POST':
65 msg = 'push requires POST request'
65 msg = 'push requires POST request'
66 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
66 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
67
67
68 # require ssl by default for pushing, auth info cannot be sniffed
68 # require ssl by default for pushing, auth info cannot be sniffed
69 # and replayed
69 # and replayed
70 scheme = req.env.get('wsgi.url_scheme')
70 scheme = req.env.get('wsgi.url_scheme')
71 if hgweb.configbool('web', 'push_ssl') and scheme != 'https':
71 if hgweb.configbool('web', 'push_ssl') and scheme != 'https':
72 raise ErrorResponse(HTTP_FORBIDDEN, 'ssl required')
72 raise ErrorResponse(HTTP_FORBIDDEN, 'ssl required')
73
73
74 deny = hgweb.configlist('web', 'deny_push')
74 deny = hgweb.configlist('web', 'deny_push')
75 if deny and (not user or ismember(hgweb.repo.ui, user, deny)):
75 if deny and (not user or ismember(hgweb.repo.ui, user, deny)):
76 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
76 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
77
77
78 allow = hgweb.configlist('web', 'allow-push')
78 allow = hgweb.configlist('web', 'allow-push')
79 if not (allow and ismember(hgweb.repo.ui, user, allow)):
79 if not (allow and ismember(hgweb.repo.ui, user, allow)):
80 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
80 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
81
81
82 # Hooks for hgweb permission checks; extensions can add hooks here.
82 # Hooks for hgweb permission checks; extensions can add hooks here.
83 # Each hook is invoked like this: hook(hgweb, request, operation),
83 # Each hook is invoked like this: hook(hgweb, request, operation),
84 # where operation is either read, pull or push. Hooks should either
84 # where operation is either read, pull or push. Hooks should either
85 # raise an ErrorResponse exception, or just return.
85 # raise an ErrorResponse exception, or just return.
86 #
86 #
87 # It is possible to do both authentication and authorization through
87 # It is possible to do both authentication and authorization through
88 # this.
88 # this.
89 permhooks = [checkauthz]
89 permhooks = [checkauthz]
90
90
91
91
92 class ErrorResponse(Exception):
92 class ErrorResponse(Exception):
93 def __init__(self, code, message=None, headers=None):
93 def __init__(self, code, message=None, headers=None):
94 if message is None:
94 if message is None:
95 message = _statusmessage(code)
95 message = _statusmessage(code)
96 Exception.__init__(self, pycompat.sysstr(message))
96 Exception.__init__(self, pycompat.sysstr(message))
97 self.code = code
97 self.code = code
98 if headers is None:
98 if headers is None:
99 headers = []
99 headers = []
100 self.headers = headers
100 self.headers = headers
101
101
102 class continuereader(object):
102 class continuereader(object):
103 def __init__(self, f, write):
103 def __init__(self, f, write):
104 self.f = f
104 self.f = f
105 self._write = write
105 self._write = write
106 self.continued = False
106 self.continued = False
107
107
108 def read(self, amt=-1):
108 def read(self, amt=-1):
109 if not self.continued:
109 if not self.continued:
110 self.continued = True
110 self.continued = True
111 self._write('HTTP/1.1 100 Continue\r\n\r\n')
111 self._write('HTTP/1.1 100 Continue\r\n\r\n')
112 return self.f.read(amt)
112 return self.f.read(amt)
113
113
114 def __getattr__(self, attr):
114 def __getattr__(self, attr):
115 if attr in ('close', 'readline', 'readlines', '__iter__'):
115 if attr in ('close', 'readline', 'readlines', '__iter__'):
116 return getattr(self.f, attr)
116 return getattr(self.f, attr)
117 raise AttributeError
117 raise AttributeError
118
118
119 def _statusmessage(code):
119 def _statusmessage(code):
120 responses = httpserver.basehttprequesthandler.responses
120 responses = httpserver.basehttprequesthandler.responses
121 return responses.get(code, ('Error', 'Unknown error'))[0]
121 return responses.get(code, ('Error', 'Unknown error'))[0]
122
122
123 def statusmessage(code, message=None):
123 def statusmessage(code, message=None):
124 return '%d %s' % (code, message or _statusmessage(code))
124 return '%d %s' % (code, message or _statusmessage(code))
125
125
126 def get_stat(spath, fn):
126 def get_stat(spath, fn):
127 """stat fn if it exists, spath otherwise"""
127 """stat fn if it exists, spath otherwise"""
128 cl_path = os.path.join(spath, fn)
128 cl_path = os.path.join(spath, fn)
129 if os.path.exists(cl_path):
129 if os.path.exists(cl_path):
130 return os.stat(cl_path)
130 return os.stat(cl_path)
131 else:
131 else:
132 return os.stat(spath)
132 return os.stat(spath)
133
133
134 def get_mtime(spath):
134 def get_mtime(spath):
135 return get_stat(spath, "00changelog.i").st_mtime
135 return get_stat(spath, "00changelog.i").st_mtime
136
136
137 def ispathsafe(path):
137 def ispathsafe(path):
138 """Determine if a path is safe to use for filesystem access."""
138 """Determine if a path is safe to use for filesystem access."""
139 parts = path.split('/')
139 parts = path.split('/')
140 for part in parts:
140 for part in parts:
141 if (part in ('', os.curdir, os.pardir) or
141 if (part in ('', os.curdir, pycompat.ospardir) or
142 pycompat.ossep in part or
142 pycompat.ossep in part or
143 pycompat.osaltsep is not None and pycompat.osaltsep in part):
143 pycompat.osaltsep is not None and pycompat.osaltsep in part):
144 return False
144 return False
145
145
146 return True
146 return True
147
147
148 def staticfile(directory, fname, req):
148 def staticfile(directory, fname, req):
149 """return a file inside directory with guessed Content-Type header
149 """return a file inside directory with guessed Content-Type header
150
150
151 fname always uses '/' as directory separator and isn't allowed to
151 fname always uses '/' as directory separator and isn't allowed to
152 contain unusual path components.
152 contain unusual path components.
153 Content-Type is guessed using the mimetypes module.
153 Content-Type is guessed using the mimetypes module.
154 Return an empty string if fname is illegal or file not found.
154 Return an empty string if fname is illegal or file not found.
155
155
156 """
156 """
157 if not ispathsafe(fname):
157 if not ispathsafe(fname):
158 return
158 return
159
159
160 fpath = os.path.join(*fname.split('/'))
160 fpath = os.path.join(*fname.split('/'))
161 if isinstance(directory, str):
161 if isinstance(directory, str):
162 directory = [directory]
162 directory = [directory]
163 for d in directory:
163 for d in directory:
164 path = os.path.join(d, fpath)
164 path = os.path.join(d, fpath)
165 if os.path.exists(path):
165 if os.path.exists(path):
166 break
166 break
167 try:
167 try:
168 os.stat(path)
168 os.stat(path)
169 ct = mimetypes.guess_type(pycompat.fsdecode(path))[0] or "text/plain"
169 ct = mimetypes.guess_type(pycompat.fsdecode(path))[0] or "text/plain"
170 with open(path, 'rb') as fh:
170 with open(path, 'rb') as fh:
171 data = fh.read()
171 data = fh.read()
172
172
173 req.respond(HTTP_OK, ct, body=data)
173 req.respond(HTTP_OK, ct, body=data)
174 except TypeError:
174 except TypeError:
175 raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal filename')
175 raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal filename')
176 except OSError as err:
176 except OSError as err:
177 if err.errno == errno.ENOENT:
177 if err.errno == errno.ENOENT:
178 raise ErrorResponse(HTTP_NOT_FOUND)
178 raise ErrorResponse(HTTP_NOT_FOUND)
179 else:
179 else:
180 raise ErrorResponse(HTTP_SERVER_ERROR,
180 raise ErrorResponse(HTTP_SERVER_ERROR,
181 encoding.strtolocal(err.strerror))
181 encoding.strtolocal(err.strerror))
182
182
183 def paritygen(stripecount, offset=0):
183 def paritygen(stripecount, offset=0):
184 """count parity of horizontal stripes for easier reading"""
184 """count parity of horizontal stripes for easier reading"""
185 if stripecount and offset:
185 if stripecount and offset:
186 # account for offset, e.g. due to building the list in reverse
186 # account for offset, e.g. due to building the list in reverse
187 count = (stripecount + offset) % stripecount
187 count = (stripecount + offset) % stripecount
188 parity = (stripecount + offset) // stripecount & 1
188 parity = (stripecount + offset) // stripecount & 1
189 else:
189 else:
190 count = 0
190 count = 0
191 parity = 0
191 parity = 0
192 while True:
192 while True:
193 yield parity
193 yield parity
194 count += 1
194 count += 1
195 if stripecount and count >= stripecount:
195 if stripecount and count >= stripecount:
196 parity = 1 - parity
196 parity = 1 - parity
197 count = 0
197 count = 0
198
198
199 def get_contact(config):
199 def get_contact(config):
200 """Return repo contact information or empty string.
200 """Return repo contact information or empty string.
201
201
202 web.contact is the primary source, but if that is not set, try
202 web.contact is the primary source, but if that is not set, try
203 ui.username or $EMAIL as a fallback to display something useful.
203 ui.username or $EMAIL as a fallback to display something useful.
204 """
204 """
205 return (config("web", "contact") or
205 return (config("web", "contact") or
206 config("ui", "username") or
206 config("ui", "username") or
207 encoding.environ.get("EMAIL") or "")
207 encoding.environ.get("EMAIL") or "")
208
208
209 def caching(web, req):
209 def caching(web, req):
210 tag = r'W/"%d"' % web.mtime
210 tag = r'W/"%d"' % web.mtime
211 if req.env.get('HTTP_IF_NONE_MATCH') == tag:
211 if req.env.get('HTTP_IF_NONE_MATCH') == tag:
212 raise ErrorResponse(HTTP_NOT_MODIFIED)
212 raise ErrorResponse(HTTP_NOT_MODIFIED)
213 req.headers.append(('ETag', tag))
213 req.headers.append(('ETag', tag))
214
214
215 def cspvalues(ui):
215 def cspvalues(ui):
216 """Obtain the Content-Security-Policy header and nonce value.
216 """Obtain the Content-Security-Policy header and nonce value.
217
217
218 Returns a 2-tuple of the CSP header value and the nonce value.
218 Returns a 2-tuple of the CSP header value and the nonce value.
219
219
220 First value is ``None`` if CSP isn't enabled. Second value is ``None``
220 First value is ``None`` if CSP isn't enabled. Second value is ``None``
221 if CSP isn't enabled or if the CSP header doesn't need a nonce.
221 if CSP isn't enabled or if the CSP header doesn't need a nonce.
222 """
222 """
223 # Without demandimport, "import uuid" could have an immediate side-effect
223 # Without demandimport, "import uuid" could have an immediate side-effect
224 # running "ldconfig" on Linux trying to find libuuid.
224 # running "ldconfig" on Linux trying to find libuuid.
225 # With Python <= 2.7.12, that "ldconfig" is run via a shell and the shell
225 # With Python <= 2.7.12, that "ldconfig" is run via a shell and the shell
226 # may pollute the terminal with:
226 # may pollute the terminal with:
227 #
227 #
228 # shell-init: error retrieving current directory: getcwd: cannot access
228 # shell-init: error retrieving current directory: getcwd: cannot access
229 # parent directories: No such file or directory
229 # parent directories: No such file or directory
230 #
230 #
231 # Python >= 2.7.13 has fixed it by running "ldconfig" directly without a
231 # Python >= 2.7.13 has fixed it by running "ldconfig" directly without a
232 # shell (hg changeset a09ae70f3489).
232 # shell (hg changeset a09ae70f3489).
233 #
233 #
234 # Moved "import uuid" from here so it's executed after we know we have
234 # Moved "import uuid" from here so it's executed after we know we have
235 # a sane cwd (i.e. after dispatch.py cwd check).
235 # a sane cwd (i.e. after dispatch.py cwd check).
236 #
236 #
237 # We can move it back once we no longer need Python <= 2.7.12 support.
237 # We can move it back once we no longer need Python <= 2.7.12 support.
238 import uuid
238 import uuid
239
239
240 # Don't allow untrusted CSP setting since it be disable protections
240 # Don't allow untrusted CSP setting since it be disable protections
241 # from a trusted/global source.
241 # from a trusted/global source.
242 csp = ui.config('web', 'csp', untrusted=False)
242 csp = ui.config('web', 'csp', untrusted=False)
243 nonce = None
243 nonce = None
244
244
245 if csp and '%nonce%' in csp:
245 if csp and '%nonce%' in csp:
246 nonce = base64.urlsafe_b64encode(uuid.uuid4().bytes).rstrip('=')
246 nonce = base64.urlsafe_b64encode(uuid.uuid4().bytes).rstrip('=')
247 csp = csp.replace('%nonce%', nonce)
247 csp = csp.replace('%nonce%', nonce)
248
248
249 return csp, nonce
249 return csp, nonce
@@ -1,263 +1,263 b''
1 from __future__ import absolute_import
1 from __future__ import absolute_import
2
2
3 import errno
3 import errno
4 import os
4 import os
5 import posixpath
5 import posixpath
6 import stat
6 import stat
7
7
8 from .i18n import _
8 from .i18n import _
9 from . import (
9 from . import (
10 encoding,
10 encoding,
11 error,
11 error,
12 pycompat,
12 pycompat,
13 util,
13 util,
14 )
14 )
15
15
16 def _lowerclean(s):
16 def _lowerclean(s):
17 return encoding.hfsignoreclean(s.lower())
17 return encoding.hfsignoreclean(s.lower())
18
18
19 class pathauditor(object):
19 class pathauditor(object):
20 '''ensure that a filesystem path contains no banned components.
20 '''ensure that a filesystem path contains no banned components.
21 the following properties of a path are checked:
21 the following properties of a path are checked:
22
22
23 - ends with a directory separator
23 - ends with a directory separator
24 - under top-level .hg
24 - under top-level .hg
25 - starts at the root of a windows drive
25 - starts at the root of a windows drive
26 - contains ".."
26 - contains ".."
27
27
28 More check are also done about the file system states:
28 More check are also done about the file system states:
29 - traverses a symlink (e.g. a/symlink_here/b)
29 - traverses a symlink (e.g. a/symlink_here/b)
30 - inside a nested repository (a callback can be used to approve
30 - inside a nested repository (a callback can be used to approve
31 some nested repositories, e.g., subrepositories)
31 some nested repositories, e.g., subrepositories)
32
32
33 The file system checks are only done when 'realfs' is set to True (the
33 The file system checks are only done when 'realfs' is set to True (the
34 default). They should be disable then we are auditing path for operation on
34 default). They should be disable then we are auditing path for operation on
35 stored history.
35 stored history.
36
36
37 If 'cached' is set to True, audited paths and sub-directories are cached.
37 If 'cached' is set to True, audited paths and sub-directories are cached.
38 Be careful to not keep the cache of unmanaged directories for long because
38 Be careful to not keep the cache of unmanaged directories for long because
39 audited paths may be replaced with symlinks.
39 audited paths may be replaced with symlinks.
40 '''
40 '''
41
41
42 def __init__(self, root, callback=None, realfs=True, cached=False):
42 def __init__(self, root, callback=None, realfs=True, cached=False):
43 self.audited = set()
43 self.audited = set()
44 self.auditeddir = set()
44 self.auditeddir = set()
45 self.root = root
45 self.root = root
46 self._realfs = realfs
46 self._realfs = realfs
47 self._cached = cached
47 self._cached = cached
48 self.callback = callback
48 self.callback = callback
49 if os.path.lexists(root) and not util.fscasesensitive(root):
49 if os.path.lexists(root) and not util.fscasesensitive(root):
50 self.normcase = util.normcase
50 self.normcase = util.normcase
51 else:
51 else:
52 self.normcase = lambda x: x
52 self.normcase = lambda x: x
53
53
54 def __call__(self, path, mode=None):
54 def __call__(self, path, mode=None):
55 '''Check the relative path.
55 '''Check the relative path.
56 path may contain a pattern (e.g. foodir/**.txt)'''
56 path may contain a pattern (e.g. foodir/**.txt)'''
57
57
58 path = util.localpath(path)
58 path = util.localpath(path)
59 normpath = self.normcase(path)
59 normpath = self.normcase(path)
60 if normpath in self.audited:
60 if normpath in self.audited:
61 return
61 return
62 # AIX ignores "/" at end of path, others raise EISDIR.
62 # AIX ignores "/" at end of path, others raise EISDIR.
63 if util.endswithsep(path):
63 if util.endswithsep(path):
64 raise error.Abort(_("path ends in directory separator: %s") % path)
64 raise error.Abort(_("path ends in directory separator: %s") % path)
65 parts = util.splitpath(path)
65 parts = util.splitpath(path)
66 if (os.path.splitdrive(path)[0]
66 if (os.path.splitdrive(path)[0]
67 or _lowerclean(parts[0]) in ('.hg', '.hg.', '')
67 or _lowerclean(parts[0]) in ('.hg', '.hg.', '')
68 or os.pardir in parts):
68 or pycompat.ospardir in parts):
69 raise error.Abort(_("path contains illegal component: %s") % path)
69 raise error.Abort(_("path contains illegal component: %s") % path)
70 # Windows shortname aliases
70 # Windows shortname aliases
71 for p in parts:
71 for p in parts:
72 if "~" in p:
72 if "~" in p:
73 first, last = p.split("~", 1)
73 first, last = p.split("~", 1)
74 if last.isdigit() and first.upper() in ["HG", "HG8B6C"]:
74 if last.isdigit() and first.upper() in ["HG", "HG8B6C"]:
75 raise error.Abort(_("path contains illegal component: %s")
75 raise error.Abort(_("path contains illegal component: %s")
76 % path)
76 % path)
77 if '.hg' in _lowerclean(path):
77 if '.hg' in _lowerclean(path):
78 lparts = [_lowerclean(p.lower()) for p in parts]
78 lparts = [_lowerclean(p.lower()) for p in parts]
79 for p in '.hg', '.hg.':
79 for p in '.hg', '.hg.':
80 if p in lparts[1:]:
80 if p in lparts[1:]:
81 pos = lparts.index(p)
81 pos = lparts.index(p)
82 base = os.path.join(*parts[:pos])
82 base = os.path.join(*parts[:pos])
83 raise error.Abort(_("path '%s' is inside nested repo %r")
83 raise error.Abort(_("path '%s' is inside nested repo %r")
84 % (path, base))
84 % (path, base))
85
85
86 normparts = util.splitpath(normpath)
86 normparts = util.splitpath(normpath)
87 assert len(parts) == len(normparts)
87 assert len(parts) == len(normparts)
88
88
89 parts.pop()
89 parts.pop()
90 normparts.pop()
90 normparts.pop()
91 prefixes = []
91 prefixes = []
92 # It's important that we check the path parts starting from the root.
92 # It's important that we check the path parts starting from the root.
93 # This means we won't accidentally traverse a symlink into some other
93 # This means we won't accidentally traverse a symlink into some other
94 # filesystem (which is potentially expensive to access).
94 # filesystem (which is potentially expensive to access).
95 for i in range(len(parts)):
95 for i in range(len(parts)):
96 prefix = pycompat.ossep.join(parts[:i + 1])
96 prefix = pycompat.ossep.join(parts[:i + 1])
97 normprefix = pycompat.ossep.join(normparts[:i + 1])
97 normprefix = pycompat.ossep.join(normparts[:i + 1])
98 if normprefix in self.auditeddir:
98 if normprefix in self.auditeddir:
99 continue
99 continue
100 if self._realfs:
100 if self._realfs:
101 self._checkfs(prefix, path)
101 self._checkfs(prefix, path)
102 prefixes.append(normprefix)
102 prefixes.append(normprefix)
103
103
104 if self._cached:
104 if self._cached:
105 self.audited.add(normpath)
105 self.audited.add(normpath)
106 # only add prefixes to the cache after checking everything: we don't
106 # only add prefixes to the cache after checking everything: we don't
107 # want to add "foo/bar/baz" before checking if there's a "foo/.hg"
107 # want to add "foo/bar/baz" before checking if there's a "foo/.hg"
108 self.auditeddir.update(prefixes)
108 self.auditeddir.update(prefixes)
109
109
110 def _checkfs(self, prefix, path):
110 def _checkfs(self, prefix, path):
111 """raise exception if a file system backed check fails"""
111 """raise exception if a file system backed check fails"""
112 curpath = os.path.join(self.root, prefix)
112 curpath = os.path.join(self.root, prefix)
113 try:
113 try:
114 st = os.lstat(curpath)
114 st = os.lstat(curpath)
115 except OSError as err:
115 except OSError as err:
116 # EINVAL can be raised as invalid path syntax under win32.
116 # EINVAL can be raised as invalid path syntax under win32.
117 # They must be ignored for patterns can be checked too.
117 # They must be ignored for patterns can be checked too.
118 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
118 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
119 raise
119 raise
120 else:
120 else:
121 if stat.S_ISLNK(st.st_mode):
121 if stat.S_ISLNK(st.st_mode):
122 msg = _('path %r traverses symbolic link %r') % (path, prefix)
122 msg = _('path %r traverses symbolic link %r') % (path, prefix)
123 raise error.Abort(msg)
123 raise error.Abort(msg)
124 elif (stat.S_ISDIR(st.st_mode) and
124 elif (stat.S_ISDIR(st.st_mode) and
125 os.path.isdir(os.path.join(curpath, '.hg'))):
125 os.path.isdir(os.path.join(curpath, '.hg'))):
126 if not self.callback or not self.callback(curpath):
126 if not self.callback or not self.callback(curpath):
127 msg = _("path '%s' is inside nested repo %r")
127 msg = _("path '%s' is inside nested repo %r")
128 raise error.Abort(msg % (path, prefix))
128 raise error.Abort(msg % (path, prefix))
129
129
130 def check(self, path):
130 def check(self, path):
131 try:
131 try:
132 self(path)
132 self(path)
133 return True
133 return True
134 except (OSError, error.Abort):
134 except (OSError, error.Abort):
135 return False
135 return False
136
136
137 def canonpath(root, cwd, myname, auditor=None):
137 def canonpath(root, cwd, myname, auditor=None):
138 '''return the canonical path of myname, given cwd and root
138 '''return the canonical path of myname, given cwd and root
139
139
140 >>> def check(root, cwd, myname):
140 >>> def check(root, cwd, myname):
141 ... a = pathauditor(root, realfs=False)
141 ... a = pathauditor(root, realfs=False)
142 ... try:
142 ... try:
143 ... return canonpath(root, cwd, myname, a)
143 ... return canonpath(root, cwd, myname, a)
144 ... except error.Abort:
144 ... except error.Abort:
145 ... return 'aborted'
145 ... return 'aborted'
146 >>> def unixonly(root, cwd, myname, expected='aborted'):
146 >>> def unixonly(root, cwd, myname, expected='aborted'):
147 ... if pycompat.iswindows:
147 ... if pycompat.iswindows:
148 ... return expected
148 ... return expected
149 ... return check(root, cwd, myname)
149 ... return check(root, cwd, myname)
150 >>> def winonly(root, cwd, myname, expected='aborted'):
150 >>> def winonly(root, cwd, myname, expected='aborted'):
151 ... if not pycompat.iswindows:
151 ... if not pycompat.iswindows:
152 ... return expected
152 ... return expected
153 ... return check(root, cwd, myname)
153 ... return check(root, cwd, myname)
154 >>> winonly(b'd:\\\\repo', b'c:\\\\dir', b'filename')
154 >>> winonly(b'd:\\\\repo', b'c:\\\\dir', b'filename')
155 'aborted'
155 'aborted'
156 >>> winonly(b'c:\\\\repo', b'c:\\\\dir', b'filename')
156 >>> winonly(b'c:\\\\repo', b'c:\\\\dir', b'filename')
157 'aborted'
157 'aborted'
158 >>> winonly(b'c:\\\\repo', b'c:\\\\', b'filename')
158 >>> winonly(b'c:\\\\repo', b'c:\\\\', b'filename')
159 'aborted'
159 'aborted'
160 >>> winonly(b'c:\\\\repo', b'c:\\\\', b'repo\\\\filename',
160 >>> winonly(b'c:\\\\repo', b'c:\\\\', b'repo\\\\filename',
161 ... b'filename')
161 ... b'filename')
162 'filename'
162 'filename'
163 >>> winonly(b'c:\\\\repo', b'c:\\\\repo', b'filename', b'filename')
163 >>> winonly(b'c:\\\\repo', b'c:\\\\repo', b'filename', b'filename')
164 'filename'
164 'filename'
165 >>> winonly(b'c:\\\\repo', b'c:\\\\repo\\\\subdir', b'filename',
165 >>> winonly(b'c:\\\\repo', b'c:\\\\repo\\\\subdir', b'filename',
166 ... b'subdir/filename')
166 ... b'subdir/filename')
167 'subdir/filename'
167 'subdir/filename'
168 >>> unixonly(b'/repo', b'/dir', b'filename')
168 >>> unixonly(b'/repo', b'/dir', b'filename')
169 'aborted'
169 'aborted'
170 >>> unixonly(b'/repo', b'/', b'filename')
170 >>> unixonly(b'/repo', b'/', b'filename')
171 'aborted'
171 'aborted'
172 >>> unixonly(b'/repo', b'/', b'repo/filename', b'filename')
172 >>> unixonly(b'/repo', b'/', b'repo/filename', b'filename')
173 'filename'
173 'filename'
174 >>> unixonly(b'/repo', b'/repo', b'filename', b'filename')
174 >>> unixonly(b'/repo', b'/repo', b'filename', b'filename')
175 'filename'
175 'filename'
176 >>> unixonly(b'/repo', b'/repo/subdir', b'filename', b'subdir/filename')
176 >>> unixonly(b'/repo', b'/repo/subdir', b'filename', b'subdir/filename')
177 'subdir/filename'
177 'subdir/filename'
178 '''
178 '''
179 if util.endswithsep(root):
179 if util.endswithsep(root):
180 rootsep = root
180 rootsep = root
181 else:
181 else:
182 rootsep = root + pycompat.ossep
182 rootsep = root + pycompat.ossep
183 name = myname
183 name = myname
184 if not os.path.isabs(name):
184 if not os.path.isabs(name):
185 name = os.path.join(root, cwd, name)
185 name = os.path.join(root, cwd, name)
186 name = os.path.normpath(name)
186 name = os.path.normpath(name)
187 if auditor is None:
187 if auditor is None:
188 auditor = pathauditor(root)
188 auditor = pathauditor(root)
189 if name != rootsep and name.startswith(rootsep):
189 if name != rootsep and name.startswith(rootsep):
190 name = name[len(rootsep):]
190 name = name[len(rootsep):]
191 auditor(name)
191 auditor(name)
192 return util.pconvert(name)
192 return util.pconvert(name)
193 elif name == root:
193 elif name == root:
194 return ''
194 return ''
195 else:
195 else:
196 # Determine whether `name' is in the hierarchy at or beneath `root',
196 # Determine whether `name' is in the hierarchy at or beneath `root',
197 # by iterating name=dirname(name) until that causes no change (can't
197 # by iterating name=dirname(name) until that causes no change (can't
198 # check name == '/', because that doesn't work on windows). The list
198 # check name == '/', because that doesn't work on windows). The list
199 # `rel' holds the reversed list of components making up the relative
199 # `rel' holds the reversed list of components making up the relative
200 # file name we want.
200 # file name we want.
201 rel = []
201 rel = []
202 while True:
202 while True:
203 try:
203 try:
204 s = util.samefile(name, root)
204 s = util.samefile(name, root)
205 except OSError:
205 except OSError:
206 s = False
206 s = False
207 if s:
207 if s:
208 if not rel:
208 if not rel:
209 # name was actually the same as root (maybe a symlink)
209 # name was actually the same as root (maybe a symlink)
210 return ''
210 return ''
211 rel.reverse()
211 rel.reverse()
212 name = os.path.join(*rel)
212 name = os.path.join(*rel)
213 auditor(name)
213 auditor(name)
214 return util.pconvert(name)
214 return util.pconvert(name)
215 dirname, basename = util.split(name)
215 dirname, basename = util.split(name)
216 rel.append(basename)
216 rel.append(basename)
217 if dirname == name:
217 if dirname == name:
218 break
218 break
219 name = dirname
219 name = dirname
220
220
221 # A common mistake is to use -R, but specify a file relative to the repo
221 # A common mistake is to use -R, but specify a file relative to the repo
222 # instead of cwd. Detect that case, and provide a hint to the user.
222 # instead of cwd. Detect that case, and provide a hint to the user.
223 hint = None
223 hint = None
224 try:
224 try:
225 if cwd != root:
225 if cwd != root:
226 canonpath(root, root, myname, auditor)
226 canonpath(root, root, myname, auditor)
227 relpath = util.pathto(root, cwd, '')
227 relpath = util.pathto(root, cwd, '')
228 if relpath[-1] == pycompat.ossep:
228 if relpath[-1] == pycompat.ossep:
229 relpath = relpath[:-1]
229 relpath = relpath[:-1]
230 hint = (_("consider using '--cwd %s'") % relpath)
230 hint = (_("consider using '--cwd %s'") % relpath)
231 except error.Abort:
231 except error.Abort:
232 pass
232 pass
233
233
234 raise error.Abort(_("%s not under root '%s'") % (myname, root),
234 raise error.Abort(_("%s not under root '%s'") % (myname, root),
235 hint=hint)
235 hint=hint)
236
236
237 def normasprefix(path):
237 def normasprefix(path):
238 '''normalize the specified path as path prefix
238 '''normalize the specified path as path prefix
239
239
240 Returned value can be used safely for "p.startswith(prefix)",
240 Returned value can be used safely for "p.startswith(prefix)",
241 "p[len(prefix):]", and so on.
241 "p[len(prefix):]", and so on.
242
242
243 For efficiency, this expects "path" argument to be already
243 For efficiency, this expects "path" argument to be already
244 normalized by "os.path.normpath", "os.path.realpath", and so on.
244 normalized by "os.path.normpath", "os.path.realpath", and so on.
245
245
246 See also issue3033 for detail about need of this function.
246 See also issue3033 for detail about need of this function.
247
247
248 >>> normasprefix(b'/foo/bar').replace(pycompat.ossep, b'/')
248 >>> normasprefix(b'/foo/bar').replace(pycompat.ossep, b'/')
249 '/foo/bar/'
249 '/foo/bar/'
250 >>> normasprefix(b'/').replace(pycompat.ossep, b'/')
250 >>> normasprefix(b'/').replace(pycompat.ossep, b'/')
251 '/'
251 '/'
252 '''
252 '''
253 d, p = os.path.splitdrive(path)
253 d, p = os.path.splitdrive(path)
254 if len(p) != len(pycompat.ossep):
254 if len(p) != len(pycompat.ossep):
255 return path + pycompat.ossep
255 return path + pycompat.ossep
256 else:
256 else:
257 return path
257 return path
258
258
259 # forward two methods from posixpath that do what we need, but we'd
259 # forward two methods from posixpath that do what we need, but we'd
260 # rather not let our internals know that we're thinking in posix terms
260 # rather not let our internals know that we're thinking in posix terms
261 # - instead we'll let them be oblivious.
261 # - instead we'll let them be oblivious.
262 join = posixpath.join
262 join = posixpath.join
263 dirname = posixpath.dirname
263 dirname = posixpath.dirname
@@ -1,357 +1,359 b''
1 # pycompat.py - portability shim for python 3
1 # pycompat.py - portability shim for python 3
2 #
2 #
3 # This software may be used and distributed according to the terms of the
3 # This software may be used and distributed according to the terms of the
4 # GNU General Public License version 2 or any later version.
4 # GNU General Public License version 2 or any later version.
5
5
6 """Mercurial portability shim for python 3.
6 """Mercurial portability shim for python 3.
7
7
8 This contains aliases to hide python version-specific details from the core.
8 This contains aliases to hide python version-specific details from the core.
9 """
9 """
10
10
11 from __future__ import absolute_import
11 from __future__ import absolute_import
12
12
13 import getopt
13 import getopt
14 import inspect
14 import inspect
15 import os
15 import os
16 import shlex
16 import shlex
17 import sys
17 import sys
18
18
19 ispy3 = (sys.version_info[0] >= 3)
19 ispy3 = (sys.version_info[0] >= 3)
20 ispypy = (r'__pypy__' in sys.builtin_module_names)
20 ispypy = (r'__pypy__' in sys.builtin_module_names)
21
21
22 if not ispy3:
22 if not ispy3:
23 import cookielib
23 import cookielib
24 import cPickle as pickle
24 import cPickle as pickle
25 import httplib
25 import httplib
26 import Queue as _queue
26 import Queue as _queue
27 import SocketServer as socketserver
27 import SocketServer as socketserver
28 import xmlrpclib
28 import xmlrpclib
29 else:
29 else:
30 import http.cookiejar as cookielib
30 import http.cookiejar as cookielib
31 import http.client as httplib
31 import http.client as httplib
32 import pickle
32 import pickle
33 import queue as _queue
33 import queue as _queue
34 import socketserver
34 import socketserver
35 import xmlrpc.client as xmlrpclib
35 import xmlrpc.client as xmlrpclib
36
36
37 empty = _queue.Empty
37 empty = _queue.Empty
38 queue = _queue.Queue
38 queue = _queue.Queue
39
39
40 def identity(a):
40 def identity(a):
41 return a
41 return a
42
42
43 if ispy3:
43 if ispy3:
44 import builtins
44 import builtins
45 import functools
45 import functools
46 import io
46 import io
47 import struct
47 import struct
48
48
49 fsencode = os.fsencode
49 fsencode = os.fsencode
50 fsdecode = os.fsdecode
50 fsdecode = os.fsdecode
51 oslinesep = os.linesep.encode('ascii')
51 oslinesep = os.linesep.encode('ascii')
52 osname = os.name.encode('ascii')
52 osname = os.name.encode('ascii')
53 ospathsep = os.pathsep.encode('ascii')
53 ospathsep = os.pathsep.encode('ascii')
54 ospardir = os.pardir.encode('ascii')
54 ossep = os.sep.encode('ascii')
55 ossep = os.sep.encode('ascii')
55 osaltsep = os.altsep
56 osaltsep = os.altsep
56 if osaltsep:
57 if osaltsep:
57 osaltsep = osaltsep.encode('ascii')
58 osaltsep = osaltsep.encode('ascii')
58 # os.getcwd() on Python 3 returns string, but it has os.getcwdb() which
59 # os.getcwd() on Python 3 returns string, but it has os.getcwdb() which
59 # returns bytes.
60 # returns bytes.
60 getcwd = os.getcwdb
61 getcwd = os.getcwdb
61 sysplatform = sys.platform.encode('ascii')
62 sysplatform = sys.platform.encode('ascii')
62 sysexecutable = sys.executable
63 sysexecutable = sys.executable
63 if sysexecutable:
64 if sysexecutable:
64 sysexecutable = os.fsencode(sysexecutable)
65 sysexecutable = os.fsencode(sysexecutable)
65 stringio = io.BytesIO
66 stringio = io.BytesIO
66 maplist = lambda *args: list(map(*args))
67 maplist = lambda *args: list(map(*args))
67 ziplist = lambda *args: list(zip(*args))
68 ziplist = lambda *args: list(zip(*args))
68 rawinput = input
69 rawinput = input
69 getargspec = inspect.getfullargspec
70 getargspec = inspect.getfullargspec
70
71
71 # TODO: .buffer might not exist if std streams were replaced; we'll need
72 # TODO: .buffer might not exist if std streams were replaced; we'll need
72 # a silly wrapper to make a bytes stream backed by a unicode one.
73 # a silly wrapper to make a bytes stream backed by a unicode one.
73 stdin = sys.stdin.buffer
74 stdin = sys.stdin.buffer
74 stdout = sys.stdout.buffer
75 stdout = sys.stdout.buffer
75 stderr = sys.stderr.buffer
76 stderr = sys.stderr.buffer
76
77
77 # Since Python 3 converts argv to wchar_t type by Py_DecodeLocale() on Unix,
78 # Since Python 3 converts argv to wchar_t type by Py_DecodeLocale() on Unix,
78 # we can use os.fsencode() to get back bytes argv.
79 # we can use os.fsencode() to get back bytes argv.
79 #
80 #
80 # https://hg.python.org/cpython/file/v3.5.1/Programs/python.c#l55
81 # https://hg.python.org/cpython/file/v3.5.1/Programs/python.c#l55
81 #
82 #
82 # TODO: On Windows, the native argv is wchar_t, so we'll need a different
83 # TODO: On Windows, the native argv is wchar_t, so we'll need a different
83 # workaround to simulate the Python 2 (i.e. ANSI Win32 API) behavior.
84 # workaround to simulate the Python 2 (i.e. ANSI Win32 API) behavior.
84 if getattr(sys, 'argv', None) is not None:
85 if getattr(sys, 'argv', None) is not None:
85 sysargv = list(map(os.fsencode, sys.argv))
86 sysargv = list(map(os.fsencode, sys.argv))
86
87
87 bytechr = struct.Struct('>B').pack
88 bytechr = struct.Struct('>B').pack
88 byterepr = b'%r'.__mod__
89 byterepr = b'%r'.__mod__
89
90
90 class bytestr(bytes):
91 class bytestr(bytes):
91 """A bytes which mostly acts as a Python 2 str
92 """A bytes which mostly acts as a Python 2 str
92
93
93 >>> bytestr(), bytestr(bytearray(b'foo')), bytestr(u'ascii'), bytestr(1)
94 >>> bytestr(), bytestr(bytearray(b'foo')), bytestr(u'ascii'), bytestr(1)
94 ('', 'foo', 'ascii', '1')
95 ('', 'foo', 'ascii', '1')
95 >>> s = bytestr(b'foo')
96 >>> s = bytestr(b'foo')
96 >>> assert s is bytestr(s)
97 >>> assert s is bytestr(s)
97
98
98 __bytes__() should be called if provided:
99 __bytes__() should be called if provided:
99
100
100 >>> class bytesable(object):
101 >>> class bytesable(object):
101 ... def __bytes__(self):
102 ... def __bytes__(self):
102 ... return b'bytes'
103 ... return b'bytes'
103 >>> bytestr(bytesable())
104 >>> bytestr(bytesable())
104 'bytes'
105 'bytes'
105
106
106 There's no implicit conversion from non-ascii str as its encoding is
107 There's no implicit conversion from non-ascii str as its encoding is
107 unknown:
108 unknown:
108
109
109 >>> bytestr(chr(0x80)) # doctest: +ELLIPSIS
110 >>> bytestr(chr(0x80)) # doctest: +ELLIPSIS
110 Traceback (most recent call last):
111 Traceback (most recent call last):
111 ...
112 ...
112 UnicodeEncodeError: ...
113 UnicodeEncodeError: ...
113
114
114 Comparison between bytestr and bytes should work:
115 Comparison between bytestr and bytes should work:
115
116
116 >>> assert bytestr(b'foo') == b'foo'
117 >>> assert bytestr(b'foo') == b'foo'
117 >>> assert b'foo' == bytestr(b'foo')
118 >>> assert b'foo' == bytestr(b'foo')
118 >>> assert b'f' in bytestr(b'foo')
119 >>> assert b'f' in bytestr(b'foo')
119 >>> assert bytestr(b'f') in b'foo'
120 >>> assert bytestr(b'f') in b'foo'
120
121
121 Sliced elements should be bytes, not integer:
122 Sliced elements should be bytes, not integer:
122
123
123 >>> s[1], s[:2]
124 >>> s[1], s[:2]
124 (b'o', b'fo')
125 (b'o', b'fo')
125 >>> list(s), list(reversed(s))
126 >>> list(s), list(reversed(s))
126 ([b'f', b'o', b'o'], [b'o', b'o', b'f'])
127 ([b'f', b'o', b'o'], [b'o', b'o', b'f'])
127
128
128 As bytestr type isn't propagated across operations, you need to cast
129 As bytestr type isn't propagated across operations, you need to cast
129 bytes to bytestr explicitly:
130 bytes to bytestr explicitly:
130
131
131 >>> s = bytestr(b'foo').upper()
132 >>> s = bytestr(b'foo').upper()
132 >>> t = bytestr(s)
133 >>> t = bytestr(s)
133 >>> s[0], t[0]
134 >>> s[0], t[0]
134 (70, b'F')
135 (70, b'F')
135
136
136 Be careful to not pass a bytestr object to a function which expects
137 Be careful to not pass a bytestr object to a function which expects
137 bytearray-like behavior.
138 bytearray-like behavior.
138
139
139 >>> t = bytes(t) # cast to bytes
140 >>> t = bytes(t) # cast to bytes
140 >>> assert type(t) is bytes
141 >>> assert type(t) is bytes
141 """
142 """
142
143
143 def __new__(cls, s=b''):
144 def __new__(cls, s=b''):
144 if isinstance(s, bytestr):
145 if isinstance(s, bytestr):
145 return s
146 return s
146 if (not isinstance(s, (bytes, bytearray))
147 if (not isinstance(s, (bytes, bytearray))
147 and not hasattr(s, u'__bytes__')): # hasattr-py3-only
148 and not hasattr(s, u'__bytes__')): # hasattr-py3-only
148 s = str(s).encode(u'ascii')
149 s = str(s).encode(u'ascii')
149 return bytes.__new__(cls, s)
150 return bytes.__new__(cls, s)
150
151
151 def __getitem__(self, key):
152 def __getitem__(self, key):
152 s = bytes.__getitem__(self, key)
153 s = bytes.__getitem__(self, key)
153 if not isinstance(s, bytes):
154 if not isinstance(s, bytes):
154 s = bytechr(s)
155 s = bytechr(s)
155 return s
156 return s
156
157
157 def __iter__(self):
158 def __iter__(self):
158 return iterbytestr(bytes.__iter__(self))
159 return iterbytestr(bytes.__iter__(self))
159
160
160 def __repr__(self):
161 def __repr__(self):
161 return bytes.__repr__(self)[1:] # drop b''
162 return bytes.__repr__(self)[1:] # drop b''
162
163
163 def iterbytestr(s):
164 def iterbytestr(s):
164 """Iterate bytes as if it were a str object of Python 2"""
165 """Iterate bytes as if it were a str object of Python 2"""
165 return map(bytechr, s)
166 return map(bytechr, s)
166
167
167 def maybebytestr(s):
168 def maybebytestr(s):
168 """Promote bytes to bytestr"""
169 """Promote bytes to bytestr"""
169 if isinstance(s, bytes):
170 if isinstance(s, bytes):
170 return bytestr(s)
171 return bytestr(s)
171 return s
172 return s
172
173
173 def sysbytes(s):
174 def sysbytes(s):
174 """Convert an internal str (e.g. keyword, __doc__) back to bytes
175 """Convert an internal str (e.g. keyword, __doc__) back to bytes
175
176
176 This never raises UnicodeEncodeError, but only ASCII characters
177 This never raises UnicodeEncodeError, but only ASCII characters
177 can be round-trip by sysstr(sysbytes(s)).
178 can be round-trip by sysstr(sysbytes(s)).
178 """
179 """
179 return s.encode(u'utf-8')
180 return s.encode(u'utf-8')
180
181
181 def sysstr(s):
182 def sysstr(s):
182 """Return a keyword str to be passed to Python functions such as
183 """Return a keyword str to be passed to Python functions such as
183 getattr() and str.encode()
184 getattr() and str.encode()
184
185
185 This never raises UnicodeDecodeError. Non-ascii characters are
186 This never raises UnicodeDecodeError. Non-ascii characters are
186 considered invalid and mapped to arbitrary but unique code points
187 considered invalid and mapped to arbitrary but unique code points
187 such that 'sysstr(a) != sysstr(b)' for all 'a != b'.
188 such that 'sysstr(a) != sysstr(b)' for all 'a != b'.
188 """
189 """
189 if isinstance(s, builtins.str):
190 if isinstance(s, builtins.str):
190 return s
191 return s
191 return s.decode(u'latin-1')
192 return s.decode(u'latin-1')
192
193
193 def strurl(url):
194 def strurl(url):
194 """Converts a bytes url back to str"""
195 """Converts a bytes url back to str"""
195 if isinstance(url, bytes):
196 if isinstance(url, bytes):
196 return url.decode(u'ascii')
197 return url.decode(u'ascii')
197 return url
198 return url
198
199
199 def bytesurl(url):
200 def bytesurl(url):
200 """Converts a str url to bytes by encoding in ascii"""
201 """Converts a str url to bytes by encoding in ascii"""
201 if isinstance(url, str):
202 if isinstance(url, str):
202 return url.encode(u'ascii')
203 return url.encode(u'ascii')
203 return url
204 return url
204
205
205 def raisewithtb(exc, tb):
206 def raisewithtb(exc, tb):
206 """Raise exception with the given traceback"""
207 """Raise exception with the given traceback"""
207 raise exc.with_traceback(tb)
208 raise exc.with_traceback(tb)
208
209
209 def getdoc(obj):
210 def getdoc(obj):
210 """Get docstring as bytes; may be None so gettext() won't confuse it
211 """Get docstring as bytes; may be None so gettext() won't confuse it
211 with _('')"""
212 with _('')"""
212 doc = getattr(obj, u'__doc__', None)
213 doc = getattr(obj, u'__doc__', None)
213 if doc is None:
214 if doc is None:
214 return doc
215 return doc
215 return sysbytes(doc)
216 return sysbytes(doc)
216
217
217 def _wrapattrfunc(f):
218 def _wrapattrfunc(f):
218 @functools.wraps(f)
219 @functools.wraps(f)
219 def w(object, name, *args):
220 def w(object, name, *args):
220 return f(object, sysstr(name), *args)
221 return f(object, sysstr(name), *args)
221 return w
222 return w
222
223
223 # these wrappers are automagically imported by hgloader
224 # these wrappers are automagically imported by hgloader
224 delattr = _wrapattrfunc(builtins.delattr)
225 delattr = _wrapattrfunc(builtins.delattr)
225 getattr = _wrapattrfunc(builtins.getattr)
226 getattr = _wrapattrfunc(builtins.getattr)
226 hasattr = _wrapattrfunc(builtins.hasattr)
227 hasattr = _wrapattrfunc(builtins.hasattr)
227 setattr = _wrapattrfunc(builtins.setattr)
228 setattr = _wrapattrfunc(builtins.setattr)
228 xrange = builtins.range
229 xrange = builtins.range
229 unicode = str
230 unicode = str
230
231
231 def open(name, mode='r', buffering=-1, encoding=None):
232 def open(name, mode='r', buffering=-1, encoding=None):
232 return builtins.open(name, sysstr(mode), buffering, encoding)
233 return builtins.open(name, sysstr(mode), buffering, encoding)
233
234
234 def _getoptbwrapper(orig, args, shortlist, namelist):
235 def _getoptbwrapper(orig, args, shortlist, namelist):
235 """
236 """
236 Takes bytes arguments, converts them to unicode, pass them to
237 Takes bytes arguments, converts them to unicode, pass them to
237 getopt.getopt(), convert the returned values back to bytes and then
238 getopt.getopt(), convert the returned values back to bytes and then
238 return them for Python 3 compatibility as getopt.getopt() don't accepts
239 return them for Python 3 compatibility as getopt.getopt() don't accepts
239 bytes on Python 3.
240 bytes on Python 3.
240 """
241 """
241 args = [a.decode('latin-1') for a in args]
242 args = [a.decode('latin-1') for a in args]
242 shortlist = shortlist.decode('latin-1')
243 shortlist = shortlist.decode('latin-1')
243 namelist = [a.decode('latin-1') for a in namelist]
244 namelist = [a.decode('latin-1') for a in namelist]
244 opts, args = orig(args, shortlist, namelist)
245 opts, args = orig(args, shortlist, namelist)
245 opts = [(a[0].encode('latin-1'), a[1].encode('latin-1'))
246 opts = [(a[0].encode('latin-1'), a[1].encode('latin-1'))
246 for a in opts]
247 for a in opts]
247 args = [a.encode('latin-1') for a in args]
248 args = [a.encode('latin-1') for a in args]
248 return opts, args
249 return opts, args
249
250
250 def strkwargs(dic):
251 def strkwargs(dic):
251 """
252 """
252 Converts the keys of a python dictonary to str i.e. unicodes so that
253 Converts the keys of a python dictonary to str i.e. unicodes so that
253 they can be passed as keyword arguments as dictonaries with bytes keys
254 they can be passed as keyword arguments as dictonaries with bytes keys
254 can't be passed as keyword arguments to functions on Python 3.
255 can't be passed as keyword arguments to functions on Python 3.
255 """
256 """
256 dic = dict((k.decode('latin-1'), v) for k, v in dic.iteritems())
257 dic = dict((k.decode('latin-1'), v) for k, v in dic.iteritems())
257 return dic
258 return dic
258
259
259 def byteskwargs(dic):
260 def byteskwargs(dic):
260 """
261 """
261 Converts keys of python dictonaries to bytes as they were converted to
262 Converts keys of python dictonaries to bytes as they were converted to
262 str to pass that dictonary as a keyword argument on Python 3.
263 str to pass that dictonary as a keyword argument on Python 3.
263 """
264 """
264 dic = dict((k.encode('latin-1'), v) for k, v in dic.iteritems())
265 dic = dict((k.encode('latin-1'), v) for k, v in dic.iteritems())
265 return dic
266 return dic
266
267
267 # TODO: handle shlex.shlex().
268 # TODO: handle shlex.shlex().
268 def shlexsplit(s, comments=False, posix=True):
269 def shlexsplit(s, comments=False, posix=True):
269 """
270 """
270 Takes bytes argument, convert it to str i.e. unicodes, pass that into
271 Takes bytes argument, convert it to str i.e. unicodes, pass that into
271 shlex.split(), convert the returned value to bytes and return that for
272 shlex.split(), convert the returned value to bytes and return that for
272 Python 3 compatibility as shelx.split() don't accept bytes on Python 3.
273 Python 3 compatibility as shelx.split() don't accept bytes on Python 3.
273 """
274 """
274 ret = shlex.split(s.decode('latin-1'), comments, posix)
275 ret = shlex.split(s.decode('latin-1'), comments, posix)
275 return [a.encode('latin-1') for a in ret]
276 return [a.encode('latin-1') for a in ret]
276
277
277 def emailparser(*args, **kwargs):
278 def emailparser(*args, **kwargs):
278 import email.parser
279 import email.parser
279 return email.parser.BytesParser(*args, **kwargs)
280 return email.parser.BytesParser(*args, **kwargs)
280
281
281 else:
282 else:
282 import cStringIO
283 import cStringIO
283
284
284 bytechr = chr
285 bytechr = chr
285 byterepr = repr
286 byterepr = repr
286 bytestr = str
287 bytestr = str
287 iterbytestr = iter
288 iterbytestr = iter
288 maybebytestr = identity
289 maybebytestr = identity
289 sysbytes = identity
290 sysbytes = identity
290 sysstr = identity
291 sysstr = identity
291 strurl = identity
292 strurl = identity
292 bytesurl = identity
293 bytesurl = identity
293
294
294 # this can't be parsed on Python 3
295 # this can't be parsed on Python 3
295 exec('def raisewithtb(exc, tb):\n'
296 exec('def raisewithtb(exc, tb):\n'
296 ' raise exc, None, tb\n')
297 ' raise exc, None, tb\n')
297
298
298 def fsencode(filename):
299 def fsencode(filename):
299 """
300 """
300 Partial backport from os.py in Python 3, which only accepts bytes.
301 Partial backport from os.py in Python 3, which only accepts bytes.
301 In Python 2, our paths should only ever be bytes, a unicode path
302 In Python 2, our paths should only ever be bytes, a unicode path
302 indicates a bug.
303 indicates a bug.
303 """
304 """
304 if isinstance(filename, str):
305 if isinstance(filename, str):
305 return filename
306 return filename
306 else:
307 else:
307 raise TypeError(
308 raise TypeError(
308 "expect str, not %s" % type(filename).__name__)
309 "expect str, not %s" % type(filename).__name__)
309
310
310 # In Python 2, fsdecode() has a very chance to receive bytes. So it's
311 # In Python 2, fsdecode() has a very chance to receive bytes. So it's
311 # better not to touch Python 2 part as it's already working fine.
312 # better not to touch Python 2 part as it's already working fine.
312 fsdecode = identity
313 fsdecode = identity
313
314
314 def getdoc(obj):
315 def getdoc(obj):
315 return getattr(obj, '__doc__', None)
316 return getattr(obj, '__doc__', None)
316
317
317 def _getoptbwrapper(orig, args, shortlist, namelist):
318 def _getoptbwrapper(orig, args, shortlist, namelist):
318 return orig(args, shortlist, namelist)
319 return orig(args, shortlist, namelist)
319
320
320 strkwargs = identity
321 strkwargs = identity
321 byteskwargs = identity
322 byteskwargs = identity
322
323
323 oslinesep = os.linesep
324 oslinesep = os.linesep
324 osname = os.name
325 osname = os.name
325 ospathsep = os.pathsep
326 ospathsep = os.pathsep
327 ospardir = os.pardir
326 ossep = os.sep
328 ossep = os.sep
327 osaltsep = os.altsep
329 osaltsep = os.altsep
328 stdin = sys.stdin
330 stdin = sys.stdin
329 stdout = sys.stdout
331 stdout = sys.stdout
330 stderr = sys.stderr
332 stderr = sys.stderr
331 if getattr(sys, 'argv', None) is not None:
333 if getattr(sys, 'argv', None) is not None:
332 sysargv = sys.argv
334 sysargv = sys.argv
333 sysplatform = sys.platform
335 sysplatform = sys.platform
334 getcwd = os.getcwd
336 getcwd = os.getcwd
335 sysexecutable = sys.executable
337 sysexecutable = sys.executable
336 shlexsplit = shlex.split
338 shlexsplit = shlex.split
337 stringio = cStringIO.StringIO
339 stringio = cStringIO.StringIO
338 maplist = map
340 maplist = map
339 ziplist = zip
341 ziplist = zip
340 rawinput = raw_input
342 rawinput = raw_input
341 getargspec = inspect.getargspec
343 getargspec = inspect.getargspec
342
344
343 def emailparser(*args, **kwargs):
345 def emailparser(*args, **kwargs):
344 import email.parser
346 import email.parser
345 return email.parser.Parser(*args, **kwargs)
347 return email.parser.Parser(*args, **kwargs)
346
348
347 isjython = sysplatform.startswith('java')
349 isjython = sysplatform.startswith('java')
348
350
349 isdarwin = sysplatform == 'darwin'
351 isdarwin = sysplatform == 'darwin'
350 isposix = osname == 'posix'
352 isposix = osname == 'posix'
351 iswindows = osname == 'nt'
353 iswindows = osname == 'nt'
352
354
353 def getoptb(args, shortlist, namelist):
355 def getoptb(args, shortlist, namelist):
354 return _getoptbwrapper(getopt.getopt, args, shortlist, namelist)
356 return _getoptbwrapper(getopt.getopt, args, shortlist, namelist)
355
357
356 def gnugetoptb(args, shortlist, namelist):
358 def gnugetoptb(args, shortlist, namelist):
357 return _getoptbwrapper(getopt.gnu_getopt, args, shortlist, namelist)
359 return _getoptbwrapper(getopt.gnu_getopt, args, shortlist, namelist)
@@ -1,1625 +1,1625 b''
1 # templater.py - template expansion for output
1 # templater.py - template expansion for output
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 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, print_function
8 from __future__ import absolute_import, print_function
9
9
10 import os
10 import os
11 import re
11 import re
12 import types
12 import types
13
13
14 from .i18n import _
14 from .i18n import _
15 from . import (
15 from . import (
16 color,
16 color,
17 config,
17 config,
18 encoding,
18 encoding,
19 error,
19 error,
20 minirst,
20 minirst,
21 obsutil,
21 obsutil,
22 parser,
22 parser,
23 pycompat,
23 pycompat,
24 registrar,
24 registrar,
25 revset as revsetmod,
25 revset as revsetmod,
26 revsetlang,
26 revsetlang,
27 scmutil,
27 scmutil,
28 templatefilters,
28 templatefilters,
29 templatekw,
29 templatekw,
30 util,
30 util,
31 )
31 )
32 from .utils import dateutil
32 from .utils import dateutil
33
33
34 class ResourceUnavailable(error.Abort):
34 class ResourceUnavailable(error.Abort):
35 pass
35 pass
36
36
37 class TemplateNotFound(error.Abort):
37 class TemplateNotFound(error.Abort):
38 pass
38 pass
39
39
40 # template parsing
40 # template parsing
41
41
42 elements = {
42 elements = {
43 # token-type: binding-strength, primary, prefix, infix, suffix
43 # token-type: binding-strength, primary, prefix, infix, suffix
44 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
44 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
45 ".": (18, None, None, (".", 18), None),
45 ".": (18, None, None, (".", 18), None),
46 "%": (15, None, None, ("%", 15), None),
46 "%": (15, None, None, ("%", 15), None),
47 "|": (15, None, None, ("|", 15), None),
47 "|": (15, None, None, ("|", 15), None),
48 "*": (5, None, None, ("*", 5), None),
48 "*": (5, None, None, ("*", 5), None),
49 "/": (5, None, None, ("/", 5), None),
49 "/": (5, None, None, ("/", 5), None),
50 "+": (4, None, None, ("+", 4), None),
50 "+": (4, None, None, ("+", 4), None),
51 "-": (4, None, ("negate", 19), ("-", 4), None),
51 "-": (4, None, ("negate", 19), ("-", 4), None),
52 "=": (3, None, None, ("keyvalue", 3), None),
52 "=": (3, None, None, ("keyvalue", 3), None),
53 ",": (2, None, None, ("list", 2), None),
53 ",": (2, None, None, ("list", 2), None),
54 ")": (0, None, None, None, None),
54 ")": (0, None, None, None, None),
55 "integer": (0, "integer", None, None, None),
55 "integer": (0, "integer", None, None, None),
56 "symbol": (0, "symbol", None, None, None),
56 "symbol": (0, "symbol", None, None, None),
57 "string": (0, "string", None, None, None),
57 "string": (0, "string", None, None, None),
58 "template": (0, "template", None, None, None),
58 "template": (0, "template", None, None, None),
59 "end": (0, None, None, None, None),
59 "end": (0, None, None, None, None),
60 }
60 }
61
61
62 def tokenize(program, start, end, term=None):
62 def tokenize(program, start, end, term=None):
63 """Parse a template expression into a stream of tokens, which must end
63 """Parse a template expression into a stream of tokens, which must end
64 with term if specified"""
64 with term if specified"""
65 pos = start
65 pos = start
66 program = pycompat.bytestr(program)
66 program = pycompat.bytestr(program)
67 while pos < end:
67 while pos < end:
68 c = program[pos]
68 c = program[pos]
69 if c.isspace(): # skip inter-token whitespace
69 if c.isspace(): # skip inter-token whitespace
70 pass
70 pass
71 elif c in "(=,).%|+-*/": # handle simple operators
71 elif c in "(=,).%|+-*/": # handle simple operators
72 yield (c, None, pos)
72 yield (c, None, pos)
73 elif c in '"\'': # handle quoted templates
73 elif c in '"\'': # handle quoted templates
74 s = pos + 1
74 s = pos + 1
75 data, pos = _parsetemplate(program, s, end, c)
75 data, pos = _parsetemplate(program, s, end, c)
76 yield ('template', data, s)
76 yield ('template', data, s)
77 pos -= 1
77 pos -= 1
78 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
78 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
79 # handle quoted strings
79 # handle quoted strings
80 c = program[pos + 1]
80 c = program[pos + 1]
81 s = pos = pos + 2
81 s = pos = pos + 2
82 while pos < end: # find closing quote
82 while pos < end: # find closing quote
83 d = program[pos]
83 d = program[pos]
84 if d == '\\': # skip over escaped characters
84 if d == '\\': # skip over escaped characters
85 pos += 2
85 pos += 2
86 continue
86 continue
87 if d == c:
87 if d == c:
88 yield ('string', program[s:pos], s)
88 yield ('string', program[s:pos], s)
89 break
89 break
90 pos += 1
90 pos += 1
91 else:
91 else:
92 raise error.ParseError(_("unterminated string"), s)
92 raise error.ParseError(_("unterminated string"), s)
93 elif c.isdigit():
93 elif c.isdigit():
94 s = pos
94 s = pos
95 while pos < end:
95 while pos < end:
96 d = program[pos]
96 d = program[pos]
97 if not d.isdigit():
97 if not d.isdigit():
98 break
98 break
99 pos += 1
99 pos += 1
100 yield ('integer', program[s:pos], s)
100 yield ('integer', program[s:pos], s)
101 pos -= 1
101 pos -= 1
102 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
102 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
103 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
103 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
104 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
104 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
105 # where some of nested templates were preprocessed as strings and
105 # where some of nested templates were preprocessed as strings and
106 # then compiled. therefore, \"...\" was allowed. (issue4733)
106 # then compiled. therefore, \"...\" was allowed. (issue4733)
107 #
107 #
108 # processing flow of _evalifliteral() at 5ab28a2e9962:
108 # processing flow of _evalifliteral() at 5ab28a2e9962:
109 # outer template string -> stringify() -> compiletemplate()
109 # outer template string -> stringify() -> compiletemplate()
110 # ------------------------ ------------ ------------------
110 # ------------------------ ------------ ------------------
111 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
111 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
112 # ~~~~~~~~
112 # ~~~~~~~~
113 # escaped quoted string
113 # escaped quoted string
114 if c == 'r':
114 if c == 'r':
115 pos += 1
115 pos += 1
116 token = 'string'
116 token = 'string'
117 else:
117 else:
118 token = 'template'
118 token = 'template'
119 quote = program[pos:pos + 2]
119 quote = program[pos:pos + 2]
120 s = pos = pos + 2
120 s = pos = pos + 2
121 while pos < end: # find closing escaped quote
121 while pos < end: # find closing escaped quote
122 if program.startswith('\\\\\\', pos, end):
122 if program.startswith('\\\\\\', pos, end):
123 pos += 4 # skip over double escaped characters
123 pos += 4 # skip over double escaped characters
124 continue
124 continue
125 if program.startswith(quote, pos, end):
125 if program.startswith(quote, pos, end):
126 # interpret as if it were a part of an outer string
126 # interpret as if it were a part of an outer string
127 data = parser.unescapestr(program[s:pos])
127 data = parser.unescapestr(program[s:pos])
128 if token == 'template':
128 if token == 'template':
129 data = _parsetemplate(data, 0, len(data))[0]
129 data = _parsetemplate(data, 0, len(data))[0]
130 yield (token, data, s)
130 yield (token, data, s)
131 pos += 1
131 pos += 1
132 break
132 break
133 pos += 1
133 pos += 1
134 else:
134 else:
135 raise error.ParseError(_("unterminated string"), s)
135 raise error.ParseError(_("unterminated string"), s)
136 elif c.isalnum() or c in '_':
136 elif c.isalnum() or c in '_':
137 s = pos
137 s = pos
138 pos += 1
138 pos += 1
139 while pos < end: # find end of symbol
139 while pos < end: # find end of symbol
140 d = program[pos]
140 d = program[pos]
141 if not (d.isalnum() or d == "_"):
141 if not (d.isalnum() or d == "_"):
142 break
142 break
143 pos += 1
143 pos += 1
144 sym = program[s:pos]
144 sym = program[s:pos]
145 yield ('symbol', sym, s)
145 yield ('symbol', sym, s)
146 pos -= 1
146 pos -= 1
147 elif c == term:
147 elif c == term:
148 yield ('end', None, pos + 1)
148 yield ('end', None, pos + 1)
149 return
149 return
150 else:
150 else:
151 raise error.ParseError(_("syntax error"), pos)
151 raise error.ParseError(_("syntax error"), pos)
152 pos += 1
152 pos += 1
153 if term:
153 if term:
154 raise error.ParseError(_("unterminated template expansion"), start)
154 raise error.ParseError(_("unterminated template expansion"), start)
155 yield ('end', None, pos)
155 yield ('end', None, pos)
156
156
157 def _parsetemplate(tmpl, start, stop, quote=''):
157 def _parsetemplate(tmpl, start, stop, quote=''):
158 r"""
158 r"""
159 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
159 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
160 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
160 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
161 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
161 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
162 ([('string', 'foo'), ('symbol', 'bar')], 9)
162 ([('string', 'foo'), ('symbol', 'bar')], 9)
163 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
163 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
164 ([('string', 'foo')], 4)
164 ([('string', 'foo')], 4)
165 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
165 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
166 ([('string', 'foo"'), ('string', 'bar')], 9)
166 ([('string', 'foo"'), ('string', 'bar')], 9)
167 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
167 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
168 ([('string', 'foo\\')], 6)
168 ([('string', 'foo\\')], 6)
169 """
169 """
170 parsed = []
170 parsed = []
171 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
171 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
172 if typ == 'string':
172 if typ == 'string':
173 parsed.append((typ, val))
173 parsed.append((typ, val))
174 elif typ == 'template':
174 elif typ == 'template':
175 parsed.append(val)
175 parsed.append(val)
176 elif typ == 'end':
176 elif typ == 'end':
177 return parsed, pos
177 return parsed, pos
178 else:
178 else:
179 raise error.ProgrammingError('unexpected type: %s' % typ)
179 raise error.ProgrammingError('unexpected type: %s' % typ)
180 raise error.ProgrammingError('unterminated scanning of template')
180 raise error.ProgrammingError('unterminated scanning of template')
181
181
182 def scantemplate(tmpl, raw=False):
182 def scantemplate(tmpl, raw=False):
183 r"""Scan (type, start, end) positions of outermost elements in template
183 r"""Scan (type, start, end) positions of outermost elements in template
184
184
185 If raw=True, a backslash is not taken as an escape character just like
185 If raw=True, a backslash is not taken as an escape character just like
186 r'' string in Python. Note that this is different from r'' literal in
186 r'' string in Python. Note that this is different from r'' literal in
187 template in that no template fragment can appear in r'', e.g. r'{foo}'
187 template in that no template fragment can appear in r'', e.g. r'{foo}'
188 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
188 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
189 'foo'.
189 'foo'.
190
190
191 >>> list(scantemplate(b'foo{bar}"baz'))
191 >>> list(scantemplate(b'foo{bar}"baz'))
192 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
192 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
193 >>> list(scantemplate(b'outer{"inner"}outer'))
193 >>> list(scantemplate(b'outer{"inner"}outer'))
194 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
194 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
195 >>> list(scantemplate(b'foo\\{escaped}'))
195 >>> list(scantemplate(b'foo\\{escaped}'))
196 [('string', 0, 5), ('string', 5, 13)]
196 [('string', 0, 5), ('string', 5, 13)]
197 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
197 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
198 [('string', 0, 4), ('template', 4, 13)]
198 [('string', 0, 4), ('template', 4, 13)]
199 """
199 """
200 last = None
200 last = None
201 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
201 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
202 if last:
202 if last:
203 yield last + (pos,)
203 yield last + (pos,)
204 if typ == 'end':
204 if typ == 'end':
205 return
205 return
206 else:
206 else:
207 last = (typ, pos)
207 last = (typ, pos)
208 raise error.ProgrammingError('unterminated scanning of template')
208 raise error.ProgrammingError('unterminated scanning of template')
209
209
210 def _scantemplate(tmpl, start, stop, quote='', raw=False):
210 def _scantemplate(tmpl, start, stop, quote='', raw=False):
211 """Parse template string into chunks of strings and template expressions"""
211 """Parse template string into chunks of strings and template expressions"""
212 sepchars = '{' + quote
212 sepchars = '{' + quote
213 unescape = [parser.unescapestr, pycompat.identity][raw]
213 unescape = [parser.unescapestr, pycompat.identity][raw]
214 pos = start
214 pos = start
215 p = parser.parser(elements)
215 p = parser.parser(elements)
216 while pos < stop:
216 while pos < stop:
217 n = min((tmpl.find(c, pos, stop) for c in sepchars),
217 n = min((tmpl.find(c, pos, stop) for c in sepchars),
218 key=lambda n: (n < 0, n))
218 key=lambda n: (n < 0, n))
219 if n < 0:
219 if n < 0:
220 yield ('string', unescape(tmpl[pos:stop]), pos)
220 yield ('string', unescape(tmpl[pos:stop]), pos)
221 pos = stop
221 pos = stop
222 break
222 break
223 c = tmpl[n:n + 1]
223 c = tmpl[n:n + 1]
224 bs = 0 # count leading backslashes
224 bs = 0 # count leading backslashes
225 if not raw:
225 if not raw:
226 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
226 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
227 if bs % 2 == 1:
227 if bs % 2 == 1:
228 # escaped (e.g. '\{', '\\\{', but not '\\{')
228 # escaped (e.g. '\{', '\\\{', but not '\\{')
229 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
229 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
230 pos = n + 1
230 pos = n + 1
231 continue
231 continue
232 if n > pos:
232 if n > pos:
233 yield ('string', unescape(tmpl[pos:n]), pos)
233 yield ('string', unescape(tmpl[pos:n]), pos)
234 if c == quote:
234 if c == quote:
235 yield ('end', None, n + 1)
235 yield ('end', None, n + 1)
236 return
236 return
237
237
238 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
238 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
239 if not tmpl.endswith('}', n + 1, pos):
239 if not tmpl.endswith('}', n + 1, pos):
240 raise error.ParseError(_("invalid token"), pos)
240 raise error.ParseError(_("invalid token"), pos)
241 yield ('template', parseres, n)
241 yield ('template', parseres, n)
242
242
243 if quote:
243 if quote:
244 raise error.ParseError(_("unterminated string"), start)
244 raise error.ParseError(_("unterminated string"), start)
245 yield ('end', None, pos)
245 yield ('end', None, pos)
246
246
247 def _unnesttemplatelist(tree):
247 def _unnesttemplatelist(tree):
248 """Expand list of templates to node tuple
248 """Expand list of templates to node tuple
249
249
250 >>> def f(tree):
250 >>> def f(tree):
251 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
251 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
252 >>> f((b'template', []))
252 >>> f((b'template', []))
253 (string '')
253 (string '')
254 >>> f((b'template', [(b'string', b'foo')]))
254 >>> f((b'template', [(b'string', b'foo')]))
255 (string 'foo')
255 (string 'foo')
256 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
256 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
257 (template
257 (template
258 (string 'foo')
258 (string 'foo')
259 (symbol 'rev'))
259 (symbol 'rev'))
260 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
260 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
261 (template
261 (template
262 (symbol 'rev'))
262 (symbol 'rev'))
263 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
263 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
264 (string 'foo')
264 (string 'foo')
265 """
265 """
266 if not isinstance(tree, tuple):
266 if not isinstance(tree, tuple):
267 return tree
267 return tree
268 op = tree[0]
268 op = tree[0]
269 if op != 'template':
269 if op != 'template':
270 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
270 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
271
271
272 assert len(tree) == 2
272 assert len(tree) == 2
273 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
273 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
274 if not xs:
274 if not xs:
275 return ('string', '') # empty template ""
275 return ('string', '') # empty template ""
276 elif len(xs) == 1 and xs[0][0] == 'string':
276 elif len(xs) == 1 and xs[0][0] == 'string':
277 return xs[0] # fast path for string with no template fragment "x"
277 return xs[0] # fast path for string with no template fragment "x"
278 else:
278 else:
279 return (op,) + xs
279 return (op,) + xs
280
280
281 def parse(tmpl):
281 def parse(tmpl):
282 """Parse template string into tree"""
282 """Parse template string into tree"""
283 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
283 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
284 assert pos == len(tmpl), 'unquoted template should be consumed'
284 assert pos == len(tmpl), 'unquoted template should be consumed'
285 return _unnesttemplatelist(('template', parsed))
285 return _unnesttemplatelist(('template', parsed))
286
286
287 def _parseexpr(expr):
287 def _parseexpr(expr):
288 """Parse a template expression into tree
288 """Parse a template expression into tree
289
289
290 >>> _parseexpr(b'"foo"')
290 >>> _parseexpr(b'"foo"')
291 ('string', 'foo')
291 ('string', 'foo')
292 >>> _parseexpr(b'foo(bar)')
292 >>> _parseexpr(b'foo(bar)')
293 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
293 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
294 >>> _parseexpr(b'foo(')
294 >>> _parseexpr(b'foo(')
295 Traceback (most recent call last):
295 Traceback (most recent call last):
296 ...
296 ...
297 ParseError: ('not a prefix: end', 4)
297 ParseError: ('not a prefix: end', 4)
298 >>> _parseexpr(b'"foo" "bar"')
298 >>> _parseexpr(b'"foo" "bar"')
299 Traceback (most recent call last):
299 Traceback (most recent call last):
300 ...
300 ...
301 ParseError: ('invalid token', 7)
301 ParseError: ('invalid token', 7)
302 """
302 """
303 p = parser.parser(elements)
303 p = parser.parser(elements)
304 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
304 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
305 if pos != len(expr):
305 if pos != len(expr):
306 raise error.ParseError(_('invalid token'), pos)
306 raise error.ParseError(_('invalid token'), pos)
307 return _unnesttemplatelist(tree)
307 return _unnesttemplatelist(tree)
308
308
309 def prettyformat(tree):
309 def prettyformat(tree):
310 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
310 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
311
311
312 def compileexp(exp, context, curmethods):
312 def compileexp(exp, context, curmethods):
313 """Compile parsed template tree to (func, data) pair"""
313 """Compile parsed template tree to (func, data) pair"""
314 if not exp:
314 if not exp:
315 raise error.ParseError(_("missing argument"))
315 raise error.ParseError(_("missing argument"))
316 t = exp[0]
316 t = exp[0]
317 if t in curmethods:
317 if t in curmethods:
318 return curmethods[t](exp, context)
318 return curmethods[t](exp, context)
319 raise error.ParseError(_("unknown method '%s'") % t)
319 raise error.ParseError(_("unknown method '%s'") % t)
320
320
321 # template evaluation
321 # template evaluation
322
322
323 def getsymbol(exp):
323 def getsymbol(exp):
324 if exp[0] == 'symbol':
324 if exp[0] == 'symbol':
325 return exp[1]
325 return exp[1]
326 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
326 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
327
327
328 def getlist(x):
328 def getlist(x):
329 if not x:
329 if not x:
330 return []
330 return []
331 if x[0] == 'list':
331 if x[0] == 'list':
332 return getlist(x[1]) + [x[2]]
332 return getlist(x[1]) + [x[2]]
333 return [x]
333 return [x]
334
334
335 def gettemplate(exp, context):
335 def gettemplate(exp, context):
336 """Compile given template tree or load named template from map file;
336 """Compile given template tree or load named template from map file;
337 returns (func, data) pair"""
337 returns (func, data) pair"""
338 if exp[0] in ('template', 'string'):
338 if exp[0] in ('template', 'string'):
339 return compileexp(exp, context, methods)
339 return compileexp(exp, context, methods)
340 if exp[0] == 'symbol':
340 if exp[0] == 'symbol':
341 # unlike runsymbol(), here 'symbol' is always taken as template name
341 # unlike runsymbol(), here 'symbol' is always taken as template name
342 # even if it exists in mapping. this allows us to override mapping
342 # even if it exists in mapping. this allows us to override mapping
343 # by web templates, e.g. 'changelogtag' is redefined in map file.
343 # by web templates, e.g. 'changelogtag' is redefined in map file.
344 return context._load(exp[1])
344 return context._load(exp[1])
345 raise error.ParseError(_("expected template specifier"))
345 raise error.ParseError(_("expected template specifier"))
346
346
347 def findsymbolicname(arg):
347 def findsymbolicname(arg):
348 """Find symbolic name for the given compiled expression; returns None
348 """Find symbolic name for the given compiled expression; returns None
349 if nothing found reliably"""
349 if nothing found reliably"""
350 while True:
350 while True:
351 func, data = arg
351 func, data = arg
352 if func is runsymbol:
352 if func is runsymbol:
353 return data
353 return data
354 elif func is runfilter:
354 elif func is runfilter:
355 arg = data[0]
355 arg = data[0]
356 else:
356 else:
357 return None
357 return None
358
358
359 def evalrawexp(context, mapping, arg):
359 def evalrawexp(context, mapping, arg):
360 """Evaluate given argument as a bare template object which may require
360 """Evaluate given argument as a bare template object which may require
361 further processing (such as folding generator of strings)"""
361 further processing (such as folding generator of strings)"""
362 func, data = arg
362 func, data = arg
363 return func(context, mapping, data)
363 return func(context, mapping, data)
364
364
365 def evalfuncarg(context, mapping, arg):
365 def evalfuncarg(context, mapping, arg):
366 """Evaluate given argument as value type"""
366 """Evaluate given argument as value type"""
367 thing = evalrawexp(context, mapping, arg)
367 thing = evalrawexp(context, mapping, arg)
368 thing = templatekw.unwrapvalue(thing)
368 thing = templatekw.unwrapvalue(thing)
369 # evalrawexp() may return string, generator of strings or arbitrary object
369 # evalrawexp() may return string, generator of strings or arbitrary object
370 # such as date tuple, but filter does not want generator.
370 # such as date tuple, but filter does not want generator.
371 if isinstance(thing, types.GeneratorType):
371 if isinstance(thing, types.GeneratorType):
372 thing = stringify(thing)
372 thing = stringify(thing)
373 return thing
373 return thing
374
374
375 def evalboolean(context, mapping, arg):
375 def evalboolean(context, mapping, arg):
376 """Evaluate given argument as boolean, but also takes boolean literals"""
376 """Evaluate given argument as boolean, but also takes boolean literals"""
377 func, data = arg
377 func, data = arg
378 if func is runsymbol:
378 if func is runsymbol:
379 thing = func(context, mapping, data, default=None)
379 thing = func(context, mapping, data, default=None)
380 if thing is None:
380 if thing is None:
381 # not a template keyword, takes as a boolean literal
381 # not a template keyword, takes as a boolean literal
382 thing = util.parsebool(data)
382 thing = util.parsebool(data)
383 else:
383 else:
384 thing = func(context, mapping, data)
384 thing = func(context, mapping, data)
385 thing = templatekw.unwrapvalue(thing)
385 thing = templatekw.unwrapvalue(thing)
386 if isinstance(thing, bool):
386 if isinstance(thing, bool):
387 return thing
387 return thing
388 # other objects are evaluated as strings, which means 0 is True, but
388 # other objects are evaluated as strings, which means 0 is True, but
389 # empty dict/list should be False as they are expected to be ''
389 # empty dict/list should be False as they are expected to be ''
390 return bool(stringify(thing))
390 return bool(stringify(thing))
391
391
392 def evalinteger(context, mapping, arg, err=None):
392 def evalinteger(context, mapping, arg, err=None):
393 v = evalfuncarg(context, mapping, arg)
393 v = evalfuncarg(context, mapping, arg)
394 try:
394 try:
395 return int(v)
395 return int(v)
396 except (TypeError, ValueError):
396 except (TypeError, ValueError):
397 raise error.ParseError(err or _('not an integer'))
397 raise error.ParseError(err or _('not an integer'))
398
398
399 def evalstring(context, mapping, arg):
399 def evalstring(context, mapping, arg):
400 return stringify(evalrawexp(context, mapping, arg))
400 return stringify(evalrawexp(context, mapping, arg))
401
401
402 def evalstringliteral(context, mapping, arg):
402 def evalstringliteral(context, mapping, arg):
403 """Evaluate given argument as string template, but returns symbol name
403 """Evaluate given argument as string template, but returns symbol name
404 if it is unknown"""
404 if it is unknown"""
405 func, data = arg
405 func, data = arg
406 if func is runsymbol:
406 if func is runsymbol:
407 thing = func(context, mapping, data, default=data)
407 thing = func(context, mapping, data, default=data)
408 else:
408 else:
409 thing = func(context, mapping, data)
409 thing = func(context, mapping, data)
410 return stringify(thing)
410 return stringify(thing)
411
411
412 _evalfuncbytype = {
412 _evalfuncbytype = {
413 bool: evalboolean,
413 bool: evalboolean,
414 bytes: evalstring,
414 bytes: evalstring,
415 int: evalinteger,
415 int: evalinteger,
416 }
416 }
417
417
418 def evalastype(context, mapping, arg, typ):
418 def evalastype(context, mapping, arg, typ):
419 """Evaluate given argument and coerce its type"""
419 """Evaluate given argument and coerce its type"""
420 try:
420 try:
421 f = _evalfuncbytype[typ]
421 f = _evalfuncbytype[typ]
422 except KeyError:
422 except KeyError:
423 raise error.ProgrammingError('invalid type specified: %r' % typ)
423 raise error.ProgrammingError('invalid type specified: %r' % typ)
424 return f(context, mapping, arg)
424 return f(context, mapping, arg)
425
425
426 def runinteger(context, mapping, data):
426 def runinteger(context, mapping, data):
427 return int(data)
427 return int(data)
428
428
429 def runstring(context, mapping, data):
429 def runstring(context, mapping, data):
430 return data
430 return data
431
431
432 def _recursivesymbolblocker(key):
432 def _recursivesymbolblocker(key):
433 def showrecursion(**args):
433 def showrecursion(**args):
434 raise error.Abort(_("recursive reference '%s' in template") % key)
434 raise error.Abort(_("recursive reference '%s' in template") % key)
435 return showrecursion
435 return showrecursion
436
436
437 def _runrecursivesymbol(context, mapping, key):
437 def _runrecursivesymbol(context, mapping, key):
438 raise error.Abort(_("recursive reference '%s' in template") % key)
438 raise error.Abort(_("recursive reference '%s' in template") % key)
439
439
440 def runsymbol(context, mapping, key, default=''):
440 def runsymbol(context, mapping, key, default=''):
441 v = context.symbol(mapping, key)
441 v = context.symbol(mapping, key)
442 if v is None:
442 if v is None:
443 # put poison to cut recursion. we can't move this to parsing phase
443 # put poison to cut recursion. we can't move this to parsing phase
444 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
444 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
445 safemapping = mapping.copy()
445 safemapping = mapping.copy()
446 safemapping[key] = _recursivesymbolblocker(key)
446 safemapping[key] = _recursivesymbolblocker(key)
447 try:
447 try:
448 v = context.process(key, safemapping)
448 v = context.process(key, safemapping)
449 except TemplateNotFound:
449 except TemplateNotFound:
450 v = default
450 v = default
451 if callable(v) and getattr(v, '_requires', None) is None:
451 if callable(v) and getattr(v, '_requires', None) is None:
452 # old templatekw: expand all keywords and resources
452 # old templatekw: expand all keywords and resources
453 props = context._resources.copy()
453 props = context._resources.copy()
454 props.update(mapping)
454 props.update(mapping)
455 return v(**pycompat.strkwargs(props))
455 return v(**pycompat.strkwargs(props))
456 if callable(v):
456 if callable(v):
457 # new templatekw
457 # new templatekw
458 try:
458 try:
459 return v(context, mapping)
459 return v(context, mapping)
460 except ResourceUnavailable:
460 except ResourceUnavailable:
461 # unsupported keyword is mapped to empty just like unknown keyword
461 # unsupported keyword is mapped to empty just like unknown keyword
462 return None
462 return None
463 return v
463 return v
464
464
465 def buildtemplate(exp, context):
465 def buildtemplate(exp, context):
466 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
466 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
467 return (runtemplate, ctmpl)
467 return (runtemplate, ctmpl)
468
468
469 def runtemplate(context, mapping, template):
469 def runtemplate(context, mapping, template):
470 for arg in template:
470 for arg in template:
471 yield evalrawexp(context, mapping, arg)
471 yield evalrawexp(context, mapping, arg)
472
472
473 def buildfilter(exp, context):
473 def buildfilter(exp, context):
474 n = getsymbol(exp[2])
474 n = getsymbol(exp[2])
475 if n in context._filters:
475 if n in context._filters:
476 filt = context._filters[n]
476 filt = context._filters[n]
477 arg = compileexp(exp[1], context, methods)
477 arg = compileexp(exp[1], context, methods)
478 return (runfilter, (arg, filt))
478 return (runfilter, (arg, filt))
479 if n in funcs:
479 if n in funcs:
480 f = funcs[n]
480 f = funcs[n]
481 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
481 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
482 return (f, args)
482 return (f, args)
483 raise error.ParseError(_("unknown function '%s'") % n)
483 raise error.ParseError(_("unknown function '%s'") % n)
484
484
485 def runfilter(context, mapping, data):
485 def runfilter(context, mapping, data):
486 arg, filt = data
486 arg, filt = data
487 thing = evalfuncarg(context, mapping, arg)
487 thing = evalfuncarg(context, mapping, arg)
488 try:
488 try:
489 return filt(thing)
489 return filt(thing)
490 except (ValueError, AttributeError, TypeError):
490 except (ValueError, AttributeError, TypeError):
491 sym = findsymbolicname(arg)
491 sym = findsymbolicname(arg)
492 if sym:
492 if sym:
493 msg = (_("template filter '%s' is not compatible with keyword '%s'")
493 msg = (_("template filter '%s' is not compatible with keyword '%s'")
494 % (pycompat.sysbytes(filt.__name__), sym))
494 % (pycompat.sysbytes(filt.__name__), sym))
495 else:
495 else:
496 msg = (_("incompatible use of template filter '%s'")
496 msg = (_("incompatible use of template filter '%s'")
497 % pycompat.sysbytes(filt.__name__))
497 % pycompat.sysbytes(filt.__name__))
498 raise error.Abort(msg)
498 raise error.Abort(msg)
499
499
500 def buildmap(exp, context):
500 def buildmap(exp, context):
501 darg = compileexp(exp[1], context, methods)
501 darg = compileexp(exp[1], context, methods)
502 targ = gettemplate(exp[2], context)
502 targ = gettemplate(exp[2], context)
503 return (runmap, (darg, targ))
503 return (runmap, (darg, targ))
504
504
505 def runmap(context, mapping, data):
505 def runmap(context, mapping, data):
506 darg, targ = data
506 darg, targ = data
507 d = evalrawexp(context, mapping, darg)
507 d = evalrawexp(context, mapping, darg)
508 if util.safehasattr(d, 'itermaps'):
508 if util.safehasattr(d, 'itermaps'):
509 diter = d.itermaps()
509 diter = d.itermaps()
510 else:
510 else:
511 try:
511 try:
512 diter = iter(d)
512 diter = iter(d)
513 except TypeError:
513 except TypeError:
514 sym = findsymbolicname(darg)
514 sym = findsymbolicname(darg)
515 if sym:
515 if sym:
516 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
516 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
517 else:
517 else:
518 raise error.ParseError(_("%r is not iterable") % d)
518 raise error.ParseError(_("%r is not iterable") % d)
519
519
520 for i, v in enumerate(diter):
520 for i, v in enumerate(diter):
521 lm = mapping.copy()
521 lm = mapping.copy()
522 lm['index'] = i
522 lm['index'] = i
523 if isinstance(v, dict):
523 if isinstance(v, dict):
524 lm.update(v)
524 lm.update(v)
525 lm['originalnode'] = mapping.get('node')
525 lm['originalnode'] = mapping.get('node')
526 yield evalrawexp(context, lm, targ)
526 yield evalrawexp(context, lm, targ)
527 else:
527 else:
528 # v is not an iterable of dicts, this happen when 'key'
528 # v is not an iterable of dicts, this happen when 'key'
529 # has been fully expanded already and format is useless.
529 # has been fully expanded already and format is useless.
530 # If so, return the expanded value.
530 # If so, return the expanded value.
531 yield v
531 yield v
532
532
533 def buildmember(exp, context):
533 def buildmember(exp, context):
534 darg = compileexp(exp[1], context, methods)
534 darg = compileexp(exp[1], context, methods)
535 memb = getsymbol(exp[2])
535 memb = getsymbol(exp[2])
536 return (runmember, (darg, memb))
536 return (runmember, (darg, memb))
537
537
538 def runmember(context, mapping, data):
538 def runmember(context, mapping, data):
539 darg, memb = data
539 darg, memb = data
540 d = evalrawexp(context, mapping, darg)
540 d = evalrawexp(context, mapping, darg)
541 if util.safehasattr(d, 'tomap'):
541 if util.safehasattr(d, 'tomap'):
542 lm = mapping.copy()
542 lm = mapping.copy()
543 lm.update(d.tomap())
543 lm.update(d.tomap())
544 return runsymbol(context, lm, memb)
544 return runsymbol(context, lm, memb)
545 if util.safehasattr(d, 'get'):
545 if util.safehasattr(d, 'get'):
546 return _getdictitem(d, memb)
546 return _getdictitem(d, memb)
547
547
548 sym = findsymbolicname(darg)
548 sym = findsymbolicname(darg)
549 if sym:
549 if sym:
550 raise error.ParseError(_("keyword '%s' has no member") % sym)
550 raise error.ParseError(_("keyword '%s' has no member") % sym)
551 else:
551 else:
552 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
552 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
553
553
554 def buildnegate(exp, context):
554 def buildnegate(exp, context):
555 arg = compileexp(exp[1], context, exprmethods)
555 arg = compileexp(exp[1], context, exprmethods)
556 return (runnegate, arg)
556 return (runnegate, arg)
557
557
558 def runnegate(context, mapping, data):
558 def runnegate(context, mapping, data):
559 data = evalinteger(context, mapping, data,
559 data = evalinteger(context, mapping, data,
560 _('negation needs an integer argument'))
560 _('negation needs an integer argument'))
561 return -data
561 return -data
562
562
563 def buildarithmetic(exp, context, func):
563 def buildarithmetic(exp, context, func):
564 left = compileexp(exp[1], context, exprmethods)
564 left = compileexp(exp[1], context, exprmethods)
565 right = compileexp(exp[2], context, exprmethods)
565 right = compileexp(exp[2], context, exprmethods)
566 return (runarithmetic, (func, left, right))
566 return (runarithmetic, (func, left, right))
567
567
568 def runarithmetic(context, mapping, data):
568 def runarithmetic(context, mapping, data):
569 func, left, right = data
569 func, left, right = data
570 left = evalinteger(context, mapping, left,
570 left = evalinteger(context, mapping, left,
571 _('arithmetic only defined on integers'))
571 _('arithmetic only defined on integers'))
572 right = evalinteger(context, mapping, right,
572 right = evalinteger(context, mapping, right,
573 _('arithmetic only defined on integers'))
573 _('arithmetic only defined on integers'))
574 try:
574 try:
575 return func(left, right)
575 return func(left, right)
576 except ZeroDivisionError:
576 except ZeroDivisionError:
577 raise error.Abort(_('division by zero is not defined'))
577 raise error.Abort(_('division by zero is not defined'))
578
578
579 def buildfunc(exp, context):
579 def buildfunc(exp, context):
580 n = getsymbol(exp[1])
580 n = getsymbol(exp[1])
581 if n in funcs:
581 if n in funcs:
582 f = funcs[n]
582 f = funcs[n]
583 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
583 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
584 return (f, args)
584 return (f, args)
585 if n in context._filters:
585 if n in context._filters:
586 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
586 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
587 if len(args) != 1:
587 if len(args) != 1:
588 raise error.ParseError(_("filter %s expects one argument") % n)
588 raise error.ParseError(_("filter %s expects one argument") % n)
589 f = context._filters[n]
589 f = context._filters[n]
590 return (runfilter, (args[0], f))
590 return (runfilter, (args[0], f))
591 raise error.ParseError(_("unknown function '%s'") % n)
591 raise error.ParseError(_("unknown function '%s'") % n)
592
592
593 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
593 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
594 """Compile parsed tree of function arguments into list or dict of
594 """Compile parsed tree of function arguments into list or dict of
595 (func, data) pairs
595 (func, data) pairs
596
596
597 >>> context = engine(lambda t: (runsymbol, t))
597 >>> context = engine(lambda t: (runsymbol, t))
598 >>> def fargs(expr, argspec):
598 >>> def fargs(expr, argspec):
599 ... x = _parseexpr(expr)
599 ... x = _parseexpr(expr)
600 ... n = getsymbol(x[1])
600 ... n = getsymbol(x[1])
601 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
601 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
602 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
602 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
603 ['l', 'k']
603 ['l', 'k']
604 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
604 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
605 >>> list(args.keys()), list(args[b'opts'].keys())
605 >>> list(args.keys()), list(args[b'opts'].keys())
606 (['opts'], ['opts', 'k'])
606 (['opts'], ['opts', 'k'])
607 """
607 """
608 def compiledict(xs):
608 def compiledict(xs):
609 return util.sortdict((k, compileexp(x, context, curmethods))
609 return util.sortdict((k, compileexp(x, context, curmethods))
610 for k, x in xs.iteritems())
610 for k, x in xs.iteritems())
611 def compilelist(xs):
611 def compilelist(xs):
612 return [compileexp(x, context, curmethods) for x in xs]
612 return [compileexp(x, context, curmethods) for x in xs]
613
613
614 if not argspec:
614 if not argspec:
615 # filter or function with no argspec: return list of positional args
615 # filter or function with no argspec: return list of positional args
616 return compilelist(getlist(exp))
616 return compilelist(getlist(exp))
617
617
618 # function with argspec: return dict of named args
618 # function with argspec: return dict of named args
619 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
619 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
620 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
620 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
621 keyvaluenode='keyvalue', keynode='symbol')
621 keyvaluenode='keyvalue', keynode='symbol')
622 compargs = util.sortdict()
622 compargs = util.sortdict()
623 if varkey:
623 if varkey:
624 compargs[varkey] = compilelist(treeargs.pop(varkey))
624 compargs[varkey] = compilelist(treeargs.pop(varkey))
625 if optkey:
625 if optkey:
626 compargs[optkey] = compiledict(treeargs.pop(optkey))
626 compargs[optkey] = compiledict(treeargs.pop(optkey))
627 compargs.update(compiledict(treeargs))
627 compargs.update(compiledict(treeargs))
628 return compargs
628 return compargs
629
629
630 def buildkeyvaluepair(exp, content):
630 def buildkeyvaluepair(exp, content):
631 raise error.ParseError(_("can't use a key-value pair in this context"))
631 raise error.ParseError(_("can't use a key-value pair in this context"))
632
632
633 # dict of template built-in functions
633 # dict of template built-in functions
634 funcs = {}
634 funcs = {}
635
635
636 templatefunc = registrar.templatefunc(funcs)
636 templatefunc = registrar.templatefunc(funcs)
637
637
638 @templatefunc('date(date[, fmt])')
638 @templatefunc('date(date[, fmt])')
639 def date(context, mapping, args):
639 def date(context, mapping, args):
640 """Format a date. See :hg:`help dates` for formatting
640 """Format a date. See :hg:`help dates` for formatting
641 strings. The default is a Unix date format, including the timezone:
641 strings. The default is a Unix date format, including the timezone:
642 "Mon Sep 04 15:13:13 2006 0700"."""
642 "Mon Sep 04 15:13:13 2006 0700"."""
643 if not (1 <= len(args) <= 2):
643 if not (1 <= len(args) <= 2):
644 # i18n: "date" is a keyword
644 # i18n: "date" is a keyword
645 raise error.ParseError(_("date expects one or two arguments"))
645 raise error.ParseError(_("date expects one or two arguments"))
646
646
647 date = evalfuncarg(context, mapping, args[0])
647 date = evalfuncarg(context, mapping, args[0])
648 fmt = None
648 fmt = None
649 if len(args) == 2:
649 if len(args) == 2:
650 fmt = evalstring(context, mapping, args[1])
650 fmt = evalstring(context, mapping, args[1])
651 try:
651 try:
652 if fmt is None:
652 if fmt is None:
653 return dateutil.datestr(date)
653 return dateutil.datestr(date)
654 else:
654 else:
655 return dateutil.datestr(date, fmt)
655 return dateutil.datestr(date, fmt)
656 except (TypeError, ValueError):
656 except (TypeError, ValueError):
657 # i18n: "date" is a keyword
657 # i18n: "date" is a keyword
658 raise error.ParseError(_("date expects a date information"))
658 raise error.ParseError(_("date expects a date information"))
659
659
660 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
660 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
661 def dict_(context, mapping, args):
661 def dict_(context, mapping, args):
662 """Construct a dict from key-value pairs. A key may be omitted if
662 """Construct a dict from key-value pairs. A key may be omitted if
663 a value expression can provide an unambiguous name."""
663 a value expression can provide an unambiguous name."""
664 data = util.sortdict()
664 data = util.sortdict()
665
665
666 for v in args['args']:
666 for v in args['args']:
667 k = findsymbolicname(v)
667 k = findsymbolicname(v)
668 if not k:
668 if not k:
669 raise error.ParseError(_('dict key cannot be inferred'))
669 raise error.ParseError(_('dict key cannot be inferred'))
670 if k in data or k in args['kwargs']:
670 if k in data or k in args['kwargs']:
671 raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
671 raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
672 data[k] = evalfuncarg(context, mapping, v)
672 data[k] = evalfuncarg(context, mapping, v)
673
673
674 data.update((k, evalfuncarg(context, mapping, v))
674 data.update((k, evalfuncarg(context, mapping, v))
675 for k, v in args['kwargs'].iteritems())
675 for k, v in args['kwargs'].iteritems())
676 return templatekw.hybriddict(data)
676 return templatekw.hybriddict(data)
677
677
678 @templatefunc('diff([includepattern [, excludepattern]])')
678 @templatefunc('diff([includepattern [, excludepattern]])')
679 def diff(context, mapping, args):
679 def diff(context, mapping, args):
680 """Show a diff, optionally
680 """Show a diff, optionally
681 specifying files to include or exclude."""
681 specifying files to include or exclude."""
682 if len(args) > 2:
682 if len(args) > 2:
683 # i18n: "diff" is a keyword
683 # i18n: "diff" is a keyword
684 raise error.ParseError(_("diff expects zero, one, or two arguments"))
684 raise error.ParseError(_("diff expects zero, one, or two arguments"))
685
685
686 def getpatterns(i):
686 def getpatterns(i):
687 if i < len(args):
687 if i < len(args):
688 s = evalstring(context, mapping, args[i]).strip()
688 s = evalstring(context, mapping, args[i]).strip()
689 if s:
689 if s:
690 return [s]
690 return [s]
691 return []
691 return []
692
692
693 ctx = context.resource(mapping, 'ctx')
693 ctx = context.resource(mapping, 'ctx')
694 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
694 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
695
695
696 return ''.join(chunks)
696 return ''.join(chunks)
697
697
698 @templatefunc('extdata(source)', argspec='source')
698 @templatefunc('extdata(source)', argspec='source')
699 def extdata(context, mapping, args):
699 def extdata(context, mapping, args):
700 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
700 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
701 if 'source' not in args:
701 if 'source' not in args:
702 # i18n: "extdata" is a keyword
702 # i18n: "extdata" is a keyword
703 raise error.ParseError(_('extdata expects one argument'))
703 raise error.ParseError(_('extdata expects one argument'))
704
704
705 source = evalstring(context, mapping, args['source'])
705 source = evalstring(context, mapping, args['source'])
706 cache = context.resource(mapping, 'cache').setdefault('extdata', {})
706 cache = context.resource(mapping, 'cache').setdefault('extdata', {})
707 ctx = context.resource(mapping, 'ctx')
707 ctx = context.resource(mapping, 'ctx')
708 if source in cache:
708 if source in cache:
709 data = cache[source]
709 data = cache[source]
710 else:
710 else:
711 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
711 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
712 return data.get(ctx.rev(), '')
712 return data.get(ctx.rev(), '')
713
713
714 @templatefunc('files(pattern)')
714 @templatefunc('files(pattern)')
715 def files(context, mapping, args):
715 def files(context, mapping, args):
716 """All files of the current changeset matching the pattern. See
716 """All files of the current changeset matching the pattern. See
717 :hg:`help patterns`."""
717 :hg:`help patterns`."""
718 if not len(args) == 1:
718 if not len(args) == 1:
719 # i18n: "files" is a keyword
719 # i18n: "files" is a keyword
720 raise error.ParseError(_("files expects one argument"))
720 raise error.ParseError(_("files expects one argument"))
721
721
722 raw = evalstring(context, mapping, args[0])
722 raw = evalstring(context, mapping, args[0])
723 ctx = context.resource(mapping, 'ctx')
723 ctx = context.resource(mapping, 'ctx')
724 m = ctx.match([raw])
724 m = ctx.match([raw])
725 files = list(ctx.matches(m))
725 files = list(ctx.matches(m))
726 return templatekw.compatlist(context, mapping, "file", files)
726 return templatekw.compatlist(context, mapping, "file", files)
727
727
728 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
728 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
729 def fill(context, mapping, args):
729 def fill(context, mapping, args):
730 """Fill many
730 """Fill many
731 paragraphs with optional indentation. See the "fill" filter."""
731 paragraphs with optional indentation. See the "fill" filter."""
732 if not (1 <= len(args) <= 4):
732 if not (1 <= len(args) <= 4):
733 # i18n: "fill" is a keyword
733 # i18n: "fill" is a keyword
734 raise error.ParseError(_("fill expects one to four arguments"))
734 raise error.ParseError(_("fill expects one to four arguments"))
735
735
736 text = evalstring(context, mapping, args[0])
736 text = evalstring(context, mapping, args[0])
737 width = 76
737 width = 76
738 initindent = ''
738 initindent = ''
739 hangindent = ''
739 hangindent = ''
740 if 2 <= len(args) <= 4:
740 if 2 <= len(args) <= 4:
741 width = evalinteger(context, mapping, args[1],
741 width = evalinteger(context, mapping, args[1],
742 # i18n: "fill" is a keyword
742 # i18n: "fill" is a keyword
743 _("fill expects an integer width"))
743 _("fill expects an integer width"))
744 try:
744 try:
745 initindent = evalstring(context, mapping, args[2])
745 initindent = evalstring(context, mapping, args[2])
746 hangindent = evalstring(context, mapping, args[3])
746 hangindent = evalstring(context, mapping, args[3])
747 except IndexError:
747 except IndexError:
748 pass
748 pass
749
749
750 return templatefilters.fill(text, width, initindent, hangindent)
750 return templatefilters.fill(text, width, initindent, hangindent)
751
751
752 @templatefunc('formatnode(node)')
752 @templatefunc('formatnode(node)')
753 def formatnode(context, mapping, args):
753 def formatnode(context, mapping, args):
754 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
754 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
755 if len(args) != 1:
755 if len(args) != 1:
756 # i18n: "formatnode" is a keyword
756 # i18n: "formatnode" is a keyword
757 raise error.ParseError(_("formatnode expects one argument"))
757 raise error.ParseError(_("formatnode expects one argument"))
758
758
759 ui = context.resource(mapping, 'ui')
759 ui = context.resource(mapping, 'ui')
760 node = evalstring(context, mapping, args[0])
760 node = evalstring(context, mapping, args[0])
761 if ui.debugflag:
761 if ui.debugflag:
762 return node
762 return node
763 return templatefilters.short(node)
763 return templatefilters.short(node)
764
764
765 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
765 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
766 argspec='text width fillchar left')
766 argspec='text width fillchar left')
767 def pad(context, mapping, args):
767 def pad(context, mapping, args):
768 """Pad text with a
768 """Pad text with a
769 fill character."""
769 fill character."""
770 if 'text' not in args or 'width' not in args:
770 if 'text' not in args or 'width' not in args:
771 # i18n: "pad" is a keyword
771 # i18n: "pad" is a keyword
772 raise error.ParseError(_("pad() expects two to four arguments"))
772 raise error.ParseError(_("pad() expects two to four arguments"))
773
773
774 width = evalinteger(context, mapping, args['width'],
774 width = evalinteger(context, mapping, args['width'],
775 # i18n: "pad" is a keyword
775 # i18n: "pad" is a keyword
776 _("pad() expects an integer width"))
776 _("pad() expects an integer width"))
777
777
778 text = evalstring(context, mapping, args['text'])
778 text = evalstring(context, mapping, args['text'])
779
779
780 left = False
780 left = False
781 fillchar = ' '
781 fillchar = ' '
782 if 'fillchar' in args:
782 if 'fillchar' in args:
783 fillchar = evalstring(context, mapping, args['fillchar'])
783 fillchar = evalstring(context, mapping, args['fillchar'])
784 if len(color.stripeffects(fillchar)) != 1:
784 if len(color.stripeffects(fillchar)) != 1:
785 # i18n: "pad" is a keyword
785 # i18n: "pad" is a keyword
786 raise error.ParseError(_("pad() expects a single fill character"))
786 raise error.ParseError(_("pad() expects a single fill character"))
787 if 'left' in args:
787 if 'left' in args:
788 left = evalboolean(context, mapping, args['left'])
788 left = evalboolean(context, mapping, args['left'])
789
789
790 fillwidth = width - encoding.colwidth(color.stripeffects(text))
790 fillwidth = width - encoding.colwidth(color.stripeffects(text))
791 if fillwidth <= 0:
791 if fillwidth <= 0:
792 return text
792 return text
793 if left:
793 if left:
794 return fillchar * fillwidth + text
794 return fillchar * fillwidth + text
795 else:
795 else:
796 return text + fillchar * fillwidth
796 return text + fillchar * fillwidth
797
797
798 @templatefunc('indent(text, indentchars[, firstline])')
798 @templatefunc('indent(text, indentchars[, firstline])')
799 def indent(context, mapping, args):
799 def indent(context, mapping, args):
800 """Indents all non-empty lines
800 """Indents all non-empty lines
801 with the characters given in the indentchars string. An optional
801 with the characters given in the indentchars string. An optional
802 third parameter will override the indent for the first line only
802 third parameter will override the indent for the first line only
803 if present."""
803 if present."""
804 if not (2 <= len(args) <= 3):
804 if not (2 <= len(args) <= 3):
805 # i18n: "indent" is a keyword
805 # i18n: "indent" is a keyword
806 raise error.ParseError(_("indent() expects two or three arguments"))
806 raise error.ParseError(_("indent() expects two or three arguments"))
807
807
808 text = evalstring(context, mapping, args[0])
808 text = evalstring(context, mapping, args[0])
809 indent = evalstring(context, mapping, args[1])
809 indent = evalstring(context, mapping, args[1])
810
810
811 if len(args) == 3:
811 if len(args) == 3:
812 firstline = evalstring(context, mapping, args[2])
812 firstline = evalstring(context, mapping, args[2])
813 else:
813 else:
814 firstline = indent
814 firstline = indent
815
815
816 # the indent function doesn't indent the first line, so we do it here
816 # the indent function doesn't indent the first line, so we do it here
817 return templatefilters.indent(firstline + text, indent)
817 return templatefilters.indent(firstline + text, indent)
818
818
819 @templatefunc('get(dict, key)')
819 @templatefunc('get(dict, key)')
820 def get(context, mapping, args):
820 def get(context, mapping, args):
821 """Get an attribute/key from an object. Some keywords
821 """Get an attribute/key from an object. Some keywords
822 are complex types. This function allows you to obtain the value of an
822 are complex types. This function allows you to obtain the value of an
823 attribute on these types."""
823 attribute on these types."""
824 if len(args) != 2:
824 if len(args) != 2:
825 # i18n: "get" is a keyword
825 # i18n: "get" is a keyword
826 raise error.ParseError(_("get() expects two arguments"))
826 raise error.ParseError(_("get() expects two arguments"))
827
827
828 dictarg = evalfuncarg(context, mapping, args[0])
828 dictarg = evalfuncarg(context, mapping, args[0])
829 if not util.safehasattr(dictarg, 'get'):
829 if not util.safehasattr(dictarg, 'get'):
830 # i18n: "get" is a keyword
830 # i18n: "get" is a keyword
831 raise error.ParseError(_("get() expects a dict as first argument"))
831 raise error.ParseError(_("get() expects a dict as first argument"))
832
832
833 key = evalfuncarg(context, mapping, args[1])
833 key = evalfuncarg(context, mapping, args[1])
834 return _getdictitem(dictarg, key)
834 return _getdictitem(dictarg, key)
835
835
836 def _getdictitem(dictarg, key):
836 def _getdictitem(dictarg, key):
837 val = dictarg.get(key)
837 val = dictarg.get(key)
838 if val is None:
838 if val is None:
839 return
839 return
840 return templatekw.wraphybridvalue(dictarg, key, val)
840 return templatekw.wraphybridvalue(dictarg, key, val)
841
841
842 @templatefunc('if(expr, then[, else])')
842 @templatefunc('if(expr, then[, else])')
843 def if_(context, mapping, args):
843 def if_(context, mapping, args):
844 """Conditionally execute based on the result of
844 """Conditionally execute based on the result of
845 an expression."""
845 an expression."""
846 if not (2 <= len(args) <= 3):
846 if not (2 <= len(args) <= 3):
847 # i18n: "if" is a keyword
847 # i18n: "if" is a keyword
848 raise error.ParseError(_("if expects two or three arguments"))
848 raise error.ParseError(_("if expects two or three arguments"))
849
849
850 test = evalboolean(context, mapping, args[0])
850 test = evalboolean(context, mapping, args[0])
851 if test:
851 if test:
852 yield evalrawexp(context, mapping, args[1])
852 yield evalrawexp(context, mapping, args[1])
853 elif len(args) == 3:
853 elif len(args) == 3:
854 yield evalrawexp(context, mapping, args[2])
854 yield evalrawexp(context, mapping, args[2])
855
855
856 @templatefunc('ifcontains(needle, haystack, then[, else])')
856 @templatefunc('ifcontains(needle, haystack, then[, else])')
857 def ifcontains(context, mapping, args):
857 def ifcontains(context, mapping, args):
858 """Conditionally execute based
858 """Conditionally execute based
859 on whether the item "needle" is in "haystack"."""
859 on whether the item "needle" is in "haystack"."""
860 if not (3 <= len(args) <= 4):
860 if not (3 <= len(args) <= 4):
861 # i18n: "ifcontains" is a keyword
861 # i18n: "ifcontains" is a keyword
862 raise error.ParseError(_("ifcontains expects three or four arguments"))
862 raise error.ParseError(_("ifcontains expects three or four arguments"))
863
863
864 haystack = evalfuncarg(context, mapping, args[1])
864 haystack = evalfuncarg(context, mapping, args[1])
865 try:
865 try:
866 needle = evalastype(context, mapping, args[0],
866 needle = evalastype(context, mapping, args[0],
867 getattr(haystack, 'keytype', None) or bytes)
867 getattr(haystack, 'keytype', None) or bytes)
868 found = (needle in haystack)
868 found = (needle in haystack)
869 except error.ParseError:
869 except error.ParseError:
870 found = False
870 found = False
871
871
872 if found:
872 if found:
873 yield evalrawexp(context, mapping, args[2])
873 yield evalrawexp(context, mapping, args[2])
874 elif len(args) == 4:
874 elif len(args) == 4:
875 yield evalrawexp(context, mapping, args[3])
875 yield evalrawexp(context, mapping, args[3])
876
876
877 @templatefunc('ifeq(expr1, expr2, then[, else])')
877 @templatefunc('ifeq(expr1, expr2, then[, else])')
878 def ifeq(context, mapping, args):
878 def ifeq(context, mapping, args):
879 """Conditionally execute based on
879 """Conditionally execute based on
880 whether 2 items are equivalent."""
880 whether 2 items are equivalent."""
881 if not (3 <= len(args) <= 4):
881 if not (3 <= len(args) <= 4):
882 # i18n: "ifeq" is a keyword
882 # i18n: "ifeq" is a keyword
883 raise error.ParseError(_("ifeq expects three or four arguments"))
883 raise error.ParseError(_("ifeq expects three or four arguments"))
884
884
885 test = evalstring(context, mapping, args[0])
885 test = evalstring(context, mapping, args[0])
886 match = evalstring(context, mapping, args[1])
886 match = evalstring(context, mapping, args[1])
887 if test == match:
887 if test == match:
888 yield evalrawexp(context, mapping, args[2])
888 yield evalrawexp(context, mapping, args[2])
889 elif len(args) == 4:
889 elif len(args) == 4:
890 yield evalrawexp(context, mapping, args[3])
890 yield evalrawexp(context, mapping, args[3])
891
891
892 @templatefunc('join(list, sep)')
892 @templatefunc('join(list, sep)')
893 def join(context, mapping, args):
893 def join(context, mapping, args):
894 """Join items in a list with a delimiter."""
894 """Join items in a list with a delimiter."""
895 if not (1 <= len(args) <= 2):
895 if not (1 <= len(args) <= 2):
896 # i18n: "join" is a keyword
896 # i18n: "join" is a keyword
897 raise error.ParseError(_("join expects one or two arguments"))
897 raise error.ParseError(_("join expects one or two arguments"))
898
898
899 # TODO: perhaps this should be evalfuncarg(), but it can't because hgweb
899 # TODO: perhaps this should be evalfuncarg(), but it can't because hgweb
900 # abuses generator as a keyword that returns a list of dicts.
900 # abuses generator as a keyword that returns a list of dicts.
901 joinset = evalrawexp(context, mapping, args[0])
901 joinset = evalrawexp(context, mapping, args[0])
902 joinset = templatekw.unwrapvalue(joinset)
902 joinset = templatekw.unwrapvalue(joinset)
903 joinfmt = getattr(joinset, 'joinfmt', pycompat.identity)
903 joinfmt = getattr(joinset, 'joinfmt', pycompat.identity)
904 joiner = " "
904 joiner = " "
905 if len(args) > 1:
905 if len(args) > 1:
906 joiner = evalstring(context, mapping, args[1])
906 joiner = evalstring(context, mapping, args[1])
907
907
908 first = True
908 first = True
909 for x in pycompat.maybebytestr(joinset):
909 for x in pycompat.maybebytestr(joinset):
910 if first:
910 if first:
911 first = False
911 first = False
912 else:
912 else:
913 yield joiner
913 yield joiner
914 yield joinfmt(x)
914 yield joinfmt(x)
915
915
916 @templatefunc('label(label, expr)')
916 @templatefunc('label(label, expr)')
917 def label(context, mapping, args):
917 def label(context, mapping, args):
918 """Apply a label to generated content. Content with
918 """Apply a label to generated content. Content with
919 a label applied can result in additional post-processing, such as
919 a label applied can result in additional post-processing, such as
920 automatic colorization."""
920 automatic colorization."""
921 if len(args) != 2:
921 if len(args) != 2:
922 # i18n: "label" is a keyword
922 # i18n: "label" is a keyword
923 raise error.ParseError(_("label expects two arguments"))
923 raise error.ParseError(_("label expects two arguments"))
924
924
925 ui = context.resource(mapping, 'ui')
925 ui = context.resource(mapping, 'ui')
926 thing = evalstring(context, mapping, args[1])
926 thing = evalstring(context, mapping, args[1])
927 # preserve unknown symbol as literal so effects like 'red', 'bold',
927 # preserve unknown symbol as literal so effects like 'red', 'bold',
928 # etc. don't need to be quoted
928 # etc. don't need to be quoted
929 label = evalstringliteral(context, mapping, args[0])
929 label = evalstringliteral(context, mapping, args[0])
930
930
931 return ui.label(thing, label)
931 return ui.label(thing, label)
932
932
933 @templatefunc('latesttag([pattern])')
933 @templatefunc('latesttag([pattern])')
934 def latesttag(context, mapping, args):
934 def latesttag(context, mapping, args):
935 """The global tags matching the given pattern on the
935 """The global tags matching the given pattern on the
936 most recent globally tagged ancestor of this changeset.
936 most recent globally tagged ancestor of this changeset.
937 If no such tags exist, the "{tag}" template resolves to
937 If no such tags exist, the "{tag}" template resolves to
938 the string "null"."""
938 the string "null"."""
939 if len(args) > 1:
939 if len(args) > 1:
940 # i18n: "latesttag" is a keyword
940 # i18n: "latesttag" is a keyword
941 raise error.ParseError(_("latesttag expects at most one argument"))
941 raise error.ParseError(_("latesttag expects at most one argument"))
942
942
943 pattern = None
943 pattern = None
944 if len(args) == 1:
944 if len(args) == 1:
945 pattern = evalstring(context, mapping, args[0])
945 pattern = evalstring(context, mapping, args[0])
946 return templatekw.showlatesttags(context, mapping, pattern)
946 return templatekw.showlatesttags(context, mapping, pattern)
947
947
948 @templatefunc('localdate(date[, tz])')
948 @templatefunc('localdate(date[, tz])')
949 def localdate(context, mapping, args):
949 def localdate(context, mapping, args):
950 """Converts a date to the specified timezone.
950 """Converts a date to the specified timezone.
951 The default is local date."""
951 The default is local date."""
952 if not (1 <= len(args) <= 2):
952 if not (1 <= len(args) <= 2):
953 # i18n: "localdate" is a keyword
953 # i18n: "localdate" is a keyword
954 raise error.ParseError(_("localdate expects one or two arguments"))
954 raise error.ParseError(_("localdate expects one or two arguments"))
955
955
956 date = evalfuncarg(context, mapping, args[0])
956 date = evalfuncarg(context, mapping, args[0])
957 try:
957 try:
958 date = dateutil.parsedate(date)
958 date = dateutil.parsedate(date)
959 except AttributeError: # not str nor date tuple
959 except AttributeError: # not str nor date tuple
960 # i18n: "localdate" is a keyword
960 # i18n: "localdate" is a keyword
961 raise error.ParseError(_("localdate expects a date information"))
961 raise error.ParseError(_("localdate expects a date information"))
962 if len(args) >= 2:
962 if len(args) >= 2:
963 tzoffset = None
963 tzoffset = None
964 tz = evalfuncarg(context, mapping, args[1])
964 tz = evalfuncarg(context, mapping, args[1])
965 if isinstance(tz, bytes):
965 if isinstance(tz, bytes):
966 tzoffset, remainder = dateutil.parsetimezone(tz)
966 tzoffset, remainder = dateutil.parsetimezone(tz)
967 if remainder:
967 if remainder:
968 tzoffset = None
968 tzoffset = None
969 if tzoffset is None:
969 if tzoffset is None:
970 try:
970 try:
971 tzoffset = int(tz)
971 tzoffset = int(tz)
972 except (TypeError, ValueError):
972 except (TypeError, ValueError):
973 # i18n: "localdate" is a keyword
973 # i18n: "localdate" is a keyword
974 raise error.ParseError(_("localdate expects a timezone"))
974 raise error.ParseError(_("localdate expects a timezone"))
975 else:
975 else:
976 tzoffset = dateutil.makedate()[1]
976 tzoffset = dateutil.makedate()[1]
977 return (date[0], tzoffset)
977 return (date[0], tzoffset)
978
978
979 @templatefunc('max(iterable)')
979 @templatefunc('max(iterable)')
980 def max_(context, mapping, args, **kwargs):
980 def max_(context, mapping, args, **kwargs):
981 """Return the max of an iterable"""
981 """Return the max of an iterable"""
982 if len(args) != 1:
982 if len(args) != 1:
983 # i18n: "max" is a keyword
983 # i18n: "max" is a keyword
984 raise error.ParseError(_("max expects one argument"))
984 raise error.ParseError(_("max expects one argument"))
985
985
986 iterable = evalfuncarg(context, mapping, args[0])
986 iterable = evalfuncarg(context, mapping, args[0])
987 try:
987 try:
988 x = max(pycompat.maybebytestr(iterable))
988 x = max(pycompat.maybebytestr(iterable))
989 except (TypeError, ValueError):
989 except (TypeError, ValueError):
990 # i18n: "max" is a keyword
990 # i18n: "max" is a keyword
991 raise error.ParseError(_("max first argument should be an iterable"))
991 raise error.ParseError(_("max first argument should be an iterable"))
992 return templatekw.wraphybridvalue(iterable, x, x)
992 return templatekw.wraphybridvalue(iterable, x, x)
993
993
994 @templatefunc('min(iterable)')
994 @templatefunc('min(iterable)')
995 def min_(context, mapping, args, **kwargs):
995 def min_(context, mapping, args, **kwargs):
996 """Return the min of an iterable"""
996 """Return the min of an iterable"""
997 if len(args) != 1:
997 if len(args) != 1:
998 # i18n: "min" is a keyword
998 # i18n: "min" is a keyword
999 raise error.ParseError(_("min expects one argument"))
999 raise error.ParseError(_("min expects one argument"))
1000
1000
1001 iterable = evalfuncarg(context, mapping, args[0])
1001 iterable = evalfuncarg(context, mapping, args[0])
1002 try:
1002 try:
1003 x = min(pycompat.maybebytestr(iterable))
1003 x = min(pycompat.maybebytestr(iterable))
1004 except (TypeError, ValueError):
1004 except (TypeError, ValueError):
1005 # i18n: "min" is a keyword
1005 # i18n: "min" is a keyword
1006 raise error.ParseError(_("min first argument should be an iterable"))
1006 raise error.ParseError(_("min first argument should be an iterable"))
1007 return templatekw.wraphybridvalue(iterable, x, x)
1007 return templatekw.wraphybridvalue(iterable, x, x)
1008
1008
1009 @templatefunc('mod(a, b)')
1009 @templatefunc('mod(a, b)')
1010 def mod(context, mapping, args):
1010 def mod(context, mapping, args):
1011 """Calculate a mod b such that a / b + a mod b == a"""
1011 """Calculate a mod b such that a / b + a mod b == a"""
1012 if not len(args) == 2:
1012 if not len(args) == 2:
1013 # i18n: "mod" is a keyword
1013 # i18n: "mod" is a keyword
1014 raise error.ParseError(_("mod expects two arguments"))
1014 raise error.ParseError(_("mod expects two arguments"))
1015
1015
1016 func = lambda a, b: a % b
1016 func = lambda a, b: a % b
1017 return runarithmetic(context, mapping, (func, args[0], args[1]))
1017 return runarithmetic(context, mapping, (func, args[0], args[1]))
1018
1018
1019 @templatefunc('obsfateoperations(markers)')
1019 @templatefunc('obsfateoperations(markers)')
1020 def obsfateoperations(context, mapping, args):
1020 def obsfateoperations(context, mapping, args):
1021 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
1021 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
1022 if len(args) != 1:
1022 if len(args) != 1:
1023 # i18n: "obsfateoperations" is a keyword
1023 # i18n: "obsfateoperations" is a keyword
1024 raise error.ParseError(_("obsfateoperations expects one argument"))
1024 raise error.ParseError(_("obsfateoperations expects one argument"))
1025
1025
1026 markers = evalfuncarg(context, mapping, args[0])
1026 markers = evalfuncarg(context, mapping, args[0])
1027
1027
1028 try:
1028 try:
1029 data = obsutil.markersoperations(markers)
1029 data = obsutil.markersoperations(markers)
1030 return templatekw.hybridlist(data, name='operation')
1030 return templatekw.hybridlist(data, name='operation')
1031 except (TypeError, KeyError):
1031 except (TypeError, KeyError):
1032 # i18n: "obsfateoperations" is a keyword
1032 # i18n: "obsfateoperations" is a keyword
1033 errmsg = _("obsfateoperations first argument should be an iterable")
1033 errmsg = _("obsfateoperations first argument should be an iterable")
1034 raise error.ParseError(errmsg)
1034 raise error.ParseError(errmsg)
1035
1035
1036 @templatefunc('obsfatedate(markers)')
1036 @templatefunc('obsfatedate(markers)')
1037 def obsfatedate(context, mapping, args):
1037 def obsfatedate(context, mapping, args):
1038 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
1038 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
1039 if len(args) != 1:
1039 if len(args) != 1:
1040 # i18n: "obsfatedate" is a keyword
1040 # i18n: "obsfatedate" is a keyword
1041 raise error.ParseError(_("obsfatedate expects one argument"))
1041 raise error.ParseError(_("obsfatedate expects one argument"))
1042
1042
1043 markers = evalfuncarg(context, mapping, args[0])
1043 markers = evalfuncarg(context, mapping, args[0])
1044
1044
1045 try:
1045 try:
1046 data = obsutil.markersdates(markers)
1046 data = obsutil.markersdates(markers)
1047 return templatekw.hybridlist(data, name='date', fmt='%d %d')
1047 return templatekw.hybridlist(data, name='date', fmt='%d %d')
1048 except (TypeError, KeyError):
1048 except (TypeError, KeyError):
1049 # i18n: "obsfatedate" is a keyword
1049 # i18n: "obsfatedate" is a keyword
1050 errmsg = _("obsfatedate first argument should be an iterable")
1050 errmsg = _("obsfatedate first argument should be an iterable")
1051 raise error.ParseError(errmsg)
1051 raise error.ParseError(errmsg)
1052
1052
1053 @templatefunc('obsfateusers(markers)')
1053 @templatefunc('obsfateusers(markers)')
1054 def obsfateusers(context, mapping, args):
1054 def obsfateusers(context, mapping, args):
1055 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
1055 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
1056 if len(args) != 1:
1056 if len(args) != 1:
1057 # i18n: "obsfateusers" is a keyword
1057 # i18n: "obsfateusers" is a keyword
1058 raise error.ParseError(_("obsfateusers expects one argument"))
1058 raise error.ParseError(_("obsfateusers expects one argument"))
1059
1059
1060 markers = evalfuncarg(context, mapping, args[0])
1060 markers = evalfuncarg(context, mapping, args[0])
1061
1061
1062 try:
1062 try:
1063 data = obsutil.markersusers(markers)
1063 data = obsutil.markersusers(markers)
1064 return templatekw.hybridlist(data, name='user')
1064 return templatekw.hybridlist(data, name='user')
1065 except (TypeError, KeyError, ValueError):
1065 except (TypeError, KeyError, ValueError):
1066 # i18n: "obsfateusers" is a keyword
1066 # i18n: "obsfateusers" is a keyword
1067 msg = _("obsfateusers first argument should be an iterable of "
1067 msg = _("obsfateusers first argument should be an iterable of "
1068 "obsmakers")
1068 "obsmakers")
1069 raise error.ParseError(msg)
1069 raise error.ParseError(msg)
1070
1070
1071 @templatefunc('obsfateverb(successors, markers)')
1071 @templatefunc('obsfateverb(successors, markers)')
1072 def obsfateverb(context, mapping, args):
1072 def obsfateverb(context, mapping, args):
1073 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
1073 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
1074 if len(args) != 2:
1074 if len(args) != 2:
1075 # i18n: "obsfateverb" is a keyword
1075 # i18n: "obsfateverb" is a keyword
1076 raise error.ParseError(_("obsfateverb expects two arguments"))
1076 raise error.ParseError(_("obsfateverb expects two arguments"))
1077
1077
1078 successors = evalfuncarg(context, mapping, args[0])
1078 successors = evalfuncarg(context, mapping, args[0])
1079 markers = evalfuncarg(context, mapping, args[1])
1079 markers = evalfuncarg(context, mapping, args[1])
1080
1080
1081 try:
1081 try:
1082 return obsutil.obsfateverb(successors, markers)
1082 return obsutil.obsfateverb(successors, markers)
1083 except TypeError:
1083 except TypeError:
1084 # i18n: "obsfateverb" is a keyword
1084 # i18n: "obsfateverb" is a keyword
1085 errmsg = _("obsfateverb first argument should be countable")
1085 errmsg = _("obsfateverb first argument should be countable")
1086 raise error.ParseError(errmsg)
1086 raise error.ParseError(errmsg)
1087
1087
1088 @templatefunc('relpath(path)')
1088 @templatefunc('relpath(path)')
1089 def relpath(context, mapping, args):
1089 def relpath(context, mapping, args):
1090 """Convert a repository-absolute path into a filesystem path relative to
1090 """Convert a repository-absolute path into a filesystem path relative to
1091 the current working directory."""
1091 the current working directory."""
1092 if len(args) != 1:
1092 if len(args) != 1:
1093 # i18n: "relpath" is a keyword
1093 # i18n: "relpath" is a keyword
1094 raise error.ParseError(_("relpath expects one argument"))
1094 raise error.ParseError(_("relpath expects one argument"))
1095
1095
1096 repo = context.resource(mapping, 'ctx').repo()
1096 repo = context.resource(mapping, 'ctx').repo()
1097 path = evalstring(context, mapping, args[0])
1097 path = evalstring(context, mapping, args[0])
1098 return repo.pathto(path)
1098 return repo.pathto(path)
1099
1099
1100 @templatefunc('revset(query[, formatargs...])')
1100 @templatefunc('revset(query[, formatargs...])')
1101 def revset(context, mapping, args):
1101 def revset(context, mapping, args):
1102 """Execute a revision set query. See
1102 """Execute a revision set query. See
1103 :hg:`help revset`."""
1103 :hg:`help revset`."""
1104 if not len(args) > 0:
1104 if not len(args) > 0:
1105 # i18n: "revset" is a keyword
1105 # i18n: "revset" is a keyword
1106 raise error.ParseError(_("revset expects one or more arguments"))
1106 raise error.ParseError(_("revset expects one or more arguments"))
1107
1107
1108 raw = evalstring(context, mapping, args[0])
1108 raw = evalstring(context, mapping, args[0])
1109 ctx = context.resource(mapping, 'ctx')
1109 ctx = context.resource(mapping, 'ctx')
1110 repo = ctx.repo()
1110 repo = ctx.repo()
1111
1111
1112 def query(expr):
1112 def query(expr):
1113 m = revsetmod.match(repo.ui, expr, repo=repo)
1113 m = revsetmod.match(repo.ui, expr, repo=repo)
1114 return m(repo)
1114 return m(repo)
1115
1115
1116 if len(args) > 1:
1116 if len(args) > 1:
1117 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
1117 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
1118 revs = query(revsetlang.formatspec(raw, *formatargs))
1118 revs = query(revsetlang.formatspec(raw, *formatargs))
1119 revs = list(revs)
1119 revs = list(revs)
1120 else:
1120 else:
1121 cache = context.resource(mapping, 'cache')
1121 cache = context.resource(mapping, 'cache')
1122 revsetcache = cache.setdefault("revsetcache", {})
1122 revsetcache = cache.setdefault("revsetcache", {})
1123 if raw in revsetcache:
1123 if raw in revsetcache:
1124 revs = revsetcache[raw]
1124 revs = revsetcache[raw]
1125 else:
1125 else:
1126 revs = query(raw)
1126 revs = query(raw)
1127 revs = list(revs)
1127 revs = list(revs)
1128 revsetcache[raw] = revs
1128 revsetcache[raw] = revs
1129 return templatekw.showrevslist(context, mapping, "revision", revs)
1129 return templatekw.showrevslist(context, mapping, "revision", revs)
1130
1130
1131 @templatefunc('rstdoc(text, style)')
1131 @templatefunc('rstdoc(text, style)')
1132 def rstdoc(context, mapping, args):
1132 def rstdoc(context, mapping, args):
1133 """Format reStructuredText."""
1133 """Format reStructuredText."""
1134 if len(args) != 2:
1134 if len(args) != 2:
1135 # i18n: "rstdoc" is a keyword
1135 # i18n: "rstdoc" is a keyword
1136 raise error.ParseError(_("rstdoc expects two arguments"))
1136 raise error.ParseError(_("rstdoc expects two arguments"))
1137
1137
1138 text = evalstring(context, mapping, args[0])
1138 text = evalstring(context, mapping, args[0])
1139 style = evalstring(context, mapping, args[1])
1139 style = evalstring(context, mapping, args[1])
1140
1140
1141 return minirst.format(text, style=style, keep=['verbose'])
1141 return minirst.format(text, style=style, keep=['verbose'])
1142
1142
1143 @templatefunc('separate(sep, args)', argspec='sep *args')
1143 @templatefunc('separate(sep, args)', argspec='sep *args')
1144 def separate(context, mapping, args):
1144 def separate(context, mapping, args):
1145 """Add a separator between non-empty arguments."""
1145 """Add a separator between non-empty arguments."""
1146 if 'sep' not in args:
1146 if 'sep' not in args:
1147 # i18n: "separate" is a keyword
1147 # i18n: "separate" is a keyword
1148 raise error.ParseError(_("separate expects at least one argument"))
1148 raise error.ParseError(_("separate expects at least one argument"))
1149
1149
1150 sep = evalstring(context, mapping, args['sep'])
1150 sep = evalstring(context, mapping, args['sep'])
1151 first = True
1151 first = True
1152 for arg in args['args']:
1152 for arg in args['args']:
1153 argstr = evalstring(context, mapping, arg)
1153 argstr = evalstring(context, mapping, arg)
1154 if not argstr:
1154 if not argstr:
1155 continue
1155 continue
1156 if first:
1156 if first:
1157 first = False
1157 first = False
1158 else:
1158 else:
1159 yield sep
1159 yield sep
1160 yield argstr
1160 yield argstr
1161
1161
1162 @templatefunc('shortest(node, minlength=4)')
1162 @templatefunc('shortest(node, minlength=4)')
1163 def shortest(context, mapping, args):
1163 def shortest(context, mapping, args):
1164 """Obtain the shortest representation of
1164 """Obtain the shortest representation of
1165 a node."""
1165 a node."""
1166 if not (1 <= len(args) <= 2):
1166 if not (1 <= len(args) <= 2):
1167 # i18n: "shortest" is a keyword
1167 # i18n: "shortest" is a keyword
1168 raise error.ParseError(_("shortest() expects one or two arguments"))
1168 raise error.ParseError(_("shortest() expects one or two arguments"))
1169
1169
1170 node = evalstring(context, mapping, args[0])
1170 node = evalstring(context, mapping, args[0])
1171
1171
1172 minlength = 4
1172 minlength = 4
1173 if len(args) > 1:
1173 if len(args) > 1:
1174 minlength = evalinteger(context, mapping, args[1],
1174 minlength = evalinteger(context, mapping, args[1],
1175 # i18n: "shortest" is a keyword
1175 # i18n: "shortest" is a keyword
1176 _("shortest() expects an integer minlength"))
1176 _("shortest() expects an integer minlength"))
1177
1177
1178 # _partialmatch() of filtered changelog could take O(len(repo)) time,
1178 # _partialmatch() of filtered changelog could take O(len(repo)) time,
1179 # which would be unacceptably slow. so we look for hash collision in
1179 # which would be unacceptably slow. so we look for hash collision in
1180 # unfiltered space, which means some hashes may be slightly longer.
1180 # unfiltered space, which means some hashes may be slightly longer.
1181 cl = context.resource(mapping, 'ctx')._repo.unfiltered().changelog
1181 cl = context.resource(mapping, 'ctx')._repo.unfiltered().changelog
1182 return cl.shortest(node, minlength)
1182 return cl.shortest(node, minlength)
1183
1183
1184 @templatefunc('strip(text[, chars])')
1184 @templatefunc('strip(text[, chars])')
1185 def strip(context, mapping, args):
1185 def strip(context, mapping, args):
1186 """Strip characters from a string. By default,
1186 """Strip characters from a string. By default,
1187 strips all leading and trailing whitespace."""
1187 strips all leading and trailing whitespace."""
1188 if not (1 <= len(args) <= 2):
1188 if not (1 <= len(args) <= 2):
1189 # i18n: "strip" is a keyword
1189 # i18n: "strip" is a keyword
1190 raise error.ParseError(_("strip expects one or two arguments"))
1190 raise error.ParseError(_("strip expects one or two arguments"))
1191
1191
1192 text = evalstring(context, mapping, args[0])
1192 text = evalstring(context, mapping, args[0])
1193 if len(args) == 2:
1193 if len(args) == 2:
1194 chars = evalstring(context, mapping, args[1])
1194 chars = evalstring(context, mapping, args[1])
1195 return text.strip(chars)
1195 return text.strip(chars)
1196 return text.strip()
1196 return text.strip()
1197
1197
1198 @templatefunc('sub(pattern, replacement, expression)')
1198 @templatefunc('sub(pattern, replacement, expression)')
1199 def sub(context, mapping, args):
1199 def sub(context, mapping, args):
1200 """Perform text substitution
1200 """Perform text substitution
1201 using regular expressions."""
1201 using regular expressions."""
1202 if len(args) != 3:
1202 if len(args) != 3:
1203 # i18n: "sub" is a keyword
1203 # i18n: "sub" is a keyword
1204 raise error.ParseError(_("sub expects three arguments"))
1204 raise error.ParseError(_("sub expects three arguments"))
1205
1205
1206 pat = evalstring(context, mapping, args[0])
1206 pat = evalstring(context, mapping, args[0])
1207 rpl = evalstring(context, mapping, args[1])
1207 rpl = evalstring(context, mapping, args[1])
1208 src = evalstring(context, mapping, args[2])
1208 src = evalstring(context, mapping, args[2])
1209 try:
1209 try:
1210 patre = re.compile(pat)
1210 patre = re.compile(pat)
1211 except re.error:
1211 except re.error:
1212 # i18n: "sub" is a keyword
1212 # i18n: "sub" is a keyword
1213 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
1213 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
1214 try:
1214 try:
1215 yield patre.sub(rpl, src)
1215 yield patre.sub(rpl, src)
1216 except re.error:
1216 except re.error:
1217 # i18n: "sub" is a keyword
1217 # i18n: "sub" is a keyword
1218 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
1218 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
1219
1219
1220 @templatefunc('startswith(pattern, text)')
1220 @templatefunc('startswith(pattern, text)')
1221 def startswith(context, mapping, args):
1221 def startswith(context, mapping, args):
1222 """Returns the value from the "text" argument
1222 """Returns the value from the "text" argument
1223 if it begins with the content from the "pattern" argument."""
1223 if it begins with the content from the "pattern" argument."""
1224 if len(args) != 2:
1224 if len(args) != 2:
1225 # i18n: "startswith" is a keyword
1225 # i18n: "startswith" is a keyword
1226 raise error.ParseError(_("startswith expects two arguments"))
1226 raise error.ParseError(_("startswith expects two arguments"))
1227
1227
1228 patn = evalstring(context, mapping, args[0])
1228 patn = evalstring(context, mapping, args[0])
1229 text = evalstring(context, mapping, args[1])
1229 text = evalstring(context, mapping, args[1])
1230 if text.startswith(patn):
1230 if text.startswith(patn):
1231 return text
1231 return text
1232 return ''
1232 return ''
1233
1233
1234 @templatefunc('word(number, text[, separator])')
1234 @templatefunc('word(number, text[, separator])')
1235 def word(context, mapping, args):
1235 def word(context, mapping, args):
1236 """Return the nth word from a string."""
1236 """Return the nth word from a string."""
1237 if not (2 <= len(args) <= 3):
1237 if not (2 <= len(args) <= 3):
1238 # i18n: "word" is a keyword
1238 # i18n: "word" is a keyword
1239 raise error.ParseError(_("word expects two or three arguments, got %d")
1239 raise error.ParseError(_("word expects two or three arguments, got %d")
1240 % len(args))
1240 % len(args))
1241
1241
1242 num = evalinteger(context, mapping, args[0],
1242 num = evalinteger(context, mapping, args[0],
1243 # i18n: "word" is a keyword
1243 # i18n: "word" is a keyword
1244 _("word expects an integer index"))
1244 _("word expects an integer index"))
1245 text = evalstring(context, mapping, args[1])
1245 text = evalstring(context, mapping, args[1])
1246 if len(args) == 3:
1246 if len(args) == 3:
1247 splitter = evalstring(context, mapping, args[2])
1247 splitter = evalstring(context, mapping, args[2])
1248 else:
1248 else:
1249 splitter = None
1249 splitter = None
1250
1250
1251 tokens = text.split(splitter)
1251 tokens = text.split(splitter)
1252 if num >= len(tokens) or num < -len(tokens):
1252 if num >= len(tokens) or num < -len(tokens):
1253 return ''
1253 return ''
1254 else:
1254 else:
1255 return tokens[num]
1255 return tokens[num]
1256
1256
1257 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
1257 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
1258 exprmethods = {
1258 exprmethods = {
1259 "integer": lambda e, c: (runinteger, e[1]),
1259 "integer": lambda e, c: (runinteger, e[1]),
1260 "string": lambda e, c: (runstring, e[1]),
1260 "string": lambda e, c: (runstring, e[1]),
1261 "symbol": lambda e, c: (runsymbol, e[1]),
1261 "symbol": lambda e, c: (runsymbol, e[1]),
1262 "template": buildtemplate,
1262 "template": buildtemplate,
1263 "group": lambda e, c: compileexp(e[1], c, exprmethods),
1263 "group": lambda e, c: compileexp(e[1], c, exprmethods),
1264 ".": buildmember,
1264 ".": buildmember,
1265 "|": buildfilter,
1265 "|": buildfilter,
1266 "%": buildmap,
1266 "%": buildmap,
1267 "func": buildfunc,
1267 "func": buildfunc,
1268 "keyvalue": buildkeyvaluepair,
1268 "keyvalue": buildkeyvaluepair,
1269 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
1269 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
1270 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
1270 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
1271 "negate": buildnegate,
1271 "negate": buildnegate,
1272 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
1272 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
1273 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
1273 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
1274 }
1274 }
1275
1275
1276 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
1276 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
1277 methods = exprmethods.copy()
1277 methods = exprmethods.copy()
1278 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
1278 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
1279
1279
1280 class _aliasrules(parser.basealiasrules):
1280 class _aliasrules(parser.basealiasrules):
1281 """Parsing and expansion rule set of template aliases"""
1281 """Parsing and expansion rule set of template aliases"""
1282 _section = _('template alias')
1282 _section = _('template alias')
1283 _parse = staticmethod(_parseexpr)
1283 _parse = staticmethod(_parseexpr)
1284
1284
1285 @staticmethod
1285 @staticmethod
1286 def _trygetfunc(tree):
1286 def _trygetfunc(tree):
1287 """Return (name, args) if tree is func(...) or ...|filter; otherwise
1287 """Return (name, args) if tree is func(...) or ...|filter; otherwise
1288 None"""
1288 None"""
1289 if tree[0] == 'func' and tree[1][0] == 'symbol':
1289 if tree[0] == 'func' and tree[1][0] == 'symbol':
1290 return tree[1][1], getlist(tree[2])
1290 return tree[1][1], getlist(tree[2])
1291 if tree[0] == '|' and tree[2][0] == 'symbol':
1291 if tree[0] == '|' and tree[2][0] == 'symbol':
1292 return tree[2][1], [tree[1]]
1292 return tree[2][1], [tree[1]]
1293
1293
1294 def expandaliases(tree, aliases):
1294 def expandaliases(tree, aliases):
1295 """Return new tree of aliases are expanded"""
1295 """Return new tree of aliases are expanded"""
1296 aliasmap = _aliasrules.buildmap(aliases)
1296 aliasmap = _aliasrules.buildmap(aliases)
1297 return _aliasrules.expand(aliasmap, tree)
1297 return _aliasrules.expand(aliasmap, tree)
1298
1298
1299 # template engine
1299 # template engine
1300
1300
1301 stringify = templatefilters.stringify
1301 stringify = templatefilters.stringify
1302
1302
1303 def _flatten(thing):
1303 def _flatten(thing):
1304 '''yield a single stream from a possibly nested set of iterators'''
1304 '''yield a single stream from a possibly nested set of iterators'''
1305 thing = templatekw.unwraphybrid(thing)
1305 thing = templatekw.unwraphybrid(thing)
1306 if isinstance(thing, bytes):
1306 if isinstance(thing, bytes):
1307 yield thing
1307 yield thing
1308 elif isinstance(thing, str):
1308 elif isinstance(thing, str):
1309 # We can only hit this on Python 3, and it's here to guard
1309 # We can only hit this on Python 3, and it's here to guard
1310 # against infinite recursion.
1310 # against infinite recursion.
1311 raise error.ProgrammingError('Mercurial IO including templates is done'
1311 raise error.ProgrammingError('Mercurial IO including templates is done'
1312 ' with bytes, not strings')
1312 ' with bytes, not strings')
1313 elif thing is None:
1313 elif thing is None:
1314 pass
1314 pass
1315 elif not util.safehasattr(thing, '__iter__'):
1315 elif not util.safehasattr(thing, '__iter__'):
1316 yield pycompat.bytestr(thing)
1316 yield pycompat.bytestr(thing)
1317 else:
1317 else:
1318 for i in thing:
1318 for i in thing:
1319 i = templatekw.unwraphybrid(i)
1319 i = templatekw.unwraphybrid(i)
1320 if isinstance(i, bytes):
1320 if isinstance(i, bytes):
1321 yield i
1321 yield i
1322 elif i is None:
1322 elif i is None:
1323 pass
1323 pass
1324 elif not util.safehasattr(i, '__iter__'):
1324 elif not util.safehasattr(i, '__iter__'):
1325 yield pycompat.bytestr(i)
1325 yield pycompat.bytestr(i)
1326 else:
1326 else:
1327 for j in _flatten(i):
1327 for j in _flatten(i):
1328 yield j
1328 yield j
1329
1329
1330 def unquotestring(s):
1330 def unquotestring(s):
1331 '''unwrap quotes if any; otherwise returns unmodified string'''
1331 '''unwrap quotes if any; otherwise returns unmodified string'''
1332 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
1332 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
1333 return s
1333 return s
1334 return s[1:-1]
1334 return s[1:-1]
1335
1335
1336 class engine(object):
1336 class engine(object):
1337 '''template expansion engine.
1337 '''template expansion engine.
1338
1338
1339 template expansion works like this. a map file contains key=value
1339 template expansion works like this. a map file contains key=value
1340 pairs. if value is quoted, it is treated as string. otherwise, it
1340 pairs. if value is quoted, it is treated as string. otherwise, it
1341 is treated as name of template file.
1341 is treated as name of template file.
1342
1342
1343 templater is asked to expand a key in map. it looks up key, and
1343 templater is asked to expand a key in map. it looks up key, and
1344 looks for strings like this: {foo}. it expands {foo} by looking up
1344 looks for strings like this: {foo}. it expands {foo} by looking up
1345 foo in map, and substituting it. expansion is recursive: it stops
1345 foo in map, and substituting it. expansion is recursive: it stops
1346 when there is no more {foo} to replace.
1346 when there is no more {foo} to replace.
1347
1347
1348 expansion also allows formatting and filtering.
1348 expansion also allows formatting and filtering.
1349
1349
1350 format uses key to expand each item in list. syntax is
1350 format uses key to expand each item in list. syntax is
1351 {key%format}.
1351 {key%format}.
1352
1352
1353 filter uses function to transform value. syntax is
1353 filter uses function to transform value. syntax is
1354 {key|filter1|filter2|...}.'''
1354 {key|filter1|filter2|...}.'''
1355
1355
1356 def __init__(self, loader, filters=None, defaults=None, resources=None,
1356 def __init__(self, loader, filters=None, defaults=None, resources=None,
1357 aliases=()):
1357 aliases=()):
1358 self._loader = loader
1358 self._loader = loader
1359 if filters is None:
1359 if filters is None:
1360 filters = {}
1360 filters = {}
1361 self._filters = filters
1361 self._filters = filters
1362 if defaults is None:
1362 if defaults is None:
1363 defaults = {}
1363 defaults = {}
1364 if resources is None:
1364 if resources is None:
1365 resources = {}
1365 resources = {}
1366 self._defaults = defaults
1366 self._defaults = defaults
1367 self._resources = resources
1367 self._resources = resources
1368 self._aliasmap = _aliasrules.buildmap(aliases)
1368 self._aliasmap = _aliasrules.buildmap(aliases)
1369 self._cache = {} # key: (func, data)
1369 self._cache = {} # key: (func, data)
1370
1370
1371 def symbol(self, mapping, key):
1371 def symbol(self, mapping, key):
1372 """Resolve symbol to value or function; None if nothing found"""
1372 """Resolve symbol to value or function; None if nothing found"""
1373 v = None
1373 v = None
1374 if key not in self._resources:
1374 if key not in self._resources:
1375 v = mapping.get(key)
1375 v = mapping.get(key)
1376 if v is None:
1376 if v is None:
1377 v = self._defaults.get(key)
1377 v = self._defaults.get(key)
1378 return v
1378 return v
1379
1379
1380 def resource(self, mapping, key):
1380 def resource(self, mapping, key):
1381 """Return internal data (e.g. cache) used for keyword/function
1381 """Return internal data (e.g. cache) used for keyword/function
1382 evaluation"""
1382 evaluation"""
1383 v = None
1383 v = None
1384 if key in self._resources:
1384 if key in self._resources:
1385 v = mapping.get(key)
1385 v = mapping.get(key)
1386 if v is None:
1386 if v is None:
1387 v = self._resources.get(key)
1387 v = self._resources.get(key)
1388 if v is None:
1388 if v is None:
1389 raise ResourceUnavailable(_('template resource not available: %s')
1389 raise ResourceUnavailable(_('template resource not available: %s')
1390 % key)
1390 % key)
1391 return v
1391 return v
1392
1392
1393 def _load(self, t):
1393 def _load(self, t):
1394 '''load, parse, and cache a template'''
1394 '''load, parse, and cache a template'''
1395 if t not in self._cache:
1395 if t not in self._cache:
1396 # put poison to cut recursion while compiling 't'
1396 # put poison to cut recursion while compiling 't'
1397 self._cache[t] = (_runrecursivesymbol, t)
1397 self._cache[t] = (_runrecursivesymbol, t)
1398 try:
1398 try:
1399 x = parse(self._loader(t))
1399 x = parse(self._loader(t))
1400 if self._aliasmap:
1400 if self._aliasmap:
1401 x = _aliasrules.expand(self._aliasmap, x)
1401 x = _aliasrules.expand(self._aliasmap, x)
1402 self._cache[t] = compileexp(x, self, methods)
1402 self._cache[t] = compileexp(x, self, methods)
1403 except: # re-raises
1403 except: # re-raises
1404 del self._cache[t]
1404 del self._cache[t]
1405 raise
1405 raise
1406 return self._cache[t]
1406 return self._cache[t]
1407
1407
1408 def process(self, t, mapping):
1408 def process(self, t, mapping):
1409 '''Perform expansion. t is name of map element to expand.
1409 '''Perform expansion. t is name of map element to expand.
1410 mapping contains added elements for use during expansion. Is a
1410 mapping contains added elements for use during expansion. Is a
1411 generator.'''
1411 generator.'''
1412 func, data = self._load(t)
1412 func, data = self._load(t)
1413 return _flatten(func(self, mapping, data))
1413 return _flatten(func(self, mapping, data))
1414
1414
1415 engines = {'default': engine}
1415 engines = {'default': engine}
1416
1416
1417 def stylelist():
1417 def stylelist():
1418 paths = templatepaths()
1418 paths = templatepaths()
1419 if not paths:
1419 if not paths:
1420 return _('no templates found, try `hg debuginstall` for more info')
1420 return _('no templates found, try `hg debuginstall` for more info')
1421 dirlist = os.listdir(paths[0])
1421 dirlist = os.listdir(paths[0])
1422 stylelist = []
1422 stylelist = []
1423 for file in dirlist:
1423 for file in dirlist:
1424 split = file.split(".")
1424 split = file.split(".")
1425 if split[-1] in ('orig', 'rej'):
1425 if split[-1] in ('orig', 'rej'):
1426 continue
1426 continue
1427 if split[0] == "map-cmdline":
1427 if split[0] == "map-cmdline":
1428 stylelist.append(split[1])
1428 stylelist.append(split[1])
1429 return ", ".join(sorted(stylelist))
1429 return ", ".join(sorted(stylelist))
1430
1430
1431 def _readmapfile(mapfile):
1431 def _readmapfile(mapfile):
1432 """Load template elements from the given map file"""
1432 """Load template elements from the given map file"""
1433 if not os.path.exists(mapfile):
1433 if not os.path.exists(mapfile):
1434 raise error.Abort(_("style '%s' not found") % mapfile,
1434 raise error.Abort(_("style '%s' not found") % mapfile,
1435 hint=_("available styles: %s") % stylelist())
1435 hint=_("available styles: %s") % stylelist())
1436
1436
1437 base = os.path.dirname(mapfile)
1437 base = os.path.dirname(mapfile)
1438 conf = config.config(includepaths=templatepaths())
1438 conf = config.config(includepaths=templatepaths())
1439 conf.read(mapfile, remap={'': 'templates'})
1439 conf.read(mapfile, remap={'': 'templates'})
1440
1440
1441 cache = {}
1441 cache = {}
1442 tmap = {}
1442 tmap = {}
1443 aliases = []
1443 aliases = []
1444
1444
1445 val = conf.get('templates', '__base__')
1445 val = conf.get('templates', '__base__')
1446 if val and val[0] not in "'\"":
1446 if val and val[0] not in "'\"":
1447 # treat as a pointer to a base class for this style
1447 # treat as a pointer to a base class for this style
1448 path = util.normpath(os.path.join(base, val))
1448 path = util.normpath(os.path.join(base, val))
1449
1449
1450 # fallback check in template paths
1450 # fallback check in template paths
1451 if not os.path.exists(path):
1451 if not os.path.exists(path):
1452 for p in templatepaths():
1452 for p in templatepaths():
1453 p2 = util.normpath(os.path.join(p, val))
1453 p2 = util.normpath(os.path.join(p, val))
1454 if os.path.isfile(p2):
1454 if os.path.isfile(p2):
1455 path = p2
1455 path = p2
1456 break
1456 break
1457 p3 = util.normpath(os.path.join(p2, "map"))
1457 p3 = util.normpath(os.path.join(p2, "map"))
1458 if os.path.isfile(p3):
1458 if os.path.isfile(p3):
1459 path = p3
1459 path = p3
1460 break
1460 break
1461
1461
1462 cache, tmap, aliases = _readmapfile(path)
1462 cache, tmap, aliases = _readmapfile(path)
1463
1463
1464 for key, val in conf['templates'].items():
1464 for key, val in conf['templates'].items():
1465 if not val:
1465 if not val:
1466 raise error.ParseError(_('missing value'),
1466 raise error.ParseError(_('missing value'),
1467 conf.source('templates', key))
1467 conf.source('templates', key))
1468 if val[0] in "'\"":
1468 if val[0] in "'\"":
1469 if val[0] != val[-1]:
1469 if val[0] != val[-1]:
1470 raise error.ParseError(_('unmatched quotes'),
1470 raise error.ParseError(_('unmatched quotes'),
1471 conf.source('templates', key))
1471 conf.source('templates', key))
1472 cache[key] = unquotestring(val)
1472 cache[key] = unquotestring(val)
1473 elif key != '__base__':
1473 elif key != '__base__':
1474 val = 'default', val
1474 val = 'default', val
1475 if ':' in val[1]:
1475 if ':' in val[1]:
1476 val = val[1].split(':', 1)
1476 val = val[1].split(':', 1)
1477 tmap[key] = val[0], os.path.join(base, val[1])
1477 tmap[key] = val[0], os.path.join(base, val[1])
1478 aliases.extend(conf['templatealias'].items())
1478 aliases.extend(conf['templatealias'].items())
1479 return cache, tmap, aliases
1479 return cache, tmap, aliases
1480
1480
1481 class templater(object):
1481 class templater(object):
1482
1482
1483 def __init__(self, filters=None, defaults=None, resources=None,
1483 def __init__(self, filters=None, defaults=None, resources=None,
1484 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
1484 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
1485 """Create template engine optionally with preloaded template fragments
1485 """Create template engine optionally with preloaded template fragments
1486
1486
1487 - ``filters``: a dict of functions to transform a value into another.
1487 - ``filters``: a dict of functions to transform a value into another.
1488 - ``defaults``: a dict of symbol values/functions; may be overridden
1488 - ``defaults``: a dict of symbol values/functions; may be overridden
1489 by a ``mapping`` dict.
1489 by a ``mapping`` dict.
1490 - ``resources``: a dict of internal data (e.g. cache), inaccessible
1490 - ``resources``: a dict of internal data (e.g. cache), inaccessible
1491 from user template; may be overridden by a ``mapping`` dict.
1491 from user template; may be overridden by a ``mapping`` dict.
1492 - ``cache``: a dict of preloaded template fragments.
1492 - ``cache``: a dict of preloaded template fragments.
1493 - ``aliases``: a list of alias (name, replacement) pairs.
1493 - ``aliases``: a list of alias (name, replacement) pairs.
1494
1494
1495 self.cache may be updated later to register additional template
1495 self.cache may be updated later to register additional template
1496 fragments.
1496 fragments.
1497 """
1497 """
1498 if filters is None:
1498 if filters is None:
1499 filters = {}
1499 filters = {}
1500 if defaults is None:
1500 if defaults is None:
1501 defaults = {}
1501 defaults = {}
1502 if resources is None:
1502 if resources is None:
1503 resources = {}
1503 resources = {}
1504 if cache is None:
1504 if cache is None:
1505 cache = {}
1505 cache = {}
1506 self.cache = cache.copy()
1506 self.cache = cache.copy()
1507 self.map = {}
1507 self.map = {}
1508 self.filters = templatefilters.filters.copy()
1508 self.filters = templatefilters.filters.copy()
1509 self.filters.update(filters)
1509 self.filters.update(filters)
1510 self.defaults = defaults
1510 self.defaults = defaults
1511 self._resources = {'templ': self}
1511 self._resources = {'templ': self}
1512 self._resources.update(resources)
1512 self._resources.update(resources)
1513 self._aliases = aliases
1513 self._aliases = aliases
1514 self.minchunk, self.maxchunk = minchunk, maxchunk
1514 self.minchunk, self.maxchunk = minchunk, maxchunk
1515 self.ecache = {}
1515 self.ecache = {}
1516
1516
1517 @classmethod
1517 @classmethod
1518 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
1518 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
1519 cache=None, minchunk=1024, maxchunk=65536):
1519 cache=None, minchunk=1024, maxchunk=65536):
1520 """Create templater from the specified map file"""
1520 """Create templater from the specified map file"""
1521 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
1521 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
1522 cache, tmap, aliases = _readmapfile(mapfile)
1522 cache, tmap, aliases = _readmapfile(mapfile)
1523 t.cache.update(cache)
1523 t.cache.update(cache)
1524 t.map = tmap
1524 t.map = tmap
1525 t._aliases = aliases
1525 t._aliases = aliases
1526 return t
1526 return t
1527
1527
1528 def __contains__(self, key):
1528 def __contains__(self, key):
1529 return key in self.cache or key in self.map
1529 return key in self.cache or key in self.map
1530
1530
1531 def load(self, t):
1531 def load(self, t):
1532 '''Get the template for the given template name. Use a local cache.'''
1532 '''Get the template for the given template name. Use a local cache.'''
1533 if t not in self.cache:
1533 if t not in self.cache:
1534 try:
1534 try:
1535 self.cache[t] = util.readfile(self.map[t][1])
1535 self.cache[t] = util.readfile(self.map[t][1])
1536 except KeyError as inst:
1536 except KeyError as inst:
1537 raise TemplateNotFound(_('"%s" not in template map') %
1537 raise TemplateNotFound(_('"%s" not in template map') %
1538 inst.args[0])
1538 inst.args[0])
1539 except IOError as inst:
1539 except IOError as inst:
1540 reason = (_('template file %s: %s')
1540 reason = (_('template file %s: %s')
1541 % (self.map[t][1], util.forcebytestr(inst.args[1])))
1541 % (self.map[t][1], util.forcebytestr(inst.args[1])))
1542 raise IOError(inst.args[0], encoding.strfromlocal(reason))
1542 raise IOError(inst.args[0], encoding.strfromlocal(reason))
1543 return self.cache[t]
1543 return self.cache[t]
1544
1544
1545 def render(self, mapping):
1545 def render(self, mapping):
1546 """Render the default unnamed template and return result as string"""
1546 """Render the default unnamed template and return result as string"""
1547 mapping = pycompat.strkwargs(mapping)
1547 mapping = pycompat.strkwargs(mapping)
1548 return stringify(self('', **mapping))
1548 return stringify(self('', **mapping))
1549
1549
1550 def __call__(self, t, **mapping):
1550 def __call__(self, t, **mapping):
1551 mapping = pycompat.byteskwargs(mapping)
1551 mapping = pycompat.byteskwargs(mapping)
1552 ttype = t in self.map and self.map[t][0] or 'default'
1552 ttype = t in self.map and self.map[t][0] or 'default'
1553 if ttype not in self.ecache:
1553 if ttype not in self.ecache:
1554 try:
1554 try:
1555 ecls = engines[ttype]
1555 ecls = engines[ttype]
1556 except KeyError:
1556 except KeyError:
1557 raise error.Abort(_('invalid template engine: %s') % ttype)
1557 raise error.Abort(_('invalid template engine: %s') % ttype)
1558 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
1558 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
1559 self._resources, self._aliases)
1559 self._resources, self._aliases)
1560 proc = self.ecache[ttype]
1560 proc = self.ecache[ttype]
1561
1561
1562 stream = proc.process(t, mapping)
1562 stream = proc.process(t, mapping)
1563 if self.minchunk:
1563 if self.minchunk:
1564 stream = util.increasingchunks(stream, min=self.minchunk,
1564 stream = util.increasingchunks(stream, min=self.minchunk,
1565 max=self.maxchunk)
1565 max=self.maxchunk)
1566 return stream
1566 return stream
1567
1567
1568 def templatepaths():
1568 def templatepaths():
1569 '''return locations used for template files.'''
1569 '''return locations used for template files.'''
1570 pathsrel = ['templates']
1570 pathsrel = ['templates']
1571 paths = [os.path.normpath(os.path.join(util.datapath, f))
1571 paths = [os.path.normpath(os.path.join(util.datapath, f))
1572 for f in pathsrel]
1572 for f in pathsrel]
1573 return [p for p in paths if os.path.isdir(p)]
1573 return [p for p in paths if os.path.isdir(p)]
1574
1574
1575 def templatepath(name):
1575 def templatepath(name):
1576 '''return location of template file. returns None if not found.'''
1576 '''return location of template file. returns None if not found.'''
1577 for p in templatepaths():
1577 for p in templatepaths():
1578 f = os.path.join(p, name)
1578 f = os.path.join(p, name)
1579 if os.path.exists(f):
1579 if os.path.exists(f):
1580 return f
1580 return f
1581 return None
1581 return None
1582
1582
1583 def stylemap(styles, paths=None):
1583 def stylemap(styles, paths=None):
1584 """Return path to mapfile for a given style.
1584 """Return path to mapfile for a given style.
1585
1585
1586 Searches mapfile in the following locations:
1586 Searches mapfile in the following locations:
1587 1. templatepath/style/map
1587 1. templatepath/style/map
1588 2. templatepath/map-style
1588 2. templatepath/map-style
1589 3. templatepath/map
1589 3. templatepath/map
1590 """
1590 """
1591
1591
1592 if paths is None:
1592 if paths is None:
1593 paths = templatepaths()
1593 paths = templatepaths()
1594 elif isinstance(paths, bytes):
1594 elif isinstance(paths, bytes):
1595 paths = [paths]
1595 paths = [paths]
1596
1596
1597 if isinstance(styles, bytes):
1597 if isinstance(styles, bytes):
1598 styles = [styles]
1598 styles = [styles]
1599
1599
1600 for style in styles:
1600 for style in styles:
1601 # only plain name is allowed to honor template paths
1601 # only plain name is allowed to honor template paths
1602 if (not style
1602 if (not style
1603 or style in (os.curdir, os.pardir)
1603 or style in (os.curdir, pycompat.ospardir)
1604 or pycompat.ossep in style
1604 or pycompat.ossep in style
1605 or pycompat.osaltsep and pycompat.osaltsep in style):
1605 or pycompat.osaltsep and pycompat.osaltsep in style):
1606 continue
1606 continue
1607 locations = [os.path.join(style, 'map'), 'map-' + style]
1607 locations = [os.path.join(style, 'map'), 'map-' + style]
1608 locations.append('map')
1608 locations.append('map')
1609
1609
1610 for path in paths:
1610 for path in paths:
1611 for location in locations:
1611 for location in locations:
1612 mapfile = os.path.join(path, location)
1612 mapfile = os.path.join(path, location)
1613 if os.path.isfile(mapfile):
1613 if os.path.isfile(mapfile):
1614 return style, mapfile
1614 return style, mapfile
1615
1615
1616 raise RuntimeError("No hgweb templates found in %r" % paths)
1616 raise RuntimeError("No hgweb templates found in %r" % paths)
1617
1617
1618 def loadfunction(ui, extname, registrarobj):
1618 def loadfunction(ui, extname, registrarobj):
1619 """Load template function from specified registrarobj
1619 """Load template function from specified registrarobj
1620 """
1620 """
1621 for name, func in registrarobj._table.iteritems():
1621 for name, func in registrarobj._table.iteritems():
1622 funcs[name] = func
1622 funcs[name] = func
1623
1623
1624 # tell hggettext to extract docstrings from these functions:
1624 # tell hggettext to extract docstrings from these functions:
1625 i18nfunctions = funcs.values()
1625 i18nfunctions = funcs.values()
General Comments 0
You need to be logged in to leave comments. Login now