##// END OF EJS Templates
templater: move hybrid class and functions to templateutil module...
Yuya Nishihara -
r36939:32f9b7e3 default
parent child Browse files
Show More
@@ -1,391 +1,391 b''
1 # lfs - hash-preserving large file support using Git-LFS protocol
1 # lfs - hash-preserving large file support using Git-LFS protocol
2 #
2 #
3 # Copyright 2017 Facebook, Inc.
3 # Copyright 2017 Facebook, Inc.
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 """lfs - large file support (EXPERIMENTAL)
8 """lfs - large file support (EXPERIMENTAL)
9
9
10 This extension allows large files to be tracked outside of the normal
10 This extension allows large files to be tracked outside of the normal
11 repository storage and stored on a centralized server, similar to the
11 repository storage and stored on a centralized server, similar to the
12 ``largefiles`` extension. The ``git-lfs`` protocol is used when
12 ``largefiles`` extension. The ``git-lfs`` protocol is used when
13 communicating with the server, so existing git infrastructure can be
13 communicating with the server, so existing git infrastructure can be
14 harnessed. Even though the files are stored outside of the repository,
14 harnessed. Even though the files are stored outside of the repository,
15 they are still integrity checked in the same manner as normal files.
15 they are still integrity checked in the same manner as normal files.
16
16
17 The files stored outside of the repository are downloaded on demand,
17 The files stored outside of the repository are downloaded on demand,
18 which reduces the time to clone, and possibly the local disk usage.
18 which reduces the time to clone, and possibly the local disk usage.
19 This changes fundamental workflows in a DVCS, so careful thought
19 This changes fundamental workflows in a DVCS, so careful thought
20 should be given before deploying it. :hg:`convert` can be used to
20 should be given before deploying it. :hg:`convert` can be used to
21 convert LFS repositories to normal repositories that no longer
21 convert LFS repositories to normal repositories that no longer
22 require this extension, and do so without changing the commit hashes.
22 require this extension, and do so without changing the commit hashes.
23 This allows the extension to be disabled if the centralized workflow
23 This allows the extension to be disabled if the centralized workflow
24 becomes burdensome. However, the pre and post convert clones will
24 becomes burdensome. However, the pre and post convert clones will
25 not be able to communicate with each other unless the extension is
25 not be able to communicate with each other unless the extension is
26 enabled on both.
26 enabled on both.
27
27
28 To start a new repository, or to add LFS files to an existing one, just
28 To start a new repository, or to add LFS files to an existing one, just
29 create an ``.hglfs`` file as described below in the root directory of
29 create an ``.hglfs`` file as described below in the root directory of
30 the repository. Typically, this file should be put under version
30 the repository. Typically, this file should be put under version
31 control, so that the settings will propagate to other repositories with
31 control, so that the settings will propagate to other repositories with
32 push and pull. During any commit, Mercurial will consult this file to
32 push and pull. During any commit, Mercurial will consult this file to
33 determine if an added or modified file should be stored externally. The
33 determine if an added or modified file should be stored externally. The
34 type of storage depends on the characteristics of the file at each
34 type of storage depends on the characteristics of the file at each
35 commit. A file that is near a size threshold may switch back and forth
35 commit. A file that is near a size threshold may switch back and forth
36 between LFS and normal storage, as needed.
36 between LFS and normal storage, as needed.
37
37
38 Alternately, both normal repositories and largefile controlled
38 Alternately, both normal repositories and largefile controlled
39 repositories can be converted to LFS by using :hg:`convert` and the
39 repositories can be converted to LFS by using :hg:`convert` and the
40 ``lfs.track`` config option described below. The ``.hglfs`` file
40 ``lfs.track`` config option described below. The ``.hglfs`` file
41 should then be created and added, to control subsequent LFS selection.
41 should then be created and added, to control subsequent LFS selection.
42 The hashes are also unchanged in this case. The LFS and non-LFS
42 The hashes are also unchanged in this case. The LFS and non-LFS
43 repositories can be distinguished because the LFS repository will
43 repositories can be distinguished because the LFS repository will
44 abort any command if this extension is disabled.
44 abort any command if this extension is disabled.
45
45
46 Committed LFS files are held locally, until the repository is pushed.
46 Committed LFS files are held locally, until the repository is pushed.
47 Prior to pushing the normal repository data, the LFS files that are
47 Prior to pushing the normal repository data, the LFS files that are
48 tracked by the outgoing commits are automatically uploaded to the
48 tracked by the outgoing commits are automatically uploaded to the
49 configured central server. No LFS files are transferred on
49 configured central server. No LFS files are transferred on
50 :hg:`pull` or :hg:`clone`. Instead, the files are downloaded on
50 :hg:`pull` or :hg:`clone`. Instead, the files are downloaded on
51 demand as they need to be read, if a cached copy cannot be found
51 demand as they need to be read, if a cached copy cannot be found
52 locally. Both committing and downloading an LFS file will link the
52 locally. Both committing and downloading an LFS file will link the
53 file to a usercache, to speed up future access. See the `usercache`
53 file to a usercache, to speed up future access. See the `usercache`
54 config setting described below.
54 config setting described below.
55
55
56 .hglfs::
56 .hglfs::
57
57
58 The extension reads its configuration from a versioned ``.hglfs``
58 The extension reads its configuration from a versioned ``.hglfs``
59 configuration file found in the root of the working directory. The
59 configuration file found in the root of the working directory. The
60 ``.hglfs`` file uses the same syntax as all other Mercurial
60 ``.hglfs`` file uses the same syntax as all other Mercurial
61 configuration files. It uses a single section, ``[track]``.
61 configuration files. It uses a single section, ``[track]``.
62
62
63 The ``[track]`` section specifies which files are stored as LFS (or
63 The ``[track]`` section specifies which files are stored as LFS (or
64 not). Each line is keyed by a file pattern, with a predicate value.
64 not). Each line is keyed by a file pattern, with a predicate value.
65 The first file pattern match is used, so put more specific patterns
65 The first file pattern match is used, so put more specific patterns
66 first. The available predicates are ``all()``, ``none()``, and
66 first. The available predicates are ``all()``, ``none()``, and
67 ``size()``. See "hg help filesets.size" for the latter.
67 ``size()``. See "hg help filesets.size" for the latter.
68
68
69 Example versioned ``.hglfs`` file::
69 Example versioned ``.hglfs`` file::
70
70
71 [track]
71 [track]
72 # No Makefile or python file, anywhere, will be LFS
72 # No Makefile or python file, anywhere, will be LFS
73 **Makefile = none()
73 **Makefile = none()
74 **.py = none()
74 **.py = none()
75
75
76 **.zip = all()
76 **.zip = all()
77 **.exe = size(">1MB")
77 **.exe = size(">1MB")
78
78
79 # Catchall for everything not matched above
79 # Catchall for everything not matched above
80 ** = size(">10MB")
80 ** = size(">10MB")
81
81
82 Configs::
82 Configs::
83
83
84 [lfs]
84 [lfs]
85 # Remote endpoint. Multiple protocols are supported:
85 # Remote endpoint. Multiple protocols are supported:
86 # - http(s)://user:pass@example.com/path
86 # - http(s)://user:pass@example.com/path
87 # git-lfs endpoint
87 # git-lfs endpoint
88 # - file:///tmp/path
88 # - file:///tmp/path
89 # local filesystem, usually for testing
89 # local filesystem, usually for testing
90 # if unset, lfs will prompt setting this when it must use this value.
90 # if unset, lfs will prompt setting this when it must use this value.
91 # (default: unset)
91 # (default: unset)
92 url = https://example.com/repo.git/info/lfs
92 url = https://example.com/repo.git/info/lfs
93
93
94 # Which files to track in LFS. Path tests are "**.extname" for file
94 # Which files to track in LFS. Path tests are "**.extname" for file
95 # extensions, and "path:under/some/directory" for path prefix. Both
95 # extensions, and "path:under/some/directory" for path prefix. Both
96 # are relative to the repository root.
96 # are relative to the repository root.
97 # File size can be tested with the "size()" fileset, and tests can be
97 # File size can be tested with the "size()" fileset, and tests can be
98 # joined with fileset operators. (See "hg help filesets.operators".)
98 # joined with fileset operators. (See "hg help filesets.operators".)
99 #
99 #
100 # Some examples:
100 # Some examples:
101 # - all() # everything
101 # - all() # everything
102 # - none() # nothing
102 # - none() # nothing
103 # - size(">20MB") # larger than 20MB
103 # - size(">20MB") # larger than 20MB
104 # - !**.txt # anything not a *.txt file
104 # - !**.txt # anything not a *.txt file
105 # - **.zip | **.tar.gz | **.7z # some types of compressed files
105 # - **.zip | **.tar.gz | **.7z # some types of compressed files
106 # - path:bin # files under "bin" in the project root
106 # - path:bin # files under "bin" in the project root
107 # - (**.php & size(">2MB")) | (**.js & size(">5MB")) | **.tar.gz
107 # - (**.php & size(">2MB")) | (**.js & size(">5MB")) | **.tar.gz
108 # | (path:bin & !path:/bin/README) | size(">1GB")
108 # | (path:bin & !path:/bin/README) | size(">1GB")
109 # (default: none())
109 # (default: none())
110 #
110 #
111 # This is ignored if there is a tracked '.hglfs' file, and this setting
111 # This is ignored if there is a tracked '.hglfs' file, and this setting
112 # will eventually be deprecated and removed.
112 # will eventually be deprecated and removed.
113 track = size(">10M")
113 track = size(">10M")
114
114
115 # how many times to retry before giving up on transferring an object
115 # how many times to retry before giving up on transferring an object
116 retry = 5
116 retry = 5
117
117
118 # the local directory to store lfs files for sharing across local clones.
118 # the local directory to store lfs files for sharing across local clones.
119 # If not set, the cache is located in an OS specific cache location.
119 # If not set, the cache is located in an OS specific cache location.
120 usercache = /path/to/global/cache
120 usercache = /path/to/global/cache
121 """
121 """
122
122
123 from __future__ import absolute_import
123 from __future__ import absolute_import
124
124
125 from mercurial.i18n import _
125 from mercurial.i18n import _
126
126
127 from mercurial import (
127 from mercurial import (
128 bundle2,
128 bundle2,
129 changegroup,
129 changegroup,
130 cmdutil,
130 cmdutil,
131 config,
131 config,
132 context,
132 context,
133 error,
133 error,
134 exchange,
134 exchange,
135 extensions,
135 extensions,
136 filelog,
136 filelog,
137 fileset,
137 fileset,
138 hg,
138 hg,
139 localrepo,
139 localrepo,
140 minifileset,
140 minifileset,
141 node,
141 node,
142 pycompat,
142 pycompat,
143 registrar,
143 registrar,
144 revlog,
144 revlog,
145 scmutil,
145 scmutil,
146 templatekw,
146 templateutil,
147 upgrade,
147 upgrade,
148 util,
148 util,
149 vfs as vfsmod,
149 vfs as vfsmod,
150 wireproto,
150 wireproto,
151 )
151 )
152
152
153 from . import (
153 from . import (
154 blobstore,
154 blobstore,
155 wrapper,
155 wrapper,
156 )
156 )
157
157
158 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
158 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
159 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
159 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
160 # be specifying the version(s) of Mercurial they are tested with, or
160 # be specifying the version(s) of Mercurial they are tested with, or
161 # leave the attribute unspecified.
161 # leave the attribute unspecified.
162 testedwith = 'ships-with-hg-core'
162 testedwith = 'ships-with-hg-core'
163
163
164 configtable = {}
164 configtable = {}
165 configitem = registrar.configitem(configtable)
165 configitem = registrar.configitem(configtable)
166
166
167 configitem('experimental', 'lfs.user-agent',
167 configitem('experimental', 'lfs.user-agent',
168 default=None,
168 default=None,
169 )
169 )
170 configitem('experimental', 'lfs.worker-enable',
170 configitem('experimental', 'lfs.worker-enable',
171 default=False,
171 default=False,
172 )
172 )
173
173
174 configitem('lfs', 'url',
174 configitem('lfs', 'url',
175 default=None,
175 default=None,
176 )
176 )
177 configitem('lfs', 'usercache',
177 configitem('lfs', 'usercache',
178 default=None,
178 default=None,
179 )
179 )
180 # Deprecated
180 # Deprecated
181 configitem('lfs', 'threshold',
181 configitem('lfs', 'threshold',
182 default=None,
182 default=None,
183 )
183 )
184 configitem('lfs', 'track',
184 configitem('lfs', 'track',
185 default='none()',
185 default='none()',
186 )
186 )
187 configitem('lfs', 'retry',
187 configitem('lfs', 'retry',
188 default=5,
188 default=5,
189 )
189 )
190
190
191 cmdtable = {}
191 cmdtable = {}
192 command = registrar.command(cmdtable)
192 command = registrar.command(cmdtable)
193
193
194 templatekeyword = registrar.templatekeyword()
194 templatekeyword = registrar.templatekeyword()
195 filesetpredicate = registrar.filesetpredicate()
195 filesetpredicate = registrar.filesetpredicate()
196
196
197 def featuresetup(ui, supported):
197 def featuresetup(ui, supported):
198 # don't die on seeing a repo with the lfs requirement
198 # don't die on seeing a repo with the lfs requirement
199 supported |= {'lfs'}
199 supported |= {'lfs'}
200
200
201 def uisetup(ui):
201 def uisetup(ui):
202 localrepo.localrepository.featuresetupfuncs.add(featuresetup)
202 localrepo.localrepository.featuresetupfuncs.add(featuresetup)
203
203
204 def reposetup(ui, repo):
204 def reposetup(ui, repo):
205 # Nothing to do with a remote repo
205 # Nothing to do with a remote repo
206 if not repo.local():
206 if not repo.local():
207 return
207 return
208
208
209 repo.svfs.lfslocalblobstore = blobstore.local(repo)
209 repo.svfs.lfslocalblobstore = blobstore.local(repo)
210 repo.svfs.lfsremoteblobstore = blobstore.remote(repo)
210 repo.svfs.lfsremoteblobstore = blobstore.remote(repo)
211
211
212 class lfsrepo(repo.__class__):
212 class lfsrepo(repo.__class__):
213 @localrepo.unfilteredmethod
213 @localrepo.unfilteredmethod
214 def commitctx(self, ctx, error=False):
214 def commitctx(self, ctx, error=False):
215 repo.svfs.options['lfstrack'] = _trackedmatcher(self)
215 repo.svfs.options['lfstrack'] = _trackedmatcher(self)
216 return super(lfsrepo, self).commitctx(ctx, error)
216 return super(lfsrepo, self).commitctx(ctx, error)
217
217
218 repo.__class__ = lfsrepo
218 repo.__class__ = lfsrepo
219
219
220 if 'lfs' not in repo.requirements:
220 if 'lfs' not in repo.requirements:
221 def checkrequireslfs(ui, repo, **kwargs):
221 def checkrequireslfs(ui, repo, **kwargs):
222 if 'lfs' not in repo.requirements:
222 if 'lfs' not in repo.requirements:
223 last = kwargs.get(r'node_last')
223 last = kwargs.get(r'node_last')
224 _bin = node.bin
224 _bin = node.bin
225 if last:
225 if last:
226 s = repo.set('%n:%n', _bin(kwargs[r'node']), _bin(last))
226 s = repo.set('%n:%n', _bin(kwargs[r'node']), _bin(last))
227 else:
227 else:
228 s = repo.set('%n', _bin(kwargs[r'node']))
228 s = repo.set('%n', _bin(kwargs[r'node']))
229 for ctx in s:
229 for ctx in s:
230 # TODO: is there a way to just walk the files in the commit?
230 # TODO: is there a way to just walk the files in the commit?
231 if any(ctx[f].islfs() for f in ctx.files() if f in ctx):
231 if any(ctx[f].islfs() for f in ctx.files() if f in ctx):
232 repo.requirements.add('lfs')
232 repo.requirements.add('lfs')
233 repo._writerequirements()
233 repo._writerequirements()
234 repo.prepushoutgoinghooks.add('lfs', wrapper.prepush)
234 repo.prepushoutgoinghooks.add('lfs', wrapper.prepush)
235 break
235 break
236
236
237 ui.setconfig('hooks', 'commit.lfs', checkrequireslfs, 'lfs')
237 ui.setconfig('hooks', 'commit.lfs', checkrequireslfs, 'lfs')
238 ui.setconfig('hooks', 'pretxnchangegroup.lfs', checkrequireslfs, 'lfs')
238 ui.setconfig('hooks', 'pretxnchangegroup.lfs', checkrequireslfs, 'lfs')
239 else:
239 else:
240 repo.prepushoutgoinghooks.add('lfs', wrapper.prepush)
240 repo.prepushoutgoinghooks.add('lfs', wrapper.prepush)
241
241
242 def _trackedmatcher(repo):
242 def _trackedmatcher(repo):
243 """Return a function (path, size) -> bool indicating whether or not to
243 """Return a function (path, size) -> bool indicating whether or not to
244 track a given file with lfs."""
244 track a given file with lfs."""
245 if not repo.wvfs.exists('.hglfs'):
245 if not repo.wvfs.exists('.hglfs'):
246 # No '.hglfs' in wdir. Fallback to config for now.
246 # No '.hglfs' in wdir. Fallback to config for now.
247 trackspec = repo.ui.config('lfs', 'track')
247 trackspec = repo.ui.config('lfs', 'track')
248
248
249 # deprecated config: lfs.threshold
249 # deprecated config: lfs.threshold
250 threshold = repo.ui.configbytes('lfs', 'threshold')
250 threshold = repo.ui.configbytes('lfs', 'threshold')
251 if threshold:
251 if threshold:
252 fileset.parse(trackspec) # make sure syntax errors are confined
252 fileset.parse(trackspec) # make sure syntax errors are confined
253 trackspec = "(%s) | size('>%d')" % (trackspec, threshold)
253 trackspec = "(%s) | size('>%d')" % (trackspec, threshold)
254
254
255 return minifileset.compile(trackspec)
255 return minifileset.compile(trackspec)
256
256
257 data = repo.wvfs.tryread('.hglfs')
257 data = repo.wvfs.tryread('.hglfs')
258 if not data:
258 if not data:
259 return lambda p, s: False
259 return lambda p, s: False
260
260
261 # Parse errors here will abort with a message that points to the .hglfs file
261 # Parse errors here will abort with a message that points to the .hglfs file
262 # and line number.
262 # and line number.
263 cfg = config.config()
263 cfg = config.config()
264 cfg.parse('.hglfs', data)
264 cfg.parse('.hglfs', data)
265
265
266 try:
266 try:
267 rules = [(minifileset.compile(pattern), minifileset.compile(rule))
267 rules = [(minifileset.compile(pattern), minifileset.compile(rule))
268 for pattern, rule in cfg.items('track')]
268 for pattern, rule in cfg.items('track')]
269 except error.ParseError as e:
269 except error.ParseError as e:
270 # The original exception gives no indicator that the error is in the
270 # The original exception gives no indicator that the error is in the
271 # .hglfs file, so add that.
271 # .hglfs file, so add that.
272
272
273 # TODO: See if the line number of the file can be made available.
273 # TODO: See if the line number of the file can be made available.
274 raise error.Abort(_('parse error in .hglfs: %s') % e)
274 raise error.Abort(_('parse error in .hglfs: %s') % e)
275
275
276 def _match(path, size):
276 def _match(path, size):
277 for pat, rule in rules:
277 for pat, rule in rules:
278 if pat(path, size):
278 if pat(path, size):
279 return rule(path, size)
279 return rule(path, size)
280
280
281 return False
281 return False
282
282
283 return _match
283 return _match
284
284
285 def wrapfilelog(filelog):
285 def wrapfilelog(filelog):
286 wrapfunction = extensions.wrapfunction
286 wrapfunction = extensions.wrapfunction
287
287
288 wrapfunction(filelog, 'addrevision', wrapper.filelogaddrevision)
288 wrapfunction(filelog, 'addrevision', wrapper.filelogaddrevision)
289 wrapfunction(filelog, 'renamed', wrapper.filelogrenamed)
289 wrapfunction(filelog, 'renamed', wrapper.filelogrenamed)
290 wrapfunction(filelog, 'size', wrapper.filelogsize)
290 wrapfunction(filelog, 'size', wrapper.filelogsize)
291
291
292 def extsetup(ui):
292 def extsetup(ui):
293 wrapfilelog(filelog.filelog)
293 wrapfilelog(filelog.filelog)
294
294
295 wrapfunction = extensions.wrapfunction
295 wrapfunction = extensions.wrapfunction
296
296
297 wrapfunction(cmdutil, '_updatecatformatter', wrapper._updatecatformatter)
297 wrapfunction(cmdutil, '_updatecatformatter', wrapper._updatecatformatter)
298 wrapfunction(scmutil, 'wrapconvertsink', wrapper.convertsink)
298 wrapfunction(scmutil, 'wrapconvertsink', wrapper.convertsink)
299
299
300 wrapfunction(upgrade, '_finishdatamigration',
300 wrapfunction(upgrade, '_finishdatamigration',
301 wrapper.upgradefinishdatamigration)
301 wrapper.upgradefinishdatamigration)
302
302
303 wrapfunction(upgrade, 'preservedrequirements',
303 wrapfunction(upgrade, 'preservedrequirements',
304 wrapper.upgraderequirements)
304 wrapper.upgraderequirements)
305
305
306 wrapfunction(upgrade, 'supporteddestrequirements',
306 wrapfunction(upgrade, 'supporteddestrequirements',
307 wrapper.upgraderequirements)
307 wrapper.upgraderequirements)
308
308
309 wrapfunction(changegroup,
309 wrapfunction(changegroup,
310 'supportedoutgoingversions',
310 'supportedoutgoingversions',
311 wrapper.supportedoutgoingversions)
311 wrapper.supportedoutgoingversions)
312 wrapfunction(changegroup,
312 wrapfunction(changegroup,
313 'allsupportedversions',
313 'allsupportedversions',
314 wrapper.allsupportedversions)
314 wrapper.allsupportedversions)
315
315
316 wrapfunction(exchange, 'push', wrapper.push)
316 wrapfunction(exchange, 'push', wrapper.push)
317 wrapfunction(wireproto, '_capabilities', wrapper._capabilities)
317 wrapfunction(wireproto, '_capabilities', wrapper._capabilities)
318
318
319 wrapfunction(context.basefilectx, 'cmp', wrapper.filectxcmp)
319 wrapfunction(context.basefilectx, 'cmp', wrapper.filectxcmp)
320 wrapfunction(context.basefilectx, 'isbinary', wrapper.filectxisbinary)
320 wrapfunction(context.basefilectx, 'isbinary', wrapper.filectxisbinary)
321 context.basefilectx.islfs = wrapper.filectxislfs
321 context.basefilectx.islfs = wrapper.filectxislfs
322
322
323 revlog.addflagprocessor(
323 revlog.addflagprocessor(
324 revlog.REVIDX_EXTSTORED,
324 revlog.REVIDX_EXTSTORED,
325 (
325 (
326 wrapper.readfromstore,
326 wrapper.readfromstore,
327 wrapper.writetostore,
327 wrapper.writetostore,
328 wrapper.bypasscheckhash,
328 wrapper.bypasscheckhash,
329 ),
329 ),
330 )
330 )
331
331
332 wrapfunction(hg, 'clone', wrapper.hgclone)
332 wrapfunction(hg, 'clone', wrapper.hgclone)
333 wrapfunction(hg, 'postshare', wrapper.hgpostshare)
333 wrapfunction(hg, 'postshare', wrapper.hgpostshare)
334
334
335 scmutil.fileprefetchhooks.add('lfs', wrapper._prefetchfiles)
335 scmutil.fileprefetchhooks.add('lfs', wrapper._prefetchfiles)
336
336
337 # Make bundle choose changegroup3 instead of changegroup2. This affects
337 # Make bundle choose changegroup3 instead of changegroup2. This affects
338 # "hg bundle" command. Note: it does not cover all bundle formats like
338 # "hg bundle" command. Note: it does not cover all bundle formats like
339 # "packed1". Using "packed1" with lfs will likely cause trouble.
339 # "packed1". Using "packed1" with lfs will likely cause trouble.
340 names = [k for k, v in exchange._bundlespeccgversions.items() if v == '02']
340 names = [k for k, v in exchange._bundlespeccgversions.items() if v == '02']
341 for k in names:
341 for k in names:
342 exchange._bundlespeccgversions[k] = '03'
342 exchange._bundlespeccgversions[k] = '03'
343
343
344 # bundlerepo uses "vfsmod.readonlyvfs(othervfs)", we need to make sure lfs
344 # bundlerepo uses "vfsmod.readonlyvfs(othervfs)", we need to make sure lfs
345 # options and blob stores are passed from othervfs to the new readonlyvfs.
345 # options and blob stores are passed from othervfs to the new readonlyvfs.
346 wrapfunction(vfsmod.readonlyvfs, '__init__', wrapper.vfsinit)
346 wrapfunction(vfsmod.readonlyvfs, '__init__', wrapper.vfsinit)
347
347
348 # when writing a bundle via "hg bundle" command, upload related LFS blobs
348 # when writing a bundle via "hg bundle" command, upload related LFS blobs
349 wrapfunction(bundle2, 'writenewbundle', wrapper.writenewbundle)
349 wrapfunction(bundle2, 'writenewbundle', wrapper.writenewbundle)
350
350
351 @filesetpredicate('lfs()', callstatus=True)
351 @filesetpredicate('lfs()', callstatus=True)
352 def lfsfileset(mctx, x):
352 def lfsfileset(mctx, x):
353 """File that uses LFS storage."""
353 """File that uses LFS storage."""
354 # i18n: "lfs" is a keyword
354 # i18n: "lfs" is a keyword
355 fileset.getargs(x, 0, 0, _("lfs takes no arguments"))
355 fileset.getargs(x, 0, 0, _("lfs takes no arguments"))
356 return [f for f in mctx.subset
356 return [f for f in mctx.subset
357 if wrapper.pointerfromctx(mctx.ctx, f, removed=True) is not None]
357 if wrapper.pointerfromctx(mctx.ctx, f, removed=True) is not None]
358
358
359 @templatekeyword('lfs_files', requires={'ctx', 'templ'})
359 @templatekeyword('lfs_files', requires={'ctx', 'templ'})
360 def lfsfiles(context, mapping):
360 def lfsfiles(context, mapping):
361 """List of strings. All files modified, added, or removed by this
361 """List of strings. All files modified, added, or removed by this
362 changeset."""
362 changeset."""
363 ctx = context.resource(mapping, 'ctx')
363 ctx = context.resource(mapping, 'ctx')
364 templ = context.resource(mapping, 'templ')
364 templ = context.resource(mapping, 'templ')
365
365
366 pointers = wrapper.pointersfromctx(ctx, removed=True) # {path: pointer}
366 pointers = wrapper.pointersfromctx(ctx, removed=True) # {path: pointer}
367 files = sorted(pointers.keys())
367 files = sorted(pointers.keys())
368
368
369 def pointer(v):
369 def pointer(v):
370 # In the file spec, version is first and the other keys are sorted.
370 # In the file spec, version is first and the other keys are sorted.
371 sortkeyfunc = lambda x: (x[0] != 'version', x)
371 sortkeyfunc = lambda x: (x[0] != 'version', x)
372 items = sorted(pointers[v].iteritems(), key=sortkeyfunc)
372 items = sorted(pointers[v].iteritems(), key=sortkeyfunc)
373 return util.sortdict(items)
373 return util.sortdict(items)
374
374
375 makemap = lambda v: {
375 makemap = lambda v: {
376 'file': v,
376 'file': v,
377 'lfsoid': pointers[v].oid() if pointers[v] else None,
377 'lfsoid': pointers[v].oid() if pointers[v] else None,
378 'lfspointer': templatekw.hybriddict(pointer(v)),
378 'lfspointer': templateutil.hybriddict(pointer(v)),
379 }
379 }
380
380
381 # TODO: make the separator ', '?
381 # TODO: make the separator ', '?
382 f = templatekw._showlist('lfs_file', files, templ, mapping)
382 f = templateutil._showlist('lfs_file', files, templ, mapping)
383 return templatekw._hybrid(f, files, makemap, pycompat.identity)
383 return templateutil.hybrid(f, files, makemap, pycompat.identity)
384
384
385 @command('debuglfsupload',
385 @command('debuglfsupload',
386 [('r', 'rev', [], _('upload large files introduced by REV'))])
386 [('r', 'rev', [], _('upload large files introduced by REV'))])
387 def debuglfsupload(ui, repo, **opts):
387 def debuglfsupload(ui, repo, **opts):
388 """upload lfs blobs added by the working copy parent or given revisions"""
388 """upload lfs blobs added by the working copy parent or given revisions"""
389 revs = opts.get(r'rev', [])
389 revs = opts.get(r'rev', [])
390 pointers = wrapper.extractpointers(repo, scmutil.revrange(repo, revs))
390 pointers = wrapper.extractpointers(repo, scmutil.revrange(repo, revs))
391 wrapper.uploadblobs(repo, pointers)
391 wrapper.uploadblobs(repo, pointers)
@@ -1,299 +1,299 b''
1 # remotenames.py - extension to display remotenames
1 # remotenames.py - extension to display remotenames
2 #
2 #
3 # Copyright 2017 Augie Fackler <raf@durin42.com>
3 # Copyright 2017 Augie Fackler <raf@durin42.com>
4 # Copyright 2017 Sean Farley <sean@farley.io>
4 # Copyright 2017 Sean Farley <sean@farley.io>
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 """ showing remotebookmarks and remotebranches in UI
9 """ showing remotebookmarks and remotebranches in UI
10
10
11 By default both remotebookmarks and remotebranches are turned on. Config knob to
11 By default both remotebookmarks and remotebranches are turned on. Config knob to
12 control the individually are as follows.
12 control the individually are as follows.
13
13
14 Config options to tweak the default behaviour:
14 Config options to tweak the default behaviour:
15
15
16 remotenames.bookmarks
16 remotenames.bookmarks
17 Boolean value to enable or disable showing of remotebookmarks
17 Boolean value to enable or disable showing of remotebookmarks
18
18
19 remotenames.branches
19 remotenames.branches
20 Boolean value to enable or disable showing of remotebranches
20 Boolean value to enable or disable showing of remotebranches
21 """
21 """
22
22
23 from __future__ import absolute_import
23 from __future__ import absolute_import
24
24
25 import collections
25 import collections
26
26
27 from mercurial.i18n import _
27 from mercurial.i18n import _
28
28
29 from mercurial.node import (
29 from mercurial.node import (
30 bin,
30 bin,
31 )
31 )
32 from mercurial import (
32 from mercurial import (
33 logexchange,
33 logexchange,
34 namespaces,
34 namespaces,
35 registrar,
35 registrar,
36 revsetlang,
36 revsetlang,
37 smartset,
37 smartset,
38 templatekw,
38 templateutil,
39 )
39 )
40
40
41 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
41 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
42 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
42 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
43 # be specifying the version(s) of Mercurial they are tested with, or
43 # be specifying the version(s) of Mercurial they are tested with, or
44 # leave the attribute unspecified.
44 # leave the attribute unspecified.
45 testedwith = 'ships-with-hg-core'
45 testedwith = 'ships-with-hg-core'
46
46
47 configtable = {}
47 configtable = {}
48 configitem = registrar.configitem(configtable)
48 configitem = registrar.configitem(configtable)
49 templatekeyword = registrar.templatekeyword()
49 templatekeyword = registrar.templatekeyword()
50 revsetpredicate = registrar.revsetpredicate()
50 revsetpredicate = registrar.revsetpredicate()
51
51
52 configitem('remotenames', 'bookmarks',
52 configitem('remotenames', 'bookmarks',
53 default=True,
53 default=True,
54 )
54 )
55 configitem('remotenames', 'branches',
55 configitem('remotenames', 'branches',
56 default=True,
56 default=True,
57 )
57 )
58
58
59 class lazyremotenamedict(collections.MutableMapping):
59 class lazyremotenamedict(collections.MutableMapping):
60 """
60 """
61 Read-only dict-like Class to lazily resolve remotename entries
61 Read-only dict-like Class to lazily resolve remotename entries
62
62
63 We are doing that because remotenames startup was slow.
63 We are doing that because remotenames startup was slow.
64 We lazily read the remotenames file once to figure out the potential entries
64 We lazily read the remotenames file once to figure out the potential entries
65 and store them in self.potentialentries. Then when asked to resolve an
65 and store them in self.potentialentries. Then when asked to resolve an
66 entry, if it is not in self.potentialentries, then it isn't there, if it
66 entry, if it is not in self.potentialentries, then it isn't there, if it
67 is in self.potentialentries we resolve it and store the result in
67 is in self.potentialentries we resolve it and store the result in
68 self.cache. We cannot be lazy is when asked all the entries (keys).
68 self.cache. We cannot be lazy is when asked all the entries (keys).
69 """
69 """
70 def __init__(self, kind, repo):
70 def __init__(self, kind, repo):
71 self.cache = {}
71 self.cache = {}
72 self.potentialentries = {}
72 self.potentialentries = {}
73 self._kind = kind # bookmarks or branches
73 self._kind = kind # bookmarks or branches
74 self._repo = repo
74 self._repo = repo
75 self.loaded = False
75 self.loaded = False
76
76
77 def _load(self):
77 def _load(self):
78 """ Read the remotenames file, store entries matching selected kind """
78 """ Read the remotenames file, store entries matching selected kind """
79 self.loaded = True
79 self.loaded = True
80 repo = self._repo
80 repo = self._repo
81 for node, rpath, rname in logexchange.readremotenamefile(repo,
81 for node, rpath, rname in logexchange.readremotenamefile(repo,
82 self._kind):
82 self._kind):
83 name = rpath + '/' + rname
83 name = rpath + '/' + rname
84 self.potentialentries[name] = (node, rpath, name)
84 self.potentialentries[name] = (node, rpath, name)
85
85
86 def _resolvedata(self, potentialentry):
86 def _resolvedata(self, potentialentry):
87 """ Check that the node for potentialentry exists and return it """
87 """ Check that the node for potentialentry exists and return it """
88 if not potentialentry in self.potentialentries:
88 if not potentialentry in self.potentialentries:
89 return None
89 return None
90 node, remote, name = self.potentialentries[potentialentry]
90 node, remote, name = self.potentialentries[potentialentry]
91 repo = self._repo
91 repo = self._repo
92 binnode = bin(node)
92 binnode = bin(node)
93 # if the node doesn't exist, skip it
93 # if the node doesn't exist, skip it
94 try:
94 try:
95 repo.changelog.rev(binnode)
95 repo.changelog.rev(binnode)
96 except LookupError:
96 except LookupError:
97 return None
97 return None
98 # Skip closed branches
98 # Skip closed branches
99 if (self._kind == 'branches' and repo[binnode].closesbranch()):
99 if (self._kind == 'branches' and repo[binnode].closesbranch()):
100 return None
100 return None
101 return [binnode]
101 return [binnode]
102
102
103 def __getitem__(self, key):
103 def __getitem__(self, key):
104 if not self.loaded:
104 if not self.loaded:
105 self._load()
105 self._load()
106 val = self._fetchandcache(key)
106 val = self._fetchandcache(key)
107 if val is not None:
107 if val is not None:
108 return val
108 return val
109 else:
109 else:
110 raise KeyError()
110 raise KeyError()
111
111
112 def __iter__(self):
112 def __iter__(self):
113 return iter(self.potentialentries)
113 return iter(self.potentialentries)
114
114
115 def __len__(self):
115 def __len__(self):
116 return len(self.potentialentries)
116 return len(self.potentialentries)
117
117
118 def __setitem__(self):
118 def __setitem__(self):
119 raise NotImplementedError
119 raise NotImplementedError
120
120
121 def __delitem__(self):
121 def __delitem__(self):
122 raise NotImplementedError
122 raise NotImplementedError
123
123
124 def _fetchandcache(self, key):
124 def _fetchandcache(self, key):
125 if key in self.cache:
125 if key in self.cache:
126 return self.cache[key]
126 return self.cache[key]
127 val = self._resolvedata(key)
127 val = self._resolvedata(key)
128 if val is not None:
128 if val is not None:
129 self.cache[key] = val
129 self.cache[key] = val
130 return val
130 return val
131 else:
131 else:
132 return None
132 return None
133
133
134 def keys(self):
134 def keys(self):
135 """ Get a list of bookmark or branch names """
135 """ Get a list of bookmark or branch names """
136 if not self.loaded:
136 if not self.loaded:
137 self._load()
137 self._load()
138 return self.potentialentries.keys()
138 return self.potentialentries.keys()
139
139
140 def iteritems(self):
140 def iteritems(self):
141 """ Iterate over (name, node) tuples """
141 """ Iterate over (name, node) tuples """
142
142
143 if not self.loaded:
143 if not self.loaded:
144 self._load()
144 self._load()
145
145
146 for k, vtup in self.potentialentries.iteritems():
146 for k, vtup in self.potentialentries.iteritems():
147 yield (k, [bin(vtup[0])])
147 yield (k, [bin(vtup[0])])
148
148
149 class remotenames(object):
149 class remotenames(object):
150 """
150 """
151 This class encapsulates all the remotenames state. It also contains
151 This class encapsulates all the remotenames state. It also contains
152 methods to access that state in convenient ways. Remotenames are lazy
152 methods to access that state in convenient ways. Remotenames are lazy
153 loaded. Whenever client code needs to ensure the freshest copy of
153 loaded. Whenever client code needs to ensure the freshest copy of
154 remotenames, use the `clearnames` method to force an eventual load.
154 remotenames, use the `clearnames` method to force an eventual load.
155 """
155 """
156
156
157 def __init__(self, repo, *args):
157 def __init__(self, repo, *args):
158 self._repo = repo
158 self._repo = repo
159 self.clearnames()
159 self.clearnames()
160
160
161 def clearnames(self):
161 def clearnames(self):
162 """ Clear all remote names state """
162 """ Clear all remote names state """
163 self.bookmarks = lazyremotenamedict("bookmarks", self._repo)
163 self.bookmarks = lazyremotenamedict("bookmarks", self._repo)
164 self.branches = lazyremotenamedict("branches", self._repo)
164 self.branches = lazyremotenamedict("branches", self._repo)
165 self._invalidatecache()
165 self._invalidatecache()
166
166
167 def _invalidatecache(self):
167 def _invalidatecache(self):
168 self._nodetobmarks = None
168 self._nodetobmarks = None
169 self._nodetobranch = None
169 self._nodetobranch = None
170
170
171 def bmarktonodes(self):
171 def bmarktonodes(self):
172 return self.bookmarks
172 return self.bookmarks
173
173
174 def nodetobmarks(self):
174 def nodetobmarks(self):
175 if not self._nodetobmarks:
175 if not self._nodetobmarks:
176 bmarktonodes = self.bmarktonodes()
176 bmarktonodes = self.bmarktonodes()
177 self._nodetobmarks = {}
177 self._nodetobmarks = {}
178 for name, node in bmarktonodes.iteritems():
178 for name, node in bmarktonodes.iteritems():
179 self._nodetobmarks.setdefault(node[0], []).append(name)
179 self._nodetobmarks.setdefault(node[0], []).append(name)
180 return self._nodetobmarks
180 return self._nodetobmarks
181
181
182 def branchtonodes(self):
182 def branchtonodes(self):
183 return self.branches
183 return self.branches
184
184
185 def nodetobranch(self):
185 def nodetobranch(self):
186 if not self._nodetobranch:
186 if not self._nodetobranch:
187 branchtonodes = self.branchtonodes()
187 branchtonodes = self.branchtonodes()
188 self._nodetobranch = {}
188 self._nodetobranch = {}
189 for name, nodes in branchtonodes.iteritems():
189 for name, nodes in branchtonodes.iteritems():
190 for node in nodes:
190 for node in nodes:
191 self._nodetobranch.setdefault(node, []).append(name)
191 self._nodetobranch.setdefault(node, []).append(name)
192 return self._nodetobranch
192 return self._nodetobranch
193
193
194 def reposetup(ui, repo):
194 def reposetup(ui, repo):
195 if not repo.local():
195 if not repo.local():
196 return
196 return
197
197
198 repo._remotenames = remotenames(repo)
198 repo._remotenames = remotenames(repo)
199 ns = namespaces.namespace
199 ns = namespaces.namespace
200
200
201 if ui.configbool('remotenames', 'bookmarks'):
201 if ui.configbool('remotenames', 'bookmarks'):
202 remotebookmarkns = ns(
202 remotebookmarkns = ns(
203 'remotebookmarks',
203 'remotebookmarks',
204 templatename='remotebookmarks',
204 templatename='remotebookmarks',
205 colorname='remotebookmark',
205 colorname='remotebookmark',
206 logfmt='remote bookmark: %s\n',
206 logfmt='remote bookmark: %s\n',
207 listnames=lambda repo: repo._remotenames.bmarktonodes().keys(),
207 listnames=lambda repo: repo._remotenames.bmarktonodes().keys(),
208 namemap=lambda repo, name:
208 namemap=lambda repo, name:
209 repo._remotenames.bmarktonodes().get(name, []),
209 repo._remotenames.bmarktonodes().get(name, []),
210 nodemap=lambda repo, node:
210 nodemap=lambda repo, node:
211 repo._remotenames.nodetobmarks().get(node, []))
211 repo._remotenames.nodetobmarks().get(node, []))
212 repo.names.addnamespace(remotebookmarkns)
212 repo.names.addnamespace(remotebookmarkns)
213
213
214 if ui.configbool('remotenames', 'branches'):
214 if ui.configbool('remotenames', 'branches'):
215 remotebranchns = ns(
215 remotebranchns = ns(
216 'remotebranches',
216 'remotebranches',
217 templatename='remotebranches',
217 templatename='remotebranches',
218 colorname='remotebranch',
218 colorname='remotebranch',
219 logfmt='remote branch: %s\n',
219 logfmt='remote branch: %s\n',
220 listnames = lambda repo: repo._remotenames.branchtonodes().keys(),
220 listnames = lambda repo: repo._remotenames.branchtonodes().keys(),
221 namemap = lambda repo, name:
221 namemap = lambda repo, name:
222 repo._remotenames.branchtonodes().get(name, []),
222 repo._remotenames.branchtonodes().get(name, []),
223 nodemap = lambda repo, node:
223 nodemap = lambda repo, node:
224 repo._remotenames.nodetobranch().get(node, []))
224 repo._remotenames.nodetobranch().get(node, []))
225 repo.names.addnamespace(remotebranchns)
225 repo.names.addnamespace(remotebranchns)
226
226
227 @templatekeyword('remotenames', requires={'repo', 'ctx', 'templ'})
227 @templatekeyword('remotenames', requires={'repo', 'ctx', 'templ'})
228 def remotenameskw(context, mapping):
228 def remotenameskw(context, mapping):
229 """List of strings. Remote names associated with the changeset."""
229 """List of strings. Remote names associated with the changeset."""
230 repo = context.resource(mapping, 'repo')
230 repo = context.resource(mapping, 'repo')
231 ctx = context.resource(mapping, 'ctx')
231 ctx = context.resource(mapping, 'ctx')
232
232
233 remotenames = []
233 remotenames = []
234 if 'remotebookmarks' in repo.names:
234 if 'remotebookmarks' in repo.names:
235 remotenames = repo.names['remotebookmarks'].names(repo, ctx.node())
235 remotenames = repo.names['remotebookmarks'].names(repo, ctx.node())
236
236
237 if 'remotebranches' in repo.names:
237 if 'remotebranches' in repo.names:
238 remotenames += repo.names['remotebranches'].names(repo, ctx.node())
238 remotenames += repo.names['remotebranches'].names(repo, ctx.node())
239
239
240 return templatekw.compatlist(context, mapping, 'remotename', remotenames,
240 return templateutil.compatlist(context, mapping, 'remotename', remotenames,
241 plural='remotenames')
241 plural='remotenames')
242
242
243 @templatekeyword('remotebookmarks', requires={'repo', 'ctx', 'templ'})
243 @templatekeyword('remotebookmarks', requires={'repo', 'ctx', 'templ'})
244 def remotebookmarkskw(context, mapping):
244 def remotebookmarkskw(context, mapping):
245 """List of strings. Remote bookmarks associated with the changeset."""
245 """List of strings. Remote bookmarks associated with the changeset."""
246 repo = context.resource(mapping, 'repo')
246 repo = context.resource(mapping, 'repo')
247 ctx = context.resource(mapping, 'ctx')
247 ctx = context.resource(mapping, 'ctx')
248
248
249 remotebmarks = []
249 remotebmarks = []
250 if 'remotebookmarks' in repo.names:
250 if 'remotebookmarks' in repo.names:
251 remotebmarks = repo.names['remotebookmarks'].names(repo, ctx.node())
251 remotebmarks = repo.names['remotebookmarks'].names(repo, ctx.node())
252
252
253 return templatekw.compatlist(context, mapping, 'remotebookmark',
253 return templateutil.compatlist(context, mapping, 'remotebookmark',
254 remotebmarks, plural='remotebookmarks')
254 remotebmarks, plural='remotebookmarks')
255
255
256 @templatekeyword('remotebranches', requires={'repo', 'ctx', 'templ'})
256 @templatekeyword('remotebranches', requires={'repo', 'ctx', 'templ'})
257 def remotebrancheskw(context, mapping):
257 def remotebrancheskw(context, mapping):
258 """List of strings. Remote branches associated with the changeset."""
258 """List of strings. Remote branches associated with the changeset."""
259 repo = context.resource(mapping, 'repo')
259 repo = context.resource(mapping, 'repo')
260 ctx = context.resource(mapping, 'ctx')
260 ctx = context.resource(mapping, 'ctx')
261
261
262 remotebranches = []
262 remotebranches = []
263 if 'remotebranches' in repo.names:
263 if 'remotebranches' in repo.names:
264 remotebranches = repo.names['remotebranches'].names(repo, ctx.node())
264 remotebranches = repo.names['remotebranches'].names(repo, ctx.node())
265
265
266 return templatekw.compatlist(context, mapping, 'remotebranch',
266 return templateutil.compatlist(context, mapping, 'remotebranch',
267 remotebranches, plural='remotebranches')
267 remotebranches, plural='remotebranches')
268
268
269 def _revsetutil(repo, subset, x, rtypes):
269 def _revsetutil(repo, subset, x, rtypes):
270 """utility function to return a set of revs based on the rtypes"""
270 """utility function to return a set of revs based on the rtypes"""
271
271
272 revs = set()
272 revs = set()
273 cl = repo.changelog
273 cl = repo.changelog
274 for rtype in rtypes:
274 for rtype in rtypes:
275 if rtype in repo.names:
275 if rtype in repo.names:
276 ns = repo.names[rtype]
276 ns = repo.names[rtype]
277 for name in ns.listnames(repo):
277 for name in ns.listnames(repo):
278 revs.update(ns.nodes(repo, name))
278 revs.update(ns.nodes(repo, name))
279
279
280 results = (cl.rev(n) for n in revs if cl.hasnode(n))
280 results = (cl.rev(n) for n in revs if cl.hasnode(n))
281 return subset & smartset.baseset(sorted(results))
281 return subset & smartset.baseset(sorted(results))
282
282
283 @revsetpredicate('remotenames()')
283 @revsetpredicate('remotenames()')
284 def remotenamesrevset(repo, subset, x):
284 def remotenamesrevset(repo, subset, x):
285 """All changesets which have a remotename on them."""
285 """All changesets which have a remotename on them."""
286 revsetlang.getargs(x, 0, 0, _("remotenames takes no arguments"))
286 revsetlang.getargs(x, 0, 0, _("remotenames takes no arguments"))
287 return _revsetutil(repo, subset, x, ('remotebookmarks', 'remotebranches'))
287 return _revsetutil(repo, subset, x, ('remotebookmarks', 'remotebranches'))
288
288
289 @revsetpredicate('remotebranches()')
289 @revsetpredicate('remotebranches()')
290 def remotebranchesrevset(repo, subset, x):
290 def remotebranchesrevset(repo, subset, x):
291 """All changesets which are branch heads on remotes."""
291 """All changesets which are branch heads on remotes."""
292 revsetlang.getargs(x, 0, 0, _("remotebranches takes no arguments"))
292 revsetlang.getargs(x, 0, 0, _("remotebranches takes no arguments"))
293 return _revsetutil(repo, subset, x, ('remotebranches',))
293 return _revsetutil(repo, subset, x, ('remotebranches',))
294
294
295 @revsetpredicate('remotebookmarks()')
295 @revsetpredicate('remotebookmarks()')
296 def remotebmarksrevset(repo, subset, x):
296 def remotebmarksrevset(repo, subset, x):
297 """All changesets which have bookmarks on remotes."""
297 """All changesets which have bookmarks on remotes."""
298 revsetlang.getargs(x, 0, 0, _("remotebookmarks takes no arguments"))
298 revsetlang.getargs(x, 0, 0, _("remotebookmarks takes no arguments"))
299 return _revsetutil(repo, subset, x, ('remotebookmarks',))
299 return _revsetutil(repo, subset, x, ('remotebookmarks',))
@@ -1,553 +1,554 b''
1 # formatter.py - generic output formatting for mercurial
1 # formatter.py - generic output formatting for mercurial
2 #
2 #
3 # Copyright 2012 Matt Mackall <mpm@selenic.com>
3 # Copyright 2012 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 """Generic output formatting for Mercurial
8 """Generic output formatting for Mercurial
9
9
10 The formatter provides API to show data in various ways. The following
10 The formatter provides API to show data in various ways. The following
11 functions should be used in place of ui.write():
11 functions should be used in place of ui.write():
12
12
13 - fm.write() for unconditional output
13 - fm.write() for unconditional output
14 - fm.condwrite() to show some extra data conditionally in plain output
14 - fm.condwrite() to show some extra data conditionally in plain output
15 - fm.context() to provide changectx to template output
15 - fm.context() to provide changectx to template output
16 - fm.data() to provide extra data to JSON or template output
16 - fm.data() to provide extra data to JSON or template output
17 - fm.plain() to show raw text that isn't provided to JSON or template output
17 - fm.plain() to show raw text that isn't provided to JSON or template output
18
18
19 To show structured data (e.g. date tuples, dicts, lists), apply fm.format*()
19 To show structured data (e.g. date tuples, dicts, lists), apply fm.format*()
20 beforehand so the data is converted to the appropriate data type. Use
20 beforehand so the data is converted to the appropriate data type. Use
21 fm.isplain() if you need to convert or format data conditionally which isn't
21 fm.isplain() if you need to convert or format data conditionally which isn't
22 supported by the formatter API.
22 supported by the formatter API.
23
23
24 To build nested structure (i.e. a list of dicts), use fm.nested().
24 To build nested structure (i.e. a list of dicts), use fm.nested().
25
25
26 See also https://www.mercurial-scm.org/wiki/GenericTemplatingPlan
26 See also https://www.mercurial-scm.org/wiki/GenericTemplatingPlan
27
27
28 fm.condwrite() vs 'if cond:':
28 fm.condwrite() vs 'if cond:':
29
29
30 In most cases, use fm.condwrite() so users can selectively show the data
30 In most cases, use fm.condwrite() so users can selectively show the data
31 in template output. If it's costly to build data, use plain 'if cond:' with
31 in template output. If it's costly to build data, use plain 'if cond:' with
32 fm.write().
32 fm.write().
33
33
34 fm.nested() vs fm.formatdict() (or fm.formatlist()):
34 fm.nested() vs fm.formatdict() (or fm.formatlist()):
35
35
36 fm.nested() should be used to form a tree structure (a list of dicts of
36 fm.nested() should be used to form a tree structure (a list of dicts of
37 lists of dicts...) which can be accessed through template keywords, e.g.
37 lists of dicts...) which can be accessed through template keywords, e.g.
38 "{foo % "{bar % {...}} {baz % {...}}"}". On the other hand, fm.formatdict()
38 "{foo % "{bar % {...}} {baz % {...}}"}". On the other hand, fm.formatdict()
39 exports a dict-type object to template, which can be accessed by e.g.
39 exports a dict-type object to template, which can be accessed by e.g.
40 "{get(foo, key)}" function.
40 "{get(foo, key)}" function.
41
41
42 Doctest helper:
42 Doctest helper:
43
43
44 >>> def show(fn, verbose=False, **opts):
44 >>> def show(fn, verbose=False, **opts):
45 ... import sys
45 ... import sys
46 ... from . import ui as uimod
46 ... from . import ui as uimod
47 ... ui = uimod.ui()
47 ... ui = uimod.ui()
48 ... ui.verbose = verbose
48 ... ui.verbose = verbose
49 ... ui.pushbuffer()
49 ... ui.pushbuffer()
50 ... try:
50 ... try:
51 ... return fn(ui, ui.formatter(pycompat.sysbytes(fn.__name__),
51 ... return fn(ui, ui.formatter(pycompat.sysbytes(fn.__name__),
52 ... pycompat.byteskwargs(opts)))
52 ... pycompat.byteskwargs(opts)))
53 ... finally:
53 ... finally:
54 ... print(pycompat.sysstr(ui.popbuffer()), end='')
54 ... print(pycompat.sysstr(ui.popbuffer()), end='')
55
55
56 Basic example:
56 Basic example:
57
57
58 >>> def files(ui, fm):
58 >>> def files(ui, fm):
59 ... files = [(b'foo', 123, (0, 0)), (b'bar', 456, (1, 0))]
59 ... files = [(b'foo', 123, (0, 0)), (b'bar', 456, (1, 0))]
60 ... for f in files:
60 ... for f in files:
61 ... fm.startitem()
61 ... fm.startitem()
62 ... fm.write(b'path', b'%s', f[0])
62 ... fm.write(b'path', b'%s', f[0])
63 ... fm.condwrite(ui.verbose, b'date', b' %s',
63 ... fm.condwrite(ui.verbose, b'date', b' %s',
64 ... fm.formatdate(f[2], b'%Y-%m-%d %H:%M:%S'))
64 ... fm.formatdate(f[2], b'%Y-%m-%d %H:%M:%S'))
65 ... fm.data(size=f[1])
65 ... fm.data(size=f[1])
66 ... fm.plain(b'\\n')
66 ... fm.plain(b'\\n')
67 ... fm.end()
67 ... fm.end()
68 >>> show(files)
68 >>> show(files)
69 foo
69 foo
70 bar
70 bar
71 >>> show(files, verbose=True)
71 >>> show(files, verbose=True)
72 foo 1970-01-01 00:00:00
72 foo 1970-01-01 00:00:00
73 bar 1970-01-01 00:00:01
73 bar 1970-01-01 00:00:01
74 >>> show(files, template=b'json')
74 >>> show(files, template=b'json')
75 [
75 [
76 {
76 {
77 "date": [0, 0],
77 "date": [0, 0],
78 "path": "foo",
78 "path": "foo",
79 "size": 123
79 "size": 123
80 },
80 },
81 {
81 {
82 "date": [1, 0],
82 "date": [1, 0],
83 "path": "bar",
83 "path": "bar",
84 "size": 456
84 "size": 456
85 }
85 }
86 ]
86 ]
87 >>> show(files, template=b'path: {path}\\ndate: {date|rfc3339date}\\n')
87 >>> show(files, template=b'path: {path}\\ndate: {date|rfc3339date}\\n')
88 path: foo
88 path: foo
89 date: 1970-01-01T00:00:00+00:00
89 date: 1970-01-01T00:00:00+00:00
90 path: bar
90 path: bar
91 date: 1970-01-01T00:00:01+00:00
91 date: 1970-01-01T00:00:01+00:00
92
92
93 Nested example:
93 Nested example:
94
94
95 >>> def subrepos(ui, fm):
95 >>> def subrepos(ui, fm):
96 ... fm.startitem()
96 ... fm.startitem()
97 ... fm.write(b'reponame', b'[%s]\\n', b'baz')
97 ... fm.write(b'reponame', b'[%s]\\n', b'baz')
98 ... files(ui, fm.nested(b'files'))
98 ... files(ui, fm.nested(b'files'))
99 ... fm.end()
99 ... fm.end()
100 >>> show(subrepos)
100 >>> show(subrepos)
101 [baz]
101 [baz]
102 foo
102 foo
103 bar
103 bar
104 >>> show(subrepos, template=b'{reponame}: {join(files % "{path}", ", ")}\\n')
104 >>> show(subrepos, template=b'{reponame}: {join(files % "{path}", ", ")}\\n')
105 baz: foo, bar
105 baz: foo, bar
106 """
106 """
107
107
108 from __future__ import absolute_import, print_function
108 from __future__ import absolute_import, print_function
109
109
110 import collections
110 import collections
111 import contextlib
111 import contextlib
112 import itertools
112 import itertools
113 import os
113 import os
114
114
115 from .i18n import _
115 from .i18n import _
116 from .node import (
116 from .node import (
117 hex,
117 hex,
118 short,
118 short,
119 )
119 )
120
120
121 from . import (
121 from . import (
122 error,
122 error,
123 pycompat,
123 pycompat,
124 templatefilters,
124 templatefilters,
125 templatekw,
125 templatekw,
126 templater,
126 templater,
127 templateutil,
127 templateutil,
128 util,
128 util,
129 )
129 )
130 from .utils import dateutil
130 from .utils import dateutil
131
131
132 pickle = util.pickle
132 pickle = util.pickle
133
133
134 class _nullconverter(object):
134 class _nullconverter(object):
135 '''convert non-primitive data types to be processed by formatter'''
135 '''convert non-primitive data types to be processed by formatter'''
136
136
137 # set to True if context object should be stored as item
137 # set to True if context object should be stored as item
138 storecontext = False
138 storecontext = False
139
139
140 @staticmethod
140 @staticmethod
141 def formatdate(date, fmt):
141 def formatdate(date, fmt):
142 '''convert date tuple to appropriate format'''
142 '''convert date tuple to appropriate format'''
143 return date
143 return date
144 @staticmethod
144 @staticmethod
145 def formatdict(data, key, value, fmt, sep):
145 def formatdict(data, key, value, fmt, sep):
146 '''convert dict or key-value pairs to appropriate dict format'''
146 '''convert dict or key-value pairs to appropriate dict format'''
147 # use plain dict instead of util.sortdict so that data can be
147 # use plain dict instead of util.sortdict so that data can be
148 # serialized as a builtin dict in pickle output
148 # serialized as a builtin dict in pickle output
149 return dict(data)
149 return dict(data)
150 @staticmethod
150 @staticmethod
151 def formatlist(data, name, fmt, sep):
151 def formatlist(data, name, fmt, sep):
152 '''convert iterable to appropriate list format'''
152 '''convert iterable to appropriate list format'''
153 return list(data)
153 return list(data)
154
154
155 class baseformatter(object):
155 class baseformatter(object):
156 def __init__(self, ui, topic, opts, converter):
156 def __init__(self, ui, topic, opts, converter):
157 self._ui = ui
157 self._ui = ui
158 self._topic = topic
158 self._topic = topic
159 self._style = opts.get("style")
159 self._style = opts.get("style")
160 self._template = opts.get("template")
160 self._template = opts.get("template")
161 self._converter = converter
161 self._converter = converter
162 self._item = None
162 self._item = None
163 # function to convert node to string suitable for this output
163 # function to convert node to string suitable for this output
164 self.hexfunc = hex
164 self.hexfunc = hex
165 def __enter__(self):
165 def __enter__(self):
166 return self
166 return self
167 def __exit__(self, exctype, excvalue, traceback):
167 def __exit__(self, exctype, excvalue, traceback):
168 if exctype is None:
168 if exctype is None:
169 self.end()
169 self.end()
170 def _showitem(self):
170 def _showitem(self):
171 '''show a formatted item once all data is collected'''
171 '''show a formatted item once all data is collected'''
172 def startitem(self):
172 def startitem(self):
173 '''begin an item in the format list'''
173 '''begin an item in the format list'''
174 if self._item is not None:
174 if self._item is not None:
175 self._showitem()
175 self._showitem()
176 self._item = {}
176 self._item = {}
177 def formatdate(self, date, fmt='%a %b %d %H:%M:%S %Y %1%2'):
177 def formatdate(self, date, fmt='%a %b %d %H:%M:%S %Y %1%2'):
178 '''convert date tuple to appropriate format'''
178 '''convert date tuple to appropriate format'''
179 return self._converter.formatdate(date, fmt)
179 return self._converter.formatdate(date, fmt)
180 def formatdict(self, data, key='key', value='value', fmt=None, sep=' '):
180 def formatdict(self, data, key='key', value='value', fmt=None, sep=' '):
181 '''convert dict or key-value pairs to appropriate dict format'''
181 '''convert dict or key-value pairs to appropriate dict format'''
182 return self._converter.formatdict(data, key, value, fmt, sep)
182 return self._converter.formatdict(data, key, value, fmt, sep)
183 def formatlist(self, data, name, fmt=None, sep=' '):
183 def formatlist(self, data, name, fmt=None, sep=' '):
184 '''convert iterable to appropriate list format'''
184 '''convert iterable to appropriate list format'''
185 # name is mandatory argument for now, but it could be optional if
185 # name is mandatory argument for now, but it could be optional if
186 # we have default template keyword, e.g. {item}
186 # we have default template keyword, e.g. {item}
187 return self._converter.formatlist(data, name, fmt, sep)
187 return self._converter.formatlist(data, name, fmt, sep)
188 def context(self, **ctxs):
188 def context(self, **ctxs):
189 '''insert context objects to be used to render template keywords'''
189 '''insert context objects to be used to render template keywords'''
190 ctxs = pycompat.byteskwargs(ctxs)
190 ctxs = pycompat.byteskwargs(ctxs)
191 assert all(k == 'ctx' for k in ctxs)
191 assert all(k == 'ctx' for k in ctxs)
192 if self._converter.storecontext:
192 if self._converter.storecontext:
193 self._item.update(ctxs)
193 self._item.update(ctxs)
194 def data(self, **data):
194 def data(self, **data):
195 '''insert data into item that's not shown in default output'''
195 '''insert data into item that's not shown in default output'''
196 data = pycompat.byteskwargs(data)
196 data = pycompat.byteskwargs(data)
197 self._item.update(data)
197 self._item.update(data)
198 def write(self, fields, deftext, *fielddata, **opts):
198 def write(self, fields, deftext, *fielddata, **opts):
199 '''do default text output while assigning data to item'''
199 '''do default text output while assigning data to item'''
200 fieldkeys = fields.split()
200 fieldkeys = fields.split()
201 assert len(fieldkeys) == len(fielddata)
201 assert len(fieldkeys) == len(fielddata)
202 self._item.update(zip(fieldkeys, fielddata))
202 self._item.update(zip(fieldkeys, fielddata))
203 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
203 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
204 '''do conditional write (primarily for plain formatter)'''
204 '''do conditional write (primarily for plain formatter)'''
205 fieldkeys = fields.split()
205 fieldkeys = fields.split()
206 assert len(fieldkeys) == len(fielddata)
206 assert len(fieldkeys) == len(fielddata)
207 self._item.update(zip(fieldkeys, fielddata))
207 self._item.update(zip(fieldkeys, fielddata))
208 def plain(self, text, **opts):
208 def plain(self, text, **opts):
209 '''show raw text for non-templated mode'''
209 '''show raw text for non-templated mode'''
210 def isplain(self):
210 def isplain(self):
211 '''check for plain formatter usage'''
211 '''check for plain formatter usage'''
212 return False
212 return False
213 def nested(self, field):
213 def nested(self, field):
214 '''sub formatter to store nested data in the specified field'''
214 '''sub formatter to store nested data in the specified field'''
215 self._item[field] = data = []
215 self._item[field] = data = []
216 return _nestedformatter(self._ui, self._converter, data)
216 return _nestedformatter(self._ui, self._converter, data)
217 def end(self):
217 def end(self):
218 '''end output for the formatter'''
218 '''end output for the formatter'''
219 if self._item is not None:
219 if self._item is not None:
220 self._showitem()
220 self._showitem()
221
221
222 def nullformatter(ui, topic):
222 def nullformatter(ui, topic):
223 '''formatter that prints nothing'''
223 '''formatter that prints nothing'''
224 return baseformatter(ui, topic, opts={}, converter=_nullconverter)
224 return baseformatter(ui, topic, opts={}, converter=_nullconverter)
225
225
226 class _nestedformatter(baseformatter):
226 class _nestedformatter(baseformatter):
227 '''build sub items and store them in the parent formatter'''
227 '''build sub items and store them in the parent formatter'''
228 def __init__(self, ui, converter, data):
228 def __init__(self, ui, converter, data):
229 baseformatter.__init__(self, ui, topic='', opts={}, converter=converter)
229 baseformatter.__init__(self, ui, topic='', opts={}, converter=converter)
230 self._data = data
230 self._data = data
231 def _showitem(self):
231 def _showitem(self):
232 self._data.append(self._item)
232 self._data.append(self._item)
233
233
234 def _iteritems(data):
234 def _iteritems(data):
235 '''iterate key-value pairs in stable order'''
235 '''iterate key-value pairs in stable order'''
236 if isinstance(data, dict):
236 if isinstance(data, dict):
237 return sorted(data.iteritems())
237 return sorted(data.iteritems())
238 return data
238 return data
239
239
240 class _plainconverter(object):
240 class _plainconverter(object):
241 '''convert non-primitive data types to text'''
241 '''convert non-primitive data types to text'''
242
242
243 storecontext = False
243 storecontext = False
244
244
245 @staticmethod
245 @staticmethod
246 def formatdate(date, fmt):
246 def formatdate(date, fmt):
247 '''stringify date tuple in the given format'''
247 '''stringify date tuple in the given format'''
248 return dateutil.datestr(date, fmt)
248 return dateutil.datestr(date, fmt)
249 @staticmethod
249 @staticmethod
250 def formatdict(data, key, value, fmt, sep):
250 def formatdict(data, key, value, fmt, sep):
251 '''stringify key-value pairs separated by sep'''
251 '''stringify key-value pairs separated by sep'''
252 prefmt = pycompat.identity
252 prefmt = pycompat.identity
253 if fmt is None:
253 if fmt is None:
254 fmt = '%s=%s'
254 fmt = '%s=%s'
255 prefmt = pycompat.bytestr
255 prefmt = pycompat.bytestr
256 return sep.join(fmt % (prefmt(k), prefmt(v))
256 return sep.join(fmt % (prefmt(k), prefmt(v))
257 for k, v in _iteritems(data))
257 for k, v in _iteritems(data))
258 @staticmethod
258 @staticmethod
259 def formatlist(data, name, fmt, sep):
259 def formatlist(data, name, fmt, sep):
260 '''stringify iterable separated by sep'''
260 '''stringify iterable separated by sep'''
261 prefmt = pycompat.identity
261 prefmt = pycompat.identity
262 if fmt is None:
262 if fmt is None:
263 fmt = '%s'
263 fmt = '%s'
264 prefmt = pycompat.bytestr
264 prefmt = pycompat.bytestr
265 return sep.join(fmt % prefmt(e) for e in data)
265 return sep.join(fmt % prefmt(e) for e in data)
266
266
267 class plainformatter(baseformatter):
267 class plainformatter(baseformatter):
268 '''the default text output scheme'''
268 '''the default text output scheme'''
269 def __init__(self, ui, out, topic, opts):
269 def __init__(self, ui, out, topic, opts):
270 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
270 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
271 if ui.debugflag:
271 if ui.debugflag:
272 self.hexfunc = hex
272 self.hexfunc = hex
273 else:
273 else:
274 self.hexfunc = short
274 self.hexfunc = short
275 if ui is out:
275 if ui is out:
276 self._write = ui.write
276 self._write = ui.write
277 else:
277 else:
278 self._write = lambda s, **opts: out.write(s)
278 self._write = lambda s, **opts: out.write(s)
279 def startitem(self):
279 def startitem(self):
280 pass
280 pass
281 def data(self, **data):
281 def data(self, **data):
282 pass
282 pass
283 def write(self, fields, deftext, *fielddata, **opts):
283 def write(self, fields, deftext, *fielddata, **opts):
284 self._write(deftext % fielddata, **opts)
284 self._write(deftext % fielddata, **opts)
285 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
285 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
286 '''do conditional write'''
286 '''do conditional write'''
287 if cond:
287 if cond:
288 self._write(deftext % fielddata, **opts)
288 self._write(deftext % fielddata, **opts)
289 def plain(self, text, **opts):
289 def plain(self, text, **opts):
290 self._write(text, **opts)
290 self._write(text, **opts)
291 def isplain(self):
291 def isplain(self):
292 return True
292 return True
293 def nested(self, field):
293 def nested(self, field):
294 # nested data will be directly written to ui
294 # nested data will be directly written to ui
295 return self
295 return self
296 def end(self):
296 def end(self):
297 pass
297 pass
298
298
299 class debugformatter(baseformatter):
299 class debugformatter(baseformatter):
300 def __init__(self, ui, out, topic, opts):
300 def __init__(self, ui, out, topic, opts):
301 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
301 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
302 self._out = out
302 self._out = out
303 self._out.write("%s = [\n" % self._topic)
303 self._out.write("%s = [\n" % self._topic)
304 def _showitem(self):
304 def _showitem(self):
305 self._out.write(' %s,\n' % pycompat.byterepr(self._item))
305 self._out.write(' %s,\n' % pycompat.byterepr(self._item))
306 def end(self):
306 def end(self):
307 baseformatter.end(self)
307 baseformatter.end(self)
308 self._out.write("]\n")
308 self._out.write("]\n")
309
309
310 class pickleformatter(baseformatter):
310 class pickleformatter(baseformatter):
311 def __init__(self, ui, out, topic, opts):
311 def __init__(self, ui, out, topic, opts):
312 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
312 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
313 self._out = out
313 self._out = out
314 self._data = []
314 self._data = []
315 def _showitem(self):
315 def _showitem(self):
316 self._data.append(self._item)
316 self._data.append(self._item)
317 def end(self):
317 def end(self):
318 baseformatter.end(self)
318 baseformatter.end(self)
319 self._out.write(pickle.dumps(self._data))
319 self._out.write(pickle.dumps(self._data))
320
320
321 class jsonformatter(baseformatter):
321 class jsonformatter(baseformatter):
322 def __init__(self, ui, out, topic, opts):
322 def __init__(self, ui, out, topic, opts):
323 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
323 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
324 self._out = out
324 self._out = out
325 self._out.write("[")
325 self._out.write("[")
326 self._first = True
326 self._first = True
327 def _showitem(self):
327 def _showitem(self):
328 if self._first:
328 if self._first:
329 self._first = False
329 self._first = False
330 else:
330 else:
331 self._out.write(",")
331 self._out.write(",")
332
332
333 self._out.write("\n {\n")
333 self._out.write("\n {\n")
334 first = True
334 first = True
335 for k, v in sorted(self._item.items()):
335 for k, v in sorted(self._item.items()):
336 if first:
336 if first:
337 first = False
337 first = False
338 else:
338 else:
339 self._out.write(",\n")
339 self._out.write(",\n")
340 u = templatefilters.json(v, paranoid=False)
340 u = templatefilters.json(v, paranoid=False)
341 self._out.write(' "%s": %s' % (k, u))
341 self._out.write(' "%s": %s' % (k, u))
342 self._out.write("\n }")
342 self._out.write("\n }")
343 def end(self):
343 def end(self):
344 baseformatter.end(self)
344 baseformatter.end(self)
345 self._out.write("\n]\n")
345 self._out.write("\n]\n")
346
346
347 class _templateconverter(object):
347 class _templateconverter(object):
348 '''convert non-primitive data types to be processed by templater'''
348 '''convert non-primitive data types to be processed by templater'''
349
349
350 storecontext = True
350 storecontext = True
351
351
352 @staticmethod
352 @staticmethod
353 def formatdate(date, fmt):
353 def formatdate(date, fmt):
354 '''return date tuple'''
354 '''return date tuple'''
355 return date
355 return date
356 @staticmethod
356 @staticmethod
357 def formatdict(data, key, value, fmt, sep):
357 def formatdict(data, key, value, fmt, sep):
358 '''build object that can be evaluated as either plain string or dict'''
358 '''build object that can be evaluated as either plain string or dict'''
359 data = util.sortdict(_iteritems(data))
359 data = util.sortdict(_iteritems(data))
360 def f():
360 def f():
361 yield _plainconverter.formatdict(data, key, value, fmt, sep)
361 yield _plainconverter.formatdict(data, key, value, fmt, sep)
362 return templatekw.hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
362 return templateutil.hybriddict(data, key=key, value=value, fmt=fmt,
363 gen=f)
363 @staticmethod
364 @staticmethod
364 def formatlist(data, name, fmt, sep):
365 def formatlist(data, name, fmt, sep):
365 '''build object that can be evaluated as either plain string or list'''
366 '''build object that can be evaluated as either plain string or list'''
366 data = list(data)
367 data = list(data)
367 def f():
368 def f():
368 yield _plainconverter.formatlist(data, name, fmt, sep)
369 yield _plainconverter.formatlist(data, name, fmt, sep)
369 return templatekw.hybridlist(data, name=name, fmt=fmt, gen=f)
370 return templateutil.hybridlist(data, name=name, fmt=fmt, gen=f)
370
371
371 class templateformatter(baseformatter):
372 class templateformatter(baseformatter):
372 def __init__(self, ui, out, topic, opts):
373 def __init__(self, ui, out, topic, opts):
373 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
374 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
374 self._out = out
375 self._out = out
375 spec = lookuptemplate(ui, topic, opts.get('template', ''))
376 spec = lookuptemplate(ui, topic, opts.get('template', ''))
376 self._tref = spec.ref
377 self._tref = spec.ref
377 self._t = loadtemplater(ui, spec, defaults=templatekw.keywords,
378 self._t = loadtemplater(ui, spec, defaults=templatekw.keywords,
378 resources=templateresources(ui),
379 resources=templateresources(ui),
379 cache=templatekw.defaulttempl)
380 cache=templatekw.defaulttempl)
380 self._parts = templatepartsmap(spec, self._t,
381 self._parts = templatepartsmap(spec, self._t,
381 ['docheader', 'docfooter', 'separator'])
382 ['docheader', 'docfooter', 'separator'])
382 self._counter = itertools.count()
383 self._counter = itertools.count()
383 self._renderitem('docheader', {})
384 self._renderitem('docheader', {})
384
385
385 def _showitem(self):
386 def _showitem(self):
386 item = self._item.copy()
387 item = self._item.copy()
387 item['index'] = index = next(self._counter)
388 item['index'] = index = next(self._counter)
388 if index > 0:
389 if index > 0:
389 self._renderitem('separator', {})
390 self._renderitem('separator', {})
390 self._renderitem(self._tref, item)
391 self._renderitem(self._tref, item)
391
392
392 def _renderitem(self, part, item):
393 def _renderitem(self, part, item):
393 if part not in self._parts:
394 if part not in self._parts:
394 return
395 return
395 ref = self._parts[part]
396 ref = self._parts[part]
396
397
397 # TODO: add support for filectx
398 # TODO: add support for filectx
398 props = {}
399 props = {}
399 # explicitly-defined fields precede templatekw
400 # explicitly-defined fields precede templatekw
400 props.update(item)
401 props.update(item)
401 if 'ctx' in item:
402 if 'ctx' in item:
402 # but template resources must be always available
403 # but template resources must be always available
403 props['repo'] = props['ctx'].repo()
404 props['repo'] = props['ctx'].repo()
404 props['revcache'] = {}
405 props['revcache'] = {}
405 props = pycompat.strkwargs(props)
406 props = pycompat.strkwargs(props)
406 g = self._t(ref, **props)
407 g = self._t(ref, **props)
407 self._out.write(templateutil.stringify(g))
408 self._out.write(templateutil.stringify(g))
408
409
409 def end(self):
410 def end(self):
410 baseformatter.end(self)
411 baseformatter.end(self)
411 self._renderitem('docfooter', {})
412 self._renderitem('docfooter', {})
412
413
413 templatespec = collections.namedtuple(r'templatespec',
414 templatespec = collections.namedtuple(r'templatespec',
414 r'ref tmpl mapfile')
415 r'ref tmpl mapfile')
415
416
416 def lookuptemplate(ui, topic, tmpl):
417 def lookuptemplate(ui, topic, tmpl):
417 """Find the template matching the given -T/--template spec 'tmpl'
418 """Find the template matching the given -T/--template spec 'tmpl'
418
419
419 'tmpl' can be any of the following:
420 'tmpl' can be any of the following:
420
421
421 - a literal template (e.g. '{rev}')
422 - a literal template (e.g. '{rev}')
422 - a map-file name or path (e.g. 'changelog')
423 - a map-file name or path (e.g. 'changelog')
423 - a reference to [templates] in config file
424 - a reference to [templates] in config file
424 - a path to raw template file
425 - a path to raw template file
425
426
426 A map file defines a stand-alone template environment. If a map file
427 A map file defines a stand-alone template environment. If a map file
427 selected, all templates defined in the file will be loaded, and the
428 selected, all templates defined in the file will be loaded, and the
428 template matching the given topic will be rendered. Aliases won't be
429 template matching the given topic will be rendered. Aliases won't be
429 loaded from user config, but from the map file.
430 loaded from user config, but from the map file.
430
431
431 If no map file selected, all templates in [templates] section will be
432 If no map file selected, all templates in [templates] section will be
432 available as well as aliases in [templatealias].
433 available as well as aliases in [templatealias].
433 """
434 """
434
435
435 # looks like a literal template?
436 # looks like a literal template?
436 if '{' in tmpl:
437 if '{' in tmpl:
437 return templatespec('', tmpl, None)
438 return templatespec('', tmpl, None)
438
439
439 # perhaps a stock style?
440 # perhaps a stock style?
440 if not os.path.split(tmpl)[0]:
441 if not os.path.split(tmpl)[0]:
441 mapname = (templater.templatepath('map-cmdline.' + tmpl)
442 mapname = (templater.templatepath('map-cmdline.' + tmpl)
442 or templater.templatepath(tmpl))
443 or templater.templatepath(tmpl))
443 if mapname and os.path.isfile(mapname):
444 if mapname and os.path.isfile(mapname):
444 return templatespec(topic, None, mapname)
445 return templatespec(topic, None, mapname)
445
446
446 # perhaps it's a reference to [templates]
447 # perhaps it's a reference to [templates]
447 if ui.config('templates', tmpl):
448 if ui.config('templates', tmpl):
448 return templatespec(tmpl, None, None)
449 return templatespec(tmpl, None, None)
449
450
450 if tmpl == 'list':
451 if tmpl == 'list':
451 ui.write(_("available styles: %s\n") % templater.stylelist())
452 ui.write(_("available styles: %s\n") % templater.stylelist())
452 raise error.Abort(_("specify a template"))
453 raise error.Abort(_("specify a template"))
453
454
454 # perhaps it's a path to a map or a template
455 # perhaps it's a path to a map or a template
455 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
456 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
456 # is it a mapfile for a style?
457 # is it a mapfile for a style?
457 if os.path.basename(tmpl).startswith("map-"):
458 if os.path.basename(tmpl).startswith("map-"):
458 return templatespec(topic, None, os.path.realpath(tmpl))
459 return templatespec(topic, None, os.path.realpath(tmpl))
459 with util.posixfile(tmpl, 'rb') as f:
460 with util.posixfile(tmpl, 'rb') as f:
460 tmpl = f.read()
461 tmpl = f.read()
461 return templatespec('', tmpl, None)
462 return templatespec('', tmpl, None)
462
463
463 # constant string?
464 # constant string?
464 return templatespec('', tmpl, None)
465 return templatespec('', tmpl, None)
465
466
466 def templatepartsmap(spec, t, partnames):
467 def templatepartsmap(spec, t, partnames):
467 """Create a mapping of {part: ref}"""
468 """Create a mapping of {part: ref}"""
468 partsmap = {spec.ref: spec.ref} # initial ref must exist in t
469 partsmap = {spec.ref: spec.ref} # initial ref must exist in t
469 if spec.mapfile:
470 if spec.mapfile:
470 partsmap.update((p, p) for p in partnames if p in t)
471 partsmap.update((p, p) for p in partnames if p in t)
471 elif spec.ref:
472 elif spec.ref:
472 for part in partnames:
473 for part in partnames:
473 ref = '%s:%s' % (spec.ref, part) # select config sub-section
474 ref = '%s:%s' % (spec.ref, part) # select config sub-section
474 if ref in t:
475 if ref in t:
475 partsmap[part] = ref
476 partsmap[part] = ref
476 return partsmap
477 return partsmap
477
478
478 def loadtemplater(ui, spec, defaults=None, resources=None, cache=None):
479 def loadtemplater(ui, spec, defaults=None, resources=None, cache=None):
479 """Create a templater from either a literal template or loading from
480 """Create a templater from either a literal template or loading from
480 a map file"""
481 a map file"""
481 assert not (spec.tmpl and spec.mapfile)
482 assert not (spec.tmpl and spec.mapfile)
482 if spec.mapfile:
483 if spec.mapfile:
483 frommapfile = templater.templater.frommapfile
484 frommapfile = templater.templater.frommapfile
484 return frommapfile(spec.mapfile, defaults=defaults, resources=resources,
485 return frommapfile(spec.mapfile, defaults=defaults, resources=resources,
485 cache=cache)
486 cache=cache)
486 return maketemplater(ui, spec.tmpl, defaults=defaults, resources=resources,
487 return maketemplater(ui, spec.tmpl, defaults=defaults, resources=resources,
487 cache=cache)
488 cache=cache)
488
489
489 def maketemplater(ui, tmpl, defaults=None, resources=None, cache=None):
490 def maketemplater(ui, tmpl, defaults=None, resources=None, cache=None):
490 """Create a templater from a string template 'tmpl'"""
491 """Create a templater from a string template 'tmpl'"""
491 aliases = ui.configitems('templatealias')
492 aliases = ui.configitems('templatealias')
492 t = templater.templater(defaults=defaults, resources=resources,
493 t = templater.templater(defaults=defaults, resources=resources,
493 cache=cache, aliases=aliases)
494 cache=cache, aliases=aliases)
494 t.cache.update((k, templater.unquotestring(v))
495 t.cache.update((k, templater.unquotestring(v))
495 for k, v in ui.configitems('templates'))
496 for k, v in ui.configitems('templates'))
496 if tmpl:
497 if tmpl:
497 t.cache[''] = tmpl
498 t.cache[''] = tmpl
498 return t
499 return t
499
500
500 def templateresources(ui, repo=None):
501 def templateresources(ui, repo=None):
501 """Create a dict of template resources designed for the default templatekw
502 """Create a dict of template resources designed for the default templatekw
502 and function"""
503 and function"""
503 return {
504 return {
504 'cache': {}, # for templatekw/funcs to store reusable data
505 'cache': {}, # for templatekw/funcs to store reusable data
505 'ctx': None,
506 'ctx': None,
506 'repo': repo,
507 'repo': repo,
507 'revcache': None, # per-ctx cache; set later
508 'revcache': None, # per-ctx cache; set later
508 'ui': ui,
509 'ui': ui,
509 }
510 }
510
511
511 def formatter(ui, out, topic, opts):
512 def formatter(ui, out, topic, opts):
512 template = opts.get("template", "")
513 template = opts.get("template", "")
513 if template == "json":
514 if template == "json":
514 return jsonformatter(ui, out, topic, opts)
515 return jsonformatter(ui, out, topic, opts)
515 elif template == "pickle":
516 elif template == "pickle":
516 return pickleformatter(ui, out, topic, opts)
517 return pickleformatter(ui, out, topic, opts)
517 elif template == "debug":
518 elif template == "debug":
518 return debugformatter(ui, out, topic, opts)
519 return debugformatter(ui, out, topic, opts)
519 elif template != "":
520 elif template != "":
520 return templateformatter(ui, out, topic, opts)
521 return templateformatter(ui, out, topic, opts)
521 # developer config: ui.formatdebug
522 # developer config: ui.formatdebug
522 elif ui.configbool('ui', 'formatdebug'):
523 elif ui.configbool('ui', 'formatdebug'):
523 return debugformatter(ui, out, topic, opts)
524 return debugformatter(ui, out, topic, opts)
524 # deprecated config: ui.formatjson
525 # deprecated config: ui.formatjson
525 elif ui.configbool('ui', 'formatjson'):
526 elif ui.configbool('ui', 'formatjson'):
526 return jsonformatter(ui, out, topic, opts)
527 return jsonformatter(ui, out, topic, opts)
527 return plainformatter(ui, out, topic, opts)
528 return plainformatter(ui, out, topic, opts)
528
529
529 @contextlib.contextmanager
530 @contextlib.contextmanager
530 def openformatter(ui, filename, topic, opts):
531 def openformatter(ui, filename, topic, opts):
531 """Create a formatter that writes outputs to the specified file
532 """Create a formatter that writes outputs to the specified file
532
533
533 Must be invoked using the 'with' statement.
534 Must be invoked using the 'with' statement.
534 """
535 """
535 with util.posixfile(filename, 'wb') as out:
536 with util.posixfile(filename, 'wb') as out:
536 with formatter(ui, out, topic, opts) as fm:
537 with formatter(ui, out, topic, opts) as fm:
537 yield fm
538 yield fm
538
539
539 @contextlib.contextmanager
540 @contextlib.contextmanager
540 def _neverending(fm):
541 def _neverending(fm):
541 yield fm
542 yield fm
542
543
543 def maybereopen(fm, filename, opts):
544 def maybereopen(fm, filename, opts):
544 """Create a formatter backed by file if filename specified, else return
545 """Create a formatter backed by file if filename specified, else return
545 the given formatter
546 the given formatter
546
547
547 Must be invoked using the 'with' statement. This will never call fm.end()
548 Must be invoked using the 'with' statement. This will never call fm.end()
548 of the given formatter.
549 of the given formatter.
549 """
550 """
550 if filename:
551 if filename:
551 return openformatter(fm._ui, filename, fm._topic, opts)
552 return openformatter(fm._ui, filename, fm._topic, opts)
552 else:
553 else:
553 return _neverending(fm)
554 return _neverending(fm)
@@ -1,454 +1,453 b''
1 # templatefilters.py - common template expansion filters
1 # templatefilters.py - common template expansion filters
2 #
2 #
3 # Copyright 2005-2008 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2008 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import os
10 import os
11 import re
11 import re
12 import time
12 import time
13
13
14 from . import (
14 from . import (
15 encoding,
15 encoding,
16 error,
16 error,
17 node,
17 node,
18 pycompat,
18 pycompat,
19 registrar,
19 registrar,
20 templatekw,
21 templateutil,
20 templateutil,
22 url,
21 url,
23 util,
22 util,
24 )
23 )
25 from .utils import dateutil
24 from .utils import dateutil
26
25
27 urlerr = util.urlerr
26 urlerr = util.urlerr
28 urlreq = util.urlreq
27 urlreq = util.urlreq
29
28
30 if pycompat.ispy3:
29 if pycompat.ispy3:
31 long = int
30 long = int
32
31
33 # filters are callables like:
32 # filters are callables like:
34 # fn(obj)
33 # fn(obj)
35 # with:
34 # with:
36 # obj - object to be filtered (text, date, list and so on)
35 # obj - object to be filtered (text, date, list and so on)
37 filters = {}
36 filters = {}
38
37
39 templatefilter = registrar.templatefilter(filters)
38 templatefilter = registrar.templatefilter(filters)
40
39
41 @templatefilter('addbreaks')
40 @templatefilter('addbreaks')
42 def addbreaks(text):
41 def addbreaks(text):
43 """Any text. Add an XHTML "<br />" tag before the end of
42 """Any text. Add an XHTML "<br />" tag before the end of
44 every line except the last.
43 every line except the last.
45 """
44 """
46 return text.replace('\n', '<br/>\n')
45 return text.replace('\n', '<br/>\n')
47
46
48 agescales = [("year", 3600 * 24 * 365, 'Y'),
47 agescales = [("year", 3600 * 24 * 365, 'Y'),
49 ("month", 3600 * 24 * 30, 'M'),
48 ("month", 3600 * 24 * 30, 'M'),
50 ("week", 3600 * 24 * 7, 'W'),
49 ("week", 3600 * 24 * 7, 'W'),
51 ("day", 3600 * 24, 'd'),
50 ("day", 3600 * 24, 'd'),
52 ("hour", 3600, 'h'),
51 ("hour", 3600, 'h'),
53 ("minute", 60, 'm'),
52 ("minute", 60, 'm'),
54 ("second", 1, 's')]
53 ("second", 1, 's')]
55
54
56 @templatefilter('age')
55 @templatefilter('age')
57 def age(date, abbrev=False):
56 def age(date, abbrev=False):
58 """Date. Returns a human-readable date/time difference between the
57 """Date. Returns a human-readable date/time difference between the
59 given date/time and the current date/time.
58 given date/time and the current date/time.
60 """
59 """
61
60
62 def plural(t, c):
61 def plural(t, c):
63 if c == 1:
62 if c == 1:
64 return t
63 return t
65 return t + "s"
64 return t + "s"
66 def fmt(t, c, a):
65 def fmt(t, c, a):
67 if abbrev:
66 if abbrev:
68 return "%d%s" % (c, a)
67 return "%d%s" % (c, a)
69 return "%d %s" % (c, plural(t, c))
68 return "%d %s" % (c, plural(t, c))
70
69
71 now = time.time()
70 now = time.time()
72 then = date[0]
71 then = date[0]
73 future = False
72 future = False
74 if then > now:
73 if then > now:
75 future = True
74 future = True
76 delta = max(1, int(then - now))
75 delta = max(1, int(then - now))
77 if delta > agescales[0][1] * 30:
76 if delta > agescales[0][1] * 30:
78 return 'in the distant future'
77 return 'in the distant future'
79 else:
78 else:
80 delta = max(1, int(now - then))
79 delta = max(1, int(now - then))
81 if delta > agescales[0][1] * 2:
80 if delta > agescales[0][1] * 2:
82 return dateutil.shortdate(date)
81 return dateutil.shortdate(date)
83
82
84 for t, s, a in agescales:
83 for t, s, a in agescales:
85 n = delta // s
84 n = delta // s
86 if n >= 2 or s == 1:
85 if n >= 2 or s == 1:
87 if future:
86 if future:
88 return '%s from now' % fmt(t, n, a)
87 return '%s from now' % fmt(t, n, a)
89 return '%s ago' % fmt(t, n, a)
88 return '%s ago' % fmt(t, n, a)
90
89
91 @templatefilter('basename')
90 @templatefilter('basename')
92 def basename(path):
91 def basename(path):
93 """Any text. Treats the text as a path, and returns the last
92 """Any text. Treats the text as a path, and returns the last
94 component of the path after splitting by the path separator.
93 component of the path after splitting by the path separator.
95 For example, "foo/bar/baz" becomes "baz" and "foo/bar//" becomes "".
94 For example, "foo/bar/baz" becomes "baz" and "foo/bar//" becomes "".
96 """
95 """
97 return os.path.basename(path)
96 return os.path.basename(path)
98
97
99 @templatefilter('count')
98 @templatefilter('count')
100 def count(i):
99 def count(i):
101 """List or text. Returns the length as an integer."""
100 """List or text. Returns the length as an integer."""
102 return len(i)
101 return len(i)
103
102
104 @templatefilter('dirname')
103 @templatefilter('dirname')
105 def dirname(path):
104 def dirname(path):
106 """Any text. Treats the text as a path, and strips the last
105 """Any text. Treats the text as a path, and strips the last
107 component of the path after splitting by the path separator.
106 component of the path after splitting by the path separator.
108 """
107 """
109 return os.path.dirname(path)
108 return os.path.dirname(path)
110
109
111 @templatefilter('domain')
110 @templatefilter('domain')
112 def domain(author):
111 def domain(author):
113 """Any text. Finds the first string that looks like an email
112 """Any text. Finds the first string that looks like an email
114 address, and extracts just the domain component. Example: ``User
113 address, and extracts just the domain component. Example: ``User
115 <user@example.com>`` becomes ``example.com``.
114 <user@example.com>`` becomes ``example.com``.
116 """
115 """
117 f = author.find('@')
116 f = author.find('@')
118 if f == -1:
117 if f == -1:
119 return ''
118 return ''
120 author = author[f + 1:]
119 author = author[f + 1:]
121 f = author.find('>')
120 f = author.find('>')
122 if f >= 0:
121 if f >= 0:
123 author = author[:f]
122 author = author[:f]
124 return author
123 return author
125
124
126 @templatefilter('email')
125 @templatefilter('email')
127 def email(text):
126 def email(text):
128 """Any text. Extracts the first string that looks like an email
127 """Any text. Extracts the first string that looks like an email
129 address. Example: ``User <user@example.com>`` becomes
128 address. Example: ``User <user@example.com>`` becomes
130 ``user@example.com``.
129 ``user@example.com``.
131 """
130 """
132 return util.email(text)
131 return util.email(text)
133
132
134 @templatefilter('escape')
133 @templatefilter('escape')
135 def escape(text):
134 def escape(text):
136 """Any text. Replaces the special XML/XHTML characters "&", "<"
135 """Any text. Replaces the special XML/XHTML characters "&", "<"
137 and ">" with XML entities, and filters out NUL characters.
136 and ">" with XML entities, and filters out NUL characters.
138 """
137 """
139 return url.escape(text.replace('\0', ''), True)
138 return url.escape(text.replace('\0', ''), True)
140
139
141 para_re = None
140 para_re = None
142 space_re = None
141 space_re = None
143
142
144 def fill(text, width, initindent='', hangindent=''):
143 def fill(text, width, initindent='', hangindent=''):
145 '''fill many paragraphs with optional indentation.'''
144 '''fill many paragraphs with optional indentation.'''
146 global para_re, space_re
145 global para_re, space_re
147 if para_re is None:
146 if para_re is None:
148 para_re = re.compile('(\n\n|\n\\s*[-*]\\s*)', re.M)
147 para_re = re.compile('(\n\n|\n\\s*[-*]\\s*)', re.M)
149 space_re = re.compile(br' +')
148 space_re = re.compile(br' +')
150
149
151 def findparas():
150 def findparas():
152 start = 0
151 start = 0
153 while True:
152 while True:
154 m = para_re.search(text, start)
153 m = para_re.search(text, start)
155 if not m:
154 if not m:
156 uctext = encoding.unifromlocal(text[start:])
155 uctext = encoding.unifromlocal(text[start:])
157 w = len(uctext)
156 w = len(uctext)
158 while 0 < w and uctext[w - 1].isspace():
157 while 0 < w and uctext[w - 1].isspace():
159 w -= 1
158 w -= 1
160 yield (encoding.unitolocal(uctext[:w]),
159 yield (encoding.unitolocal(uctext[:w]),
161 encoding.unitolocal(uctext[w:]))
160 encoding.unitolocal(uctext[w:]))
162 break
161 break
163 yield text[start:m.start(0)], m.group(1)
162 yield text[start:m.start(0)], m.group(1)
164 start = m.end(1)
163 start = m.end(1)
165
164
166 return "".join([util.wrap(space_re.sub(' ', util.wrap(para, width)),
165 return "".join([util.wrap(space_re.sub(' ', util.wrap(para, width)),
167 width, initindent, hangindent) + rest
166 width, initindent, hangindent) + rest
168 for para, rest in findparas()])
167 for para, rest in findparas()])
169
168
170 @templatefilter('fill68')
169 @templatefilter('fill68')
171 def fill68(text):
170 def fill68(text):
172 """Any text. Wraps the text to fit in 68 columns."""
171 """Any text. Wraps the text to fit in 68 columns."""
173 return fill(text, 68)
172 return fill(text, 68)
174
173
175 @templatefilter('fill76')
174 @templatefilter('fill76')
176 def fill76(text):
175 def fill76(text):
177 """Any text. Wraps the text to fit in 76 columns."""
176 """Any text. Wraps the text to fit in 76 columns."""
178 return fill(text, 76)
177 return fill(text, 76)
179
178
180 @templatefilter('firstline')
179 @templatefilter('firstline')
181 def firstline(text):
180 def firstline(text):
182 """Any text. Returns the first line of text."""
181 """Any text. Returns the first line of text."""
183 try:
182 try:
184 return text.splitlines(True)[0].rstrip('\r\n')
183 return text.splitlines(True)[0].rstrip('\r\n')
185 except IndexError:
184 except IndexError:
186 return ''
185 return ''
187
186
188 @templatefilter('hex')
187 @templatefilter('hex')
189 def hexfilter(text):
188 def hexfilter(text):
190 """Any text. Convert a binary Mercurial node identifier into
189 """Any text. Convert a binary Mercurial node identifier into
191 its long hexadecimal representation.
190 its long hexadecimal representation.
192 """
191 """
193 return node.hex(text)
192 return node.hex(text)
194
193
195 @templatefilter('hgdate')
194 @templatefilter('hgdate')
196 def hgdate(text):
195 def hgdate(text):
197 """Date. Returns the date as a pair of numbers: "1157407993
196 """Date. Returns the date as a pair of numbers: "1157407993
198 25200" (Unix timestamp, timezone offset).
197 25200" (Unix timestamp, timezone offset).
199 """
198 """
200 return "%d %d" % text
199 return "%d %d" % text
201
200
202 @templatefilter('isodate')
201 @templatefilter('isodate')
203 def isodate(text):
202 def isodate(text):
204 """Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
203 """Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
205 +0200".
204 +0200".
206 """
205 """
207 return dateutil.datestr(text, '%Y-%m-%d %H:%M %1%2')
206 return dateutil.datestr(text, '%Y-%m-%d %H:%M %1%2')
208
207
209 @templatefilter('isodatesec')
208 @templatefilter('isodatesec')
210 def isodatesec(text):
209 def isodatesec(text):
211 """Date. Returns the date in ISO 8601 format, including
210 """Date. Returns the date in ISO 8601 format, including
212 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
211 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
213 filter.
212 filter.
214 """
213 """
215 return dateutil.datestr(text, '%Y-%m-%d %H:%M:%S %1%2')
214 return dateutil.datestr(text, '%Y-%m-%d %H:%M:%S %1%2')
216
215
217 def indent(text, prefix):
216 def indent(text, prefix):
218 '''indent each non-empty line of text after first with prefix.'''
217 '''indent each non-empty line of text after first with prefix.'''
219 lines = text.splitlines()
218 lines = text.splitlines()
220 num_lines = len(lines)
219 num_lines = len(lines)
221 endswithnewline = text[-1:] == '\n'
220 endswithnewline = text[-1:] == '\n'
222 def indenter():
221 def indenter():
223 for i in xrange(num_lines):
222 for i in xrange(num_lines):
224 l = lines[i]
223 l = lines[i]
225 if i and l.strip():
224 if i and l.strip():
226 yield prefix
225 yield prefix
227 yield l
226 yield l
228 if i < num_lines - 1 or endswithnewline:
227 if i < num_lines - 1 or endswithnewline:
229 yield '\n'
228 yield '\n'
230 return "".join(indenter())
229 return "".join(indenter())
231
230
232 @templatefilter('json')
231 @templatefilter('json')
233 def json(obj, paranoid=True):
232 def json(obj, paranoid=True):
234 if obj is None:
233 if obj is None:
235 return 'null'
234 return 'null'
236 elif obj is False:
235 elif obj is False:
237 return 'false'
236 return 'false'
238 elif obj is True:
237 elif obj is True:
239 return 'true'
238 return 'true'
240 elif isinstance(obj, (int, long, float)):
239 elif isinstance(obj, (int, long, float)):
241 return pycompat.bytestr(obj)
240 return pycompat.bytestr(obj)
242 elif isinstance(obj, bytes):
241 elif isinstance(obj, bytes):
243 return '"%s"' % encoding.jsonescape(obj, paranoid=paranoid)
242 return '"%s"' % encoding.jsonescape(obj, paranoid=paranoid)
244 elif isinstance(obj, str):
243 elif isinstance(obj, str):
245 # This branch is unreachable on Python 2, because bytes == str
244 # This branch is unreachable on Python 2, because bytes == str
246 # and we'll return in the next-earlier block in the elif
245 # and we'll return in the next-earlier block in the elif
247 # ladder. On Python 3, this helps us catch bugs before they
246 # ladder. On Python 3, this helps us catch bugs before they
248 # hurt someone.
247 # hurt someone.
249 raise error.ProgrammingError(
248 raise error.ProgrammingError(
250 'Mercurial only does output with bytes on Python 3: %r' % obj)
249 'Mercurial only does output with bytes on Python 3: %r' % obj)
251 elif util.safehasattr(obj, 'keys'):
250 elif util.safehasattr(obj, 'keys'):
252 out = ['"%s": %s' % (encoding.jsonescape(k, paranoid=paranoid),
251 out = ['"%s": %s' % (encoding.jsonescape(k, paranoid=paranoid),
253 json(v, paranoid))
252 json(v, paranoid))
254 for k, v in sorted(obj.iteritems())]
253 for k, v in sorted(obj.iteritems())]
255 return '{' + ', '.join(out) + '}'
254 return '{' + ', '.join(out) + '}'
256 elif util.safehasattr(obj, '__iter__'):
255 elif util.safehasattr(obj, '__iter__'):
257 out = [json(i, paranoid) for i in obj]
256 out = [json(i, paranoid) for i in obj]
258 return '[' + ', '.join(out) + ']'
257 return '[' + ', '.join(out) + ']'
259 else:
258 else:
260 raise TypeError('cannot encode type %s' % obj.__class__.__name__)
259 raise TypeError('cannot encode type %s' % obj.__class__.__name__)
261
260
262 @templatefilter('lower')
261 @templatefilter('lower')
263 def lower(text):
262 def lower(text):
264 """Any text. Converts the text to lowercase."""
263 """Any text. Converts the text to lowercase."""
265 return encoding.lower(text)
264 return encoding.lower(text)
266
265
267 @templatefilter('nonempty')
266 @templatefilter('nonempty')
268 def nonempty(text):
267 def nonempty(text):
269 """Any text. Returns '(none)' if the string is empty."""
268 """Any text. Returns '(none)' if the string is empty."""
270 return text or "(none)"
269 return text or "(none)"
271
270
272 @templatefilter('obfuscate')
271 @templatefilter('obfuscate')
273 def obfuscate(text):
272 def obfuscate(text):
274 """Any text. Returns the input text rendered as a sequence of
273 """Any text. Returns the input text rendered as a sequence of
275 XML entities.
274 XML entities.
276 """
275 """
277 text = unicode(text, pycompat.sysstr(encoding.encoding), r'replace')
276 text = unicode(text, pycompat.sysstr(encoding.encoding), r'replace')
278 return ''.join(['&#%d;' % ord(c) for c in text])
277 return ''.join(['&#%d;' % ord(c) for c in text])
279
278
280 @templatefilter('permissions')
279 @templatefilter('permissions')
281 def permissions(flags):
280 def permissions(flags):
282 if "l" in flags:
281 if "l" in flags:
283 return "lrwxrwxrwx"
282 return "lrwxrwxrwx"
284 if "x" in flags:
283 if "x" in flags:
285 return "-rwxr-xr-x"
284 return "-rwxr-xr-x"
286 return "-rw-r--r--"
285 return "-rw-r--r--"
287
286
288 @templatefilter('person')
287 @templatefilter('person')
289 def person(author):
288 def person(author):
290 """Any text. Returns the name before an email address,
289 """Any text. Returns the name before an email address,
291 interpreting it as per RFC 5322.
290 interpreting it as per RFC 5322.
292
291
293 >>> person(b'foo@bar')
292 >>> person(b'foo@bar')
294 'foo'
293 'foo'
295 >>> person(b'Foo Bar <foo@bar>')
294 >>> person(b'Foo Bar <foo@bar>')
296 'Foo Bar'
295 'Foo Bar'
297 >>> person(b'"Foo Bar" <foo@bar>')
296 >>> person(b'"Foo Bar" <foo@bar>')
298 'Foo Bar'
297 'Foo Bar'
299 >>> person(b'"Foo \"buz\" Bar" <foo@bar>')
298 >>> person(b'"Foo \"buz\" Bar" <foo@bar>')
300 'Foo "buz" Bar'
299 'Foo "buz" Bar'
301 >>> # The following are invalid, but do exist in real-life
300 >>> # The following are invalid, but do exist in real-life
302 ...
301 ...
303 >>> person(b'Foo "buz" Bar <foo@bar>')
302 >>> person(b'Foo "buz" Bar <foo@bar>')
304 'Foo "buz" Bar'
303 'Foo "buz" Bar'
305 >>> person(b'"Foo Bar <foo@bar>')
304 >>> person(b'"Foo Bar <foo@bar>')
306 'Foo Bar'
305 'Foo Bar'
307 """
306 """
308 if '@' not in author:
307 if '@' not in author:
309 return author
308 return author
310 f = author.find('<')
309 f = author.find('<')
311 if f != -1:
310 if f != -1:
312 return author[:f].strip(' "').replace('\\"', '"')
311 return author[:f].strip(' "').replace('\\"', '"')
313 f = author.find('@')
312 f = author.find('@')
314 return author[:f].replace('.', ' ')
313 return author[:f].replace('.', ' ')
315
314
316 @templatefilter('revescape')
315 @templatefilter('revescape')
317 def revescape(text):
316 def revescape(text):
318 """Any text. Escapes all "special" characters, except @.
317 """Any text. Escapes all "special" characters, except @.
319 Forward slashes are escaped twice to prevent web servers from prematurely
318 Forward slashes are escaped twice to prevent web servers from prematurely
320 unescaping them. For example, "@foo bar/baz" becomes "@foo%20bar%252Fbaz".
319 unescaping them. For example, "@foo bar/baz" becomes "@foo%20bar%252Fbaz".
321 """
320 """
322 return urlreq.quote(text, safe='/@').replace('/', '%252F')
321 return urlreq.quote(text, safe='/@').replace('/', '%252F')
323
322
324 @templatefilter('rfc3339date')
323 @templatefilter('rfc3339date')
325 def rfc3339date(text):
324 def rfc3339date(text):
326 """Date. Returns a date using the Internet date format
325 """Date. Returns a date using the Internet date format
327 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
326 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
328 """
327 """
329 return dateutil.datestr(text, "%Y-%m-%dT%H:%M:%S%1:%2")
328 return dateutil.datestr(text, "%Y-%m-%dT%H:%M:%S%1:%2")
330
329
331 @templatefilter('rfc822date')
330 @templatefilter('rfc822date')
332 def rfc822date(text):
331 def rfc822date(text):
333 """Date. Returns a date using the same format used in email
332 """Date. Returns a date using the same format used in email
334 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
333 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
335 """
334 """
336 return dateutil.datestr(text, "%a, %d %b %Y %H:%M:%S %1%2")
335 return dateutil.datestr(text, "%a, %d %b %Y %H:%M:%S %1%2")
337
336
338 @templatefilter('short')
337 @templatefilter('short')
339 def short(text):
338 def short(text):
340 """Changeset hash. Returns the short form of a changeset hash,
339 """Changeset hash. Returns the short form of a changeset hash,
341 i.e. a 12 hexadecimal digit string.
340 i.e. a 12 hexadecimal digit string.
342 """
341 """
343 return text[:12]
342 return text[:12]
344
343
345 @templatefilter('shortbisect')
344 @templatefilter('shortbisect')
346 def shortbisect(label):
345 def shortbisect(label):
347 """Any text. Treats `label` as a bisection status, and
346 """Any text. Treats `label` as a bisection status, and
348 returns a single-character representing the status (G: good, B: bad,
347 returns a single-character representing the status (G: good, B: bad,
349 S: skipped, U: untested, I: ignored). Returns single space if `text`
348 S: skipped, U: untested, I: ignored). Returns single space if `text`
350 is not a valid bisection status.
349 is not a valid bisection status.
351 """
350 """
352 if label:
351 if label:
353 return label[0:1].upper()
352 return label[0:1].upper()
354 return ' '
353 return ' '
355
354
356 @templatefilter('shortdate')
355 @templatefilter('shortdate')
357 def shortdate(text):
356 def shortdate(text):
358 """Date. Returns a date like "2006-09-18"."""
357 """Date. Returns a date like "2006-09-18"."""
359 return dateutil.shortdate(text)
358 return dateutil.shortdate(text)
360
359
361 @templatefilter('slashpath')
360 @templatefilter('slashpath')
362 def slashpath(path):
361 def slashpath(path):
363 """Any text. Replaces the native path separator with slash."""
362 """Any text. Replaces the native path separator with slash."""
364 return util.pconvert(path)
363 return util.pconvert(path)
365
364
366 @templatefilter('splitlines')
365 @templatefilter('splitlines')
367 def splitlines(text):
366 def splitlines(text):
368 """Any text. Split text into a list of lines."""
367 """Any text. Split text into a list of lines."""
369 return templatekw.hybridlist(text.splitlines(), name='line')
368 return templateutil.hybridlist(text.splitlines(), name='line')
370
369
371 @templatefilter('stringescape')
370 @templatefilter('stringescape')
372 def stringescape(text):
371 def stringescape(text):
373 return util.escapestr(text)
372 return util.escapestr(text)
374
373
375 @templatefilter('stringify')
374 @templatefilter('stringify')
376 def stringify(thing):
375 def stringify(thing):
377 """Any type. Turns the value into text by converting values into
376 """Any type. Turns the value into text by converting values into
378 text and concatenating them.
377 text and concatenating them.
379 """
378 """
380 return templateutil.stringify(thing)
379 return templateutil.stringify(thing)
381
380
382 @templatefilter('stripdir')
381 @templatefilter('stripdir')
383 def stripdir(text):
382 def stripdir(text):
384 """Treat the text as path and strip a directory level, if
383 """Treat the text as path and strip a directory level, if
385 possible. For example, "foo" and "foo/bar" becomes "foo".
384 possible. For example, "foo" and "foo/bar" becomes "foo".
386 """
385 """
387 dir = os.path.dirname(text)
386 dir = os.path.dirname(text)
388 if dir == "":
387 if dir == "":
389 return os.path.basename(text)
388 return os.path.basename(text)
390 else:
389 else:
391 return dir
390 return dir
392
391
393 @templatefilter('tabindent')
392 @templatefilter('tabindent')
394 def tabindent(text):
393 def tabindent(text):
395 """Any text. Returns the text, with every non-empty line
394 """Any text. Returns the text, with every non-empty line
396 except the first starting with a tab character.
395 except the first starting with a tab character.
397 """
396 """
398 return indent(text, '\t')
397 return indent(text, '\t')
399
398
400 @templatefilter('upper')
399 @templatefilter('upper')
401 def upper(text):
400 def upper(text):
402 """Any text. Converts the text to uppercase."""
401 """Any text. Converts the text to uppercase."""
403 return encoding.upper(text)
402 return encoding.upper(text)
404
403
405 @templatefilter('urlescape')
404 @templatefilter('urlescape')
406 def urlescape(text):
405 def urlescape(text):
407 """Any text. Escapes all "special" characters. For example,
406 """Any text. Escapes all "special" characters. For example,
408 "foo bar" becomes "foo%20bar".
407 "foo bar" becomes "foo%20bar".
409 """
408 """
410 return urlreq.quote(text)
409 return urlreq.quote(text)
411
410
412 @templatefilter('user')
411 @templatefilter('user')
413 def userfilter(text):
412 def userfilter(text):
414 """Any text. Returns a short representation of a user name or email
413 """Any text. Returns a short representation of a user name or email
415 address."""
414 address."""
416 return util.shortuser(text)
415 return util.shortuser(text)
417
416
418 @templatefilter('emailuser')
417 @templatefilter('emailuser')
419 def emailuser(text):
418 def emailuser(text):
420 """Any text. Returns the user portion of an email address."""
419 """Any text. Returns the user portion of an email address."""
421 return util.emailuser(text)
420 return util.emailuser(text)
422
421
423 @templatefilter('utf8')
422 @templatefilter('utf8')
424 def utf8(text):
423 def utf8(text):
425 """Any text. Converts from the local character encoding to UTF-8."""
424 """Any text. Converts from the local character encoding to UTF-8."""
426 return encoding.fromlocal(text)
425 return encoding.fromlocal(text)
427
426
428 @templatefilter('xmlescape')
427 @templatefilter('xmlescape')
429 def xmlescape(text):
428 def xmlescape(text):
430 text = (text
429 text = (text
431 .replace('&', '&amp;')
430 .replace('&', '&amp;')
432 .replace('<', '&lt;')
431 .replace('<', '&lt;')
433 .replace('>', '&gt;')
432 .replace('>', '&gt;')
434 .replace('"', '&quot;')
433 .replace('"', '&quot;')
435 .replace("'", '&#39;')) # &apos; invalid in HTML
434 .replace("'", '&#39;')) # &apos; invalid in HTML
436 return re.sub('[\x00-\x08\x0B\x0C\x0E-\x1F]', ' ', text)
435 return re.sub('[\x00-\x08\x0B\x0C\x0E-\x1F]', ' ', text)
437
436
438 def websub(text, websubtable):
437 def websub(text, websubtable):
439 """:websub: Any text. Only applies to hgweb. Applies the regular
438 """:websub: Any text. Only applies to hgweb. Applies the regular
440 expression replacements defined in the websub section.
439 expression replacements defined in the websub section.
441 """
440 """
442 if websubtable:
441 if websubtable:
443 for regexp, format in websubtable:
442 for regexp, format in websubtable:
444 text = regexp.sub(format, text)
443 text = regexp.sub(format, text)
445 return text
444 return text
446
445
447 def loadfilter(ui, extname, registrarobj):
446 def loadfilter(ui, extname, registrarobj):
448 """Load template filter from specified registrarobj
447 """Load template filter from specified registrarobj
449 """
448 """
450 for name, func in registrarobj._table.iteritems():
449 for name, func in registrarobj._table.iteritems():
451 filters[name] = func
450 filters[name] = func
452
451
453 # tell hggettext to extract docstrings from these functions:
452 # tell hggettext to extract docstrings from these functions:
454 i18nfunctions = filters.values()
453 i18nfunctions = filters.values()
@@ -1,1003 +1,802 b''
1 # templatekw.py - common changeset template keywords
1 # templatekw.py - common changeset template keywords
2 #
2 #
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 from .i18n import _
10 from .i18n import _
11 from .node import (
11 from .node import (
12 hex,
12 hex,
13 nullid,
13 nullid,
14 )
14 )
15
15
16 from . import (
16 from . import (
17 encoding,
17 encoding,
18 error,
18 error,
19 hbisect,
19 hbisect,
20 i18n,
20 i18n,
21 obsutil,
21 obsutil,
22 patch,
22 patch,
23 pycompat,
23 pycompat,
24 registrar,
24 registrar,
25 scmutil,
25 scmutil,
26 templateutil,
26 util,
27 util,
27 )
28 )
28
29
29 class _hybrid(object):
30 _hybrid = templateutil.hybrid
30 """Wrapper for list or dict to support legacy template
31 _mappable = templateutil.mappable
31
32 _showlist = templateutil._showlist
32 This class allows us to handle both:
33 hybriddict = templateutil.hybriddict
33 - "{files}" (legacy command-line-specific list hack) and
34 hybridlist = templateutil.hybridlist
34 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
35 compatdict = templateutil.compatdict
35 and to access raw values:
36 compatlist = templateutil.compatlist
36 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
37 - "{get(extras, key)}"
38 - "{files|json}"
39 """
40
41 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
42 if gen is not None:
43 self.gen = gen # generator or function returning generator
44 self._values = values
45 self._makemap = makemap
46 self.joinfmt = joinfmt
47 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
48 def gen(self):
49 """Default generator to stringify this as {join(self, ' ')}"""
50 for i, x in enumerate(self._values):
51 if i > 0:
52 yield ' '
53 yield self.joinfmt(x)
54 def itermaps(self):
55 makemap = self._makemap
56 for x in self._values:
57 yield makemap(x)
58 def __contains__(self, x):
59 return x in self._values
60 def __getitem__(self, key):
61 return self._values[key]
62 def __len__(self):
63 return len(self._values)
64 def __iter__(self):
65 return iter(self._values)
66 def __getattr__(self, name):
67 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
68 r'itervalues', r'keys', r'values'):
69 raise AttributeError(name)
70 return getattr(self._values, name)
71
72 class _mappable(object):
73 """Wrapper for non-list/dict object to support map operation
74
75 This class allows us to handle both:
76 - "{manifest}"
77 - "{manifest % '{rev}:{node}'}"
78 - "{manifest.rev}"
79
80 Unlike a _hybrid, this does not simulate the behavior of the underling
81 value. Use unwrapvalue() or unwraphybrid() to obtain the inner object.
82 """
83
84 def __init__(self, gen, key, value, makemap):
85 if gen is not None:
86 self.gen = gen # generator or function returning generator
87 self._key = key
88 self._value = value # may be generator of strings
89 self._makemap = makemap
90
91 def gen(self):
92 yield pycompat.bytestr(self._value)
93
94 def tomap(self):
95 return self._makemap(self._key)
96
97 def itermaps(self):
98 yield self.tomap()
99
100 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
101 """Wrap data to support both dict-like and string-like operations"""
102 prefmt = pycompat.identity
103 if fmt is None:
104 fmt = '%s=%s'
105 prefmt = pycompat.bytestr
106 return _hybrid(gen, data, lambda k: {key: k, value: data[k]},
107 lambda k: fmt % (prefmt(k), prefmt(data[k])))
108
109 def hybridlist(data, name, fmt=None, gen=None):
110 """Wrap data to support both list-like and string-like operations"""
111 prefmt = pycompat.identity
112 if fmt is None:
113 fmt = '%s'
114 prefmt = pycompat.bytestr
115 return _hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
116
117 def unwraphybrid(thing):
118 """Return an object which can be stringified possibly by using a legacy
119 template"""
120 gen = getattr(thing, 'gen', None)
121 if gen is None:
122 return thing
123 if callable(gen):
124 return gen()
125 return gen
126
127 def unwrapvalue(thing):
128 """Move the inner value object out of the wrapper"""
129 if not util.safehasattr(thing, '_value'):
130 return thing
131 return thing._value
132
133 def wraphybridvalue(container, key, value):
134 """Wrap an element of hybrid container to be mappable
135
136 The key is passed to the makemap function of the given container, which
137 should be an item generated by iter(container).
138 """
139 makemap = getattr(container, '_makemap', None)
140 if makemap is None:
141 return value
142 if util.safehasattr(value, '_makemap'):
143 # a nested hybrid list/dict, which has its own way of map operation
144 return value
145 return _mappable(None, key, value, makemap)
146
147 def compatdict(context, mapping, name, data, key='key', value='value',
148 fmt=None, plural=None, separator=' '):
149 """Wrap data like hybriddict(), but also supports old-style list template
150
151 This exists for backward compatibility with the old-style template. Use
152 hybriddict() for new template keywords.
153 """
154 c = [{key: k, value: v} for k, v in data.iteritems()]
155 t = context.resource(mapping, 'templ')
156 f = _showlist(name, c, t, mapping, plural, separator)
157 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
158
159 def compatlist(context, mapping, name, data, element=None, fmt=None,
160 plural=None, separator=' '):
161 """Wrap data like hybridlist(), but also supports old-style list template
162
163 This exists for backward compatibility with the old-style template. Use
164 hybridlist() for new template keywords.
165 """
166 t = context.resource(mapping, 'templ')
167 f = _showlist(name, data, t, mapping, plural, separator)
168 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
169
37
170 def showdict(name, data, mapping, plural=None, key='key', value='value',
38 def showdict(name, data, mapping, plural=None, key='key', value='value',
171 fmt=None, separator=' '):
39 fmt=None, separator=' '):
172 ui = mapping.get('ui')
40 ui = mapping.get('ui')
173 if ui:
41 if ui:
174 ui.deprecwarn("templatekw.showdict() is deprecated, use compatdict()",
42 ui.deprecwarn("templatekw.showdict() is deprecated, use "
175 '4.6')
43 "templateutil.compatdict()", '4.6')
176 c = [{key: k, value: v} for k, v in data.iteritems()]
44 c = [{key: k, value: v} for k, v in data.iteritems()]
177 f = _showlist(name, c, mapping['templ'], mapping, plural, separator)
45 f = _showlist(name, c, mapping['templ'], mapping, plural, separator)
178 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
46 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
179
47
180 def showlist(name, values, mapping, plural=None, element=None, separator=' '):
48 def showlist(name, values, mapping, plural=None, element=None, separator=' '):
181 ui = mapping.get('ui')
49 ui = mapping.get('ui')
182 if ui:
50 if ui:
183 ui.deprecwarn("templatekw.showlist() is deprecated, use compatlist()",
51 ui.deprecwarn("templatekw.showlist() is deprecated, use "
184 '4.6')
52 "templateutil.compatlist()", '4.6')
185 if not element:
53 if not element:
186 element = name
54 element = name
187 f = _showlist(name, values, mapping['templ'], mapping, plural, separator)
55 f = _showlist(name, values, mapping['templ'], mapping, plural, separator)
188 return hybridlist(values, name=element, gen=f)
56 return hybridlist(values, name=element, gen=f)
189
57
190 def _showlist(name, values, templ, mapping, plural=None, separator=' '):
191 '''expand set of values.
192 name is name of key in template map.
193 values is list of strings or dicts.
194 plural is plural of name, if not simply name + 's'.
195 separator is used to join values as a string
196
197 expansion works like this, given name 'foo'.
198
199 if values is empty, expand 'no_foos'.
200
201 if 'foo' not in template map, return values as a string,
202 joined by 'separator'.
203
204 expand 'start_foos'.
205
206 for each value, expand 'foo'. if 'last_foo' in template
207 map, expand it instead of 'foo' for last key.
208
209 expand 'end_foos'.
210 '''
211 strmapping = pycompat.strkwargs(mapping)
212 if not plural:
213 plural = name + 's'
214 if not values:
215 noname = 'no_' + plural
216 if noname in templ:
217 yield templ(noname, **strmapping)
218 return
219 if name not in templ:
220 if isinstance(values[0], bytes):
221 yield separator.join(values)
222 else:
223 for v in values:
224 r = dict(v)
225 r.update(mapping)
226 yield r
227 return
228 startname = 'start_' + plural
229 if startname in templ:
230 yield templ(startname, **strmapping)
231 vmapping = mapping.copy()
232 def one(v, tag=name):
233 try:
234 vmapping.update(v)
235 # Python 2 raises ValueError if the type of v is wrong. Python
236 # 3 raises TypeError.
237 except (AttributeError, TypeError, ValueError):
238 try:
239 # Python 2 raises ValueError trying to destructure an e.g.
240 # bytes. Python 3 raises TypeError.
241 for a, b in v:
242 vmapping[a] = b
243 except (TypeError, ValueError):
244 vmapping[name] = v
245 return templ(tag, **pycompat.strkwargs(vmapping))
246 lastname = 'last_' + name
247 if lastname in templ:
248 last = values.pop()
249 else:
250 last = None
251 for v in values:
252 yield one(v)
253 if last is not None:
254 yield one(last, tag=lastname)
255 endname = 'end_' + plural
256 if endname in templ:
257 yield templ(endname, **strmapping)
258
259 def getlatesttags(context, mapping, pattern=None):
58 def getlatesttags(context, mapping, pattern=None):
260 '''return date, distance and name for the latest tag of rev'''
59 '''return date, distance and name for the latest tag of rev'''
261 repo = context.resource(mapping, 'repo')
60 repo = context.resource(mapping, 'repo')
262 ctx = context.resource(mapping, 'ctx')
61 ctx = context.resource(mapping, 'ctx')
263 cache = context.resource(mapping, 'cache')
62 cache = context.resource(mapping, 'cache')
264
63
265 cachename = 'latesttags'
64 cachename = 'latesttags'
266 if pattern is not None:
65 if pattern is not None:
267 cachename += '-' + pattern
66 cachename += '-' + pattern
268 match = util.stringmatcher(pattern)[2]
67 match = util.stringmatcher(pattern)[2]
269 else:
68 else:
270 match = util.always
69 match = util.always
271
70
272 if cachename not in cache:
71 if cachename not in cache:
273 # Cache mapping from rev to a tuple with tag date, tag
72 # Cache mapping from rev to a tuple with tag date, tag
274 # distance and tag name
73 # distance and tag name
275 cache[cachename] = {-1: (0, 0, ['null'])}
74 cache[cachename] = {-1: (0, 0, ['null'])}
276 latesttags = cache[cachename]
75 latesttags = cache[cachename]
277
76
278 rev = ctx.rev()
77 rev = ctx.rev()
279 todo = [rev]
78 todo = [rev]
280 while todo:
79 while todo:
281 rev = todo.pop()
80 rev = todo.pop()
282 if rev in latesttags:
81 if rev in latesttags:
283 continue
82 continue
284 ctx = repo[rev]
83 ctx = repo[rev]
285 tags = [t for t in ctx.tags()
84 tags = [t for t in ctx.tags()
286 if (repo.tagtype(t) and repo.tagtype(t) != 'local'
85 if (repo.tagtype(t) and repo.tagtype(t) != 'local'
287 and match(t))]
86 and match(t))]
288 if tags:
87 if tags:
289 latesttags[rev] = ctx.date()[0], 0, [t for t in sorted(tags)]
88 latesttags[rev] = ctx.date()[0], 0, [t for t in sorted(tags)]
290 continue
89 continue
291 try:
90 try:
292 ptags = [latesttags[p.rev()] for p in ctx.parents()]
91 ptags = [latesttags[p.rev()] for p in ctx.parents()]
293 if len(ptags) > 1:
92 if len(ptags) > 1:
294 if ptags[0][2] == ptags[1][2]:
93 if ptags[0][2] == ptags[1][2]:
295 # The tuples are laid out so the right one can be found by
94 # The tuples are laid out so the right one can be found by
296 # comparison in this case.
95 # comparison in this case.
297 pdate, pdist, ptag = max(ptags)
96 pdate, pdist, ptag = max(ptags)
298 else:
97 else:
299 def key(x):
98 def key(x):
300 changessincetag = len(repo.revs('only(%d, %s)',
99 changessincetag = len(repo.revs('only(%d, %s)',
301 ctx.rev(), x[2][0]))
100 ctx.rev(), x[2][0]))
302 # Smallest number of changes since tag wins. Date is
101 # Smallest number of changes since tag wins. Date is
303 # used as tiebreaker.
102 # used as tiebreaker.
304 return [-changessincetag, x[0]]
103 return [-changessincetag, x[0]]
305 pdate, pdist, ptag = max(ptags, key=key)
104 pdate, pdist, ptag = max(ptags, key=key)
306 else:
105 else:
307 pdate, pdist, ptag = ptags[0]
106 pdate, pdist, ptag = ptags[0]
308 except KeyError:
107 except KeyError:
309 # Cache miss - recurse
108 # Cache miss - recurse
310 todo.append(rev)
109 todo.append(rev)
311 todo.extend(p.rev() for p in ctx.parents())
110 todo.extend(p.rev() for p in ctx.parents())
312 continue
111 continue
313 latesttags[rev] = pdate, pdist + 1, ptag
112 latesttags[rev] = pdate, pdist + 1, ptag
314 return latesttags[rev]
113 return latesttags[rev]
315
114
316 def getrenamedfn(repo, endrev=None):
115 def getrenamedfn(repo, endrev=None):
317 rcache = {}
116 rcache = {}
318 if endrev is None:
117 if endrev is None:
319 endrev = len(repo)
118 endrev = len(repo)
320
119
321 def getrenamed(fn, rev):
120 def getrenamed(fn, rev):
322 '''looks up all renames for a file (up to endrev) the first
121 '''looks up all renames for a file (up to endrev) the first
323 time the file is given. It indexes on the changerev and only
122 time the file is given. It indexes on the changerev and only
324 parses the manifest if linkrev != changerev.
123 parses the manifest if linkrev != changerev.
325 Returns rename info for fn at changerev rev.'''
124 Returns rename info for fn at changerev rev.'''
326 if fn not in rcache:
125 if fn not in rcache:
327 rcache[fn] = {}
126 rcache[fn] = {}
328 fl = repo.file(fn)
127 fl = repo.file(fn)
329 for i in fl:
128 for i in fl:
330 lr = fl.linkrev(i)
129 lr = fl.linkrev(i)
331 renamed = fl.renamed(fl.node(i))
130 renamed = fl.renamed(fl.node(i))
332 rcache[fn][lr] = renamed
131 rcache[fn][lr] = renamed
333 if lr >= endrev:
132 if lr >= endrev:
334 break
133 break
335 if rev in rcache[fn]:
134 if rev in rcache[fn]:
336 return rcache[fn][rev]
135 return rcache[fn][rev]
337
136
338 # If linkrev != rev (i.e. rev not found in rcache) fallback to
137 # If linkrev != rev (i.e. rev not found in rcache) fallback to
339 # filectx logic.
138 # filectx logic.
340 try:
139 try:
341 return repo[rev][fn].renamed()
140 return repo[rev][fn].renamed()
342 except error.LookupError:
141 except error.LookupError:
343 return None
142 return None
344
143
345 return getrenamed
144 return getrenamed
346
145
347 def getlogcolumns():
146 def getlogcolumns():
348 """Return a dict of log column labels"""
147 """Return a dict of log column labels"""
349 _ = pycompat.identity # temporarily disable gettext
148 _ = pycompat.identity # temporarily disable gettext
350 # i18n: column positioning for "hg log"
149 # i18n: column positioning for "hg log"
351 columns = _('bookmark: %s\n'
150 columns = _('bookmark: %s\n'
352 'branch: %s\n'
151 'branch: %s\n'
353 'changeset: %s\n'
152 'changeset: %s\n'
354 'copies: %s\n'
153 'copies: %s\n'
355 'date: %s\n'
154 'date: %s\n'
356 'extra: %s=%s\n'
155 'extra: %s=%s\n'
357 'files+: %s\n'
156 'files+: %s\n'
358 'files-: %s\n'
157 'files-: %s\n'
359 'files: %s\n'
158 'files: %s\n'
360 'instability: %s\n'
159 'instability: %s\n'
361 'manifest: %s\n'
160 'manifest: %s\n'
362 'obsolete: %s\n'
161 'obsolete: %s\n'
363 'parent: %s\n'
162 'parent: %s\n'
364 'phase: %s\n'
163 'phase: %s\n'
365 'summary: %s\n'
164 'summary: %s\n'
366 'tag: %s\n'
165 'tag: %s\n'
367 'user: %s\n')
166 'user: %s\n')
368 return dict(zip([s.split(':', 1)[0] for s in columns.splitlines()],
167 return dict(zip([s.split(':', 1)[0] for s in columns.splitlines()],
369 i18n._(columns).splitlines(True)))
168 i18n._(columns).splitlines(True)))
370
169
371 # default templates internally used for rendering of lists
170 # default templates internally used for rendering of lists
372 defaulttempl = {
171 defaulttempl = {
373 'parent': '{rev}:{node|formatnode} ',
172 'parent': '{rev}:{node|formatnode} ',
374 'manifest': '{rev}:{node|formatnode}',
173 'manifest': '{rev}:{node|formatnode}',
375 'file_copy': '{name} ({source})',
174 'file_copy': '{name} ({source})',
376 'envvar': '{key}={value}',
175 'envvar': '{key}={value}',
377 'extra': '{key}={value|stringescape}'
176 'extra': '{key}={value|stringescape}'
378 }
177 }
379 # filecopy is preserved for compatibility reasons
178 # filecopy is preserved for compatibility reasons
380 defaulttempl['filecopy'] = defaulttempl['file_copy']
179 defaulttempl['filecopy'] = defaulttempl['file_copy']
381
180
382 # keywords are callables (see registrar.templatekeyword for details)
181 # keywords are callables (see registrar.templatekeyword for details)
383 keywords = {}
182 keywords = {}
384 templatekeyword = registrar.templatekeyword(keywords)
183 templatekeyword = registrar.templatekeyword(keywords)
385
184
386 @templatekeyword('author', requires={'ctx'})
185 @templatekeyword('author', requires={'ctx'})
387 def showauthor(context, mapping):
186 def showauthor(context, mapping):
388 """String. The unmodified author of the changeset."""
187 """String. The unmodified author of the changeset."""
389 ctx = context.resource(mapping, 'ctx')
188 ctx = context.resource(mapping, 'ctx')
390 return ctx.user()
189 return ctx.user()
391
190
392 @templatekeyword('bisect', requires={'repo', 'ctx'})
191 @templatekeyword('bisect', requires={'repo', 'ctx'})
393 def showbisect(context, mapping):
192 def showbisect(context, mapping):
394 """String. The changeset bisection status."""
193 """String. The changeset bisection status."""
395 repo = context.resource(mapping, 'repo')
194 repo = context.resource(mapping, 'repo')
396 ctx = context.resource(mapping, 'ctx')
195 ctx = context.resource(mapping, 'ctx')
397 return hbisect.label(repo, ctx.node())
196 return hbisect.label(repo, ctx.node())
398
197
399 @templatekeyword('branch', requires={'ctx'})
198 @templatekeyword('branch', requires={'ctx'})
400 def showbranch(context, mapping):
199 def showbranch(context, mapping):
401 """String. The name of the branch on which the changeset was
200 """String. The name of the branch on which the changeset was
402 committed.
201 committed.
403 """
202 """
404 ctx = context.resource(mapping, 'ctx')
203 ctx = context.resource(mapping, 'ctx')
405 return ctx.branch()
204 return ctx.branch()
406
205
407 @templatekeyword('branches', requires={'ctx', 'templ'})
206 @templatekeyword('branches', requires={'ctx', 'templ'})
408 def showbranches(context, mapping):
207 def showbranches(context, mapping):
409 """List of strings. The name of the branch on which the
208 """List of strings. The name of the branch on which the
410 changeset was committed. Will be empty if the branch name was
209 changeset was committed. Will be empty if the branch name was
411 default. (DEPRECATED)
210 default. (DEPRECATED)
412 """
211 """
413 ctx = context.resource(mapping, 'ctx')
212 ctx = context.resource(mapping, 'ctx')
414 branch = ctx.branch()
213 branch = ctx.branch()
415 if branch != 'default':
214 if branch != 'default':
416 return compatlist(context, mapping, 'branch', [branch],
215 return compatlist(context, mapping, 'branch', [branch],
417 plural='branches')
216 plural='branches')
418 return compatlist(context, mapping, 'branch', [], plural='branches')
217 return compatlist(context, mapping, 'branch', [], plural='branches')
419
218
420 @templatekeyword('bookmarks', requires={'repo', 'ctx', 'templ'})
219 @templatekeyword('bookmarks', requires={'repo', 'ctx', 'templ'})
421 def showbookmarks(context, mapping):
220 def showbookmarks(context, mapping):
422 """List of strings. Any bookmarks associated with the
221 """List of strings. Any bookmarks associated with the
423 changeset. Also sets 'active', the name of the active bookmark.
222 changeset. Also sets 'active', the name of the active bookmark.
424 """
223 """
425 repo = context.resource(mapping, 'repo')
224 repo = context.resource(mapping, 'repo')
426 ctx = context.resource(mapping, 'ctx')
225 ctx = context.resource(mapping, 'ctx')
427 templ = context.resource(mapping, 'templ')
226 templ = context.resource(mapping, 'templ')
428 bookmarks = ctx.bookmarks()
227 bookmarks = ctx.bookmarks()
429 active = repo._activebookmark
228 active = repo._activebookmark
430 makemap = lambda v: {'bookmark': v, 'active': active, 'current': active}
229 makemap = lambda v: {'bookmark': v, 'active': active, 'current': active}
431 f = _showlist('bookmark', bookmarks, templ, mapping)
230 f = _showlist('bookmark', bookmarks, templ, mapping)
432 return _hybrid(f, bookmarks, makemap, pycompat.identity)
231 return _hybrid(f, bookmarks, makemap, pycompat.identity)
433
232
434 @templatekeyword('children', requires={'ctx', 'templ'})
233 @templatekeyword('children', requires={'ctx', 'templ'})
435 def showchildren(context, mapping):
234 def showchildren(context, mapping):
436 """List of strings. The children of the changeset."""
235 """List of strings. The children of the changeset."""
437 ctx = context.resource(mapping, 'ctx')
236 ctx = context.resource(mapping, 'ctx')
438 childrevs = ['%d:%s' % (cctx.rev(), cctx) for cctx in ctx.children()]
237 childrevs = ['%d:%s' % (cctx.rev(), cctx) for cctx in ctx.children()]
439 return compatlist(context, mapping, 'children', childrevs, element='child')
238 return compatlist(context, mapping, 'children', childrevs, element='child')
440
239
441 # Deprecated, but kept alive for help generation a purpose.
240 # Deprecated, but kept alive for help generation a purpose.
442 @templatekeyword('currentbookmark', requires={'repo', 'ctx'})
241 @templatekeyword('currentbookmark', requires={'repo', 'ctx'})
443 def showcurrentbookmark(context, mapping):
242 def showcurrentbookmark(context, mapping):
444 """String. The active bookmark, if it is associated with the changeset.
243 """String. The active bookmark, if it is associated with the changeset.
445 (DEPRECATED)"""
244 (DEPRECATED)"""
446 return showactivebookmark(context, mapping)
245 return showactivebookmark(context, mapping)
447
246
448 @templatekeyword('activebookmark', requires={'repo', 'ctx'})
247 @templatekeyword('activebookmark', requires={'repo', 'ctx'})
449 def showactivebookmark(context, mapping):
248 def showactivebookmark(context, mapping):
450 """String. The active bookmark, if it is associated with the changeset."""
249 """String. The active bookmark, if it is associated with the changeset."""
451 repo = context.resource(mapping, 'repo')
250 repo = context.resource(mapping, 'repo')
452 ctx = context.resource(mapping, 'ctx')
251 ctx = context.resource(mapping, 'ctx')
453 active = repo._activebookmark
252 active = repo._activebookmark
454 if active and active in ctx.bookmarks():
253 if active and active in ctx.bookmarks():
455 return active
254 return active
456 return ''
255 return ''
457
256
458 @templatekeyword('date', requires={'ctx'})
257 @templatekeyword('date', requires={'ctx'})
459 def showdate(context, mapping):
258 def showdate(context, mapping):
460 """Date information. The date when the changeset was committed."""
259 """Date information. The date when the changeset was committed."""
461 ctx = context.resource(mapping, 'ctx')
260 ctx = context.resource(mapping, 'ctx')
462 return ctx.date()
261 return ctx.date()
463
262
464 @templatekeyword('desc', requires={'ctx'})
263 @templatekeyword('desc', requires={'ctx'})
465 def showdescription(context, mapping):
264 def showdescription(context, mapping):
466 """String. The text of the changeset description."""
265 """String. The text of the changeset description."""
467 ctx = context.resource(mapping, 'ctx')
266 ctx = context.resource(mapping, 'ctx')
468 s = ctx.description()
267 s = ctx.description()
469 if isinstance(s, encoding.localstr):
268 if isinstance(s, encoding.localstr):
470 # try hard to preserve utf-8 bytes
269 # try hard to preserve utf-8 bytes
471 return encoding.tolocal(encoding.fromlocal(s).strip())
270 return encoding.tolocal(encoding.fromlocal(s).strip())
472 else:
271 else:
473 return s.strip()
272 return s.strip()
474
273
475 @templatekeyword('diffstat', requires={'ctx'})
274 @templatekeyword('diffstat', requires={'ctx'})
476 def showdiffstat(context, mapping):
275 def showdiffstat(context, mapping):
477 """String. Statistics of changes with the following format:
276 """String. Statistics of changes with the following format:
478 "modified files: +added/-removed lines"
277 "modified files: +added/-removed lines"
479 """
278 """
480 ctx = context.resource(mapping, 'ctx')
279 ctx = context.resource(mapping, 'ctx')
481 stats = patch.diffstatdata(util.iterlines(ctx.diff(noprefix=False)))
280 stats = patch.diffstatdata(util.iterlines(ctx.diff(noprefix=False)))
482 maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
281 maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
483 return '%d: +%d/-%d' % (len(stats), adds, removes)
282 return '%d: +%d/-%d' % (len(stats), adds, removes)
484
283
485 @templatekeyword('envvars', requires={'ui', 'templ'})
284 @templatekeyword('envvars', requires={'ui', 'templ'})
486 def showenvvars(context, mapping):
285 def showenvvars(context, mapping):
487 """A dictionary of environment variables. (EXPERIMENTAL)"""
286 """A dictionary of environment variables. (EXPERIMENTAL)"""
488 ui = context.resource(mapping, 'ui')
287 ui = context.resource(mapping, 'ui')
489 env = ui.exportableenviron()
288 env = ui.exportableenviron()
490 env = util.sortdict((k, env[k]) for k in sorted(env))
289 env = util.sortdict((k, env[k]) for k in sorted(env))
491 return compatdict(context, mapping, 'envvar', env, plural='envvars')
290 return compatdict(context, mapping, 'envvar', env, plural='envvars')
492
291
493 @templatekeyword('extras', requires={'ctx', 'templ'})
292 @templatekeyword('extras', requires={'ctx', 'templ'})
494 def showextras(context, mapping):
293 def showextras(context, mapping):
495 """List of dicts with key, value entries of the 'extras'
294 """List of dicts with key, value entries of the 'extras'
496 field of this changeset."""
295 field of this changeset."""
497 ctx = context.resource(mapping, 'ctx')
296 ctx = context.resource(mapping, 'ctx')
498 templ = context.resource(mapping, 'templ')
297 templ = context.resource(mapping, 'templ')
499 extras = ctx.extra()
298 extras = ctx.extra()
500 extras = util.sortdict((k, extras[k]) for k in sorted(extras))
299 extras = util.sortdict((k, extras[k]) for k in sorted(extras))
501 makemap = lambda k: {'key': k, 'value': extras[k]}
300 makemap = lambda k: {'key': k, 'value': extras[k]}
502 c = [makemap(k) for k in extras]
301 c = [makemap(k) for k in extras]
503 f = _showlist('extra', c, templ, mapping, plural='extras')
302 f = _showlist('extra', c, templ, mapping, plural='extras')
504 return _hybrid(f, extras, makemap,
303 return _hybrid(f, extras, makemap,
505 lambda k: '%s=%s' % (k, util.escapestr(extras[k])))
304 lambda k: '%s=%s' % (k, util.escapestr(extras[k])))
506
305
507 def _showfilesbystat(context, mapping, name, index):
306 def _showfilesbystat(context, mapping, name, index):
508 repo = context.resource(mapping, 'repo')
307 repo = context.resource(mapping, 'repo')
509 ctx = context.resource(mapping, 'ctx')
308 ctx = context.resource(mapping, 'ctx')
510 revcache = context.resource(mapping, 'revcache')
309 revcache = context.resource(mapping, 'revcache')
511 if 'files' not in revcache:
310 if 'files' not in revcache:
512 revcache['files'] = repo.status(ctx.p1(), ctx)[:3]
311 revcache['files'] = repo.status(ctx.p1(), ctx)[:3]
513 files = revcache['files'][index]
312 files = revcache['files'][index]
514 return compatlist(context, mapping, name, files, element='file')
313 return compatlist(context, mapping, name, files, element='file')
515
314
516 @templatekeyword('file_adds', requires={'repo', 'ctx', 'revcache', 'templ'})
315 @templatekeyword('file_adds', requires={'repo', 'ctx', 'revcache', 'templ'})
517 def showfileadds(context, mapping):
316 def showfileadds(context, mapping):
518 """List of strings. Files added by this changeset."""
317 """List of strings. Files added by this changeset."""
519 return _showfilesbystat(context, mapping, 'file_add', 1)
318 return _showfilesbystat(context, mapping, 'file_add', 1)
520
319
521 @templatekeyword('file_copies',
320 @templatekeyword('file_copies',
522 requires={'repo', 'ctx', 'cache', 'revcache', 'templ'})
321 requires={'repo', 'ctx', 'cache', 'revcache', 'templ'})
523 def showfilecopies(context, mapping):
322 def showfilecopies(context, mapping):
524 """List of strings. Files copied in this changeset with
323 """List of strings. Files copied in this changeset with
525 their sources.
324 their sources.
526 """
325 """
527 repo = context.resource(mapping, 'repo')
326 repo = context.resource(mapping, 'repo')
528 ctx = context.resource(mapping, 'ctx')
327 ctx = context.resource(mapping, 'ctx')
529 cache = context.resource(mapping, 'cache')
328 cache = context.resource(mapping, 'cache')
530 copies = context.resource(mapping, 'revcache').get('copies')
329 copies = context.resource(mapping, 'revcache').get('copies')
531 if copies is None:
330 if copies is None:
532 if 'getrenamed' not in cache:
331 if 'getrenamed' not in cache:
533 cache['getrenamed'] = getrenamedfn(repo)
332 cache['getrenamed'] = getrenamedfn(repo)
534 copies = []
333 copies = []
535 getrenamed = cache['getrenamed']
334 getrenamed = cache['getrenamed']
536 for fn in ctx.files():
335 for fn in ctx.files():
537 rename = getrenamed(fn, ctx.rev())
336 rename = getrenamed(fn, ctx.rev())
538 if rename:
337 if rename:
539 copies.append((fn, rename[0]))
338 copies.append((fn, rename[0]))
540
339
541 copies = util.sortdict(copies)
340 copies = util.sortdict(copies)
542 return compatdict(context, mapping, 'file_copy', copies,
341 return compatdict(context, mapping, 'file_copy', copies,
543 key='name', value='source', fmt='%s (%s)',
342 key='name', value='source', fmt='%s (%s)',
544 plural='file_copies')
343 plural='file_copies')
545
344
546 # showfilecopiesswitch() displays file copies only if copy records are
345 # showfilecopiesswitch() displays file copies only if copy records are
547 # provided before calling the templater, usually with a --copies
346 # provided before calling the templater, usually with a --copies
548 # command line switch.
347 # command line switch.
549 @templatekeyword('file_copies_switch', requires={'revcache', 'templ'})
348 @templatekeyword('file_copies_switch', requires={'revcache', 'templ'})
550 def showfilecopiesswitch(context, mapping):
349 def showfilecopiesswitch(context, mapping):
551 """List of strings. Like "file_copies" but displayed
350 """List of strings. Like "file_copies" but displayed
552 only if the --copied switch is set.
351 only if the --copied switch is set.
553 """
352 """
554 copies = context.resource(mapping, 'revcache').get('copies') or []
353 copies = context.resource(mapping, 'revcache').get('copies') or []
555 copies = util.sortdict(copies)
354 copies = util.sortdict(copies)
556 return compatdict(context, mapping, 'file_copy', copies,
355 return compatdict(context, mapping, 'file_copy', copies,
557 key='name', value='source', fmt='%s (%s)',
356 key='name', value='source', fmt='%s (%s)',
558 plural='file_copies')
357 plural='file_copies')
559
358
560 @templatekeyword('file_dels', requires={'repo', 'ctx', 'revcache', 'templ'})
359 @templatekeyword('file_dels', requires={'repo', 'ctx', 'revcache', 'templ'})
561 def showfiledels(context, mapping):
360 def showfiledels(context, mapping):
562 """List of strings. Files removed by this changeset."""
361 """List of strings. Files removed by this changeset."""
563 return _showfilesbystat(context, mapping, 'file_del', 2)
362 return _showfilesbystat(context, mapping, 'file_del', 2)
564
363
565 @templatekeyword('file_mods', requires={'repo', 'ctx', 'revcache', 'templ'})
364 @templatekeyword('file_mods', requires={'repo', 'ctx', 'revcache', 'templ'})
566 def showfilemods(context, mapping):
365 def showfilemods(context, mapping):
567 """List of strings. Files modified by this changeset."""
366 """List of strings. Files modified by this changeset."""
568 return _showfilesbystat(context, mapping, 'file_mod', 0)
367 return _showfilesbystat(context, mapping, 'file_mod', 0)
569
368
570 @templatekeyword('files', requires={'ctx', 'templ'})
369 @templatekeyword('files', requires={'ctx', 'templ'})
571 def showfiles(context, mapping):
370 def showfiles(context, mapping):
572 """List of strings. All files modified, added, or removed by this
371 """List of strings. All files modified, added, or removed by this
573 changeset.
372 changeset.
574 """
373 """
575 ctx = context.resource(mapping, 'ctx')
374 ctx = context.resource(mapping, 'ctx')
576 return compatlist(context, mapping, 'file', ctx.files())
375 return compatlist(context, mapping, 'file', ctx.files())
577
376
578 @templatekeyword('graphnode', requires={'repo', 'ctx'})
377 @templatekeyword('graphnode', requires={'repo', 'ctx'})
579 def showgraphnode(context, mapping):
378 def showgraphnode(context, mapping):
580 """String. The character representing the changeset node in an ASCII
379 """String. The character representing the changeset node in an ASCII
581 revision graph."""
380 revision graph."""
582 repo = context.resource(mapping, 'repo')
381 repo = context.resource(mapping, 'repo')
583 ctx = context.resource(mapping, 'ctx')
382 ctx = context.resource(mapping, 'ctx')
584 return getgraphnode(repo, ctx)
383 return getgraphnode(repo, ctx)
585
384
586 def getgraphnode(repo, ctx):
385 def getgraphnode(repo, ctx):
587 wpnodes = repo.dirstate.parents()
386 wpnodes = repo.dirstate.parents()
588 if wpnodes[1] == nullid:
387 if wpnodes[1] == nullid:
589 wpnodes = wpnodes[:1]
388 wpnodes = wpnodes[:1]
590 if ctx.node() in wpnodes:
389 if ctx.node() in wpnodes:
591 return '@'
390 return '@'
592 elif ctx.obsolete():
391 elif ctx.obsolete():
593 return 'x'
392 return 'x'
594 elif ctx.isunstable():
393 elif ctx.isunstable():
595 return '*'
394 return '*'
596 elif ctx.closesbranch():
395 elif ctx.closesbranch():
597 return '_'
396 return '_'
598 else:
397 else:
599 return 'o'
398 return 'o'
600
399
601 @templatekeyword('graphwidth', requires=())
400 @templatekeyword('graphwidth', requires=())
602 def showgraphwidth(context, mapping):
401 def showgraphwidth(context, mapping):
603 """Integer. The width of the graph drawn by 'log --graph' or zero."""
402 """Integer. The width of the graph drawn by 'log --graph' or zero."""
604 # just hosts documentation; should be overridden by template mapping
403 # just hosts documentation; should be overridden by template mapping
605 return 0
404 return 0
606
405
607 @templatekeyword('index', requires=())
406 @templatekeyword('index', requires=())
608 def showindex(context, mapping):
407 def showindex(context, mapping):
609 """Integer. The current iteration of the loop. (0 indexed)"""
408 """Integer. The current iteration of the loop. (0 indexed)"""
610 # just hosts documentation; should be overridden by template mapping
409 # just hosts documentation; should be overridden by template mapping
611 raise error.Abort(_("can't use index in this context"))
410 raise error.Abort(_("can't use index in this context"))
612
411
613 @templatekeyword('latesttag', requires={'repo', 'ctx', 'cache', 'templ'})
412 @templatekeyword('latesttag', requires={'repo', 'ctx', 'cache', 'templ'})
614 def showlatesttag(context, mapping):
413 def showlatesttag(context, mapping):
615 """List of strings. The global tags on the most recent globally
414 """List of strings. The global tags on the most recent globally
616 tagged ancestor of this changeset. If no such tags exist, the list
415 tagged ancestor of this changeset. If no such tags exist, the list
617 consists of the single string "null".
416 consists of the single string "null".
618 """
417 """
619 return showlatesttags(context, mapping, None)
418 return showlatesttags(context, mapping, None)
620
419
621 def showlatesttags(context, mapping, pattern):
420 def showlatesttags(context, mapping, pattern):
622 """helper method for the latesttag keyword and function"""
421 """helper method for the latesttag keyword and function"""
623 latesttags = getlatesttags(context, mapping, pattern)
422 latesttags = getlatesttags(context, mapping, pattern)
624
423
625 # latesttag[0] is an implementation detail for sorting csets on different
424 # latesttag[0] is an implementation detail for sorting csets on different
626 # branches in a stable manner- it is the date the tagged cset was created,
425 # branches in a stable manner- it is the date the tagged cset was created,
627 # not the date the tag was created. Therefore it isn't made visible here.
426 # not the date the tag was created. Therefore it isn't made visible here.
628 makemap = lambda v: {
427 makemap = lambda v: {
629 'changes': _showchangessincetag,
428 'changes': _showchangessincetag,
630 'distance': latesttags[1],
429 'distance': latesttags[1],
631 'latesttag': v, # BC with {latesttag % '{latesttag}'}
430 'latesttag': v, # BC with {latesttag % '{latesttag}'}
632 'tag': v
431 'tag': v
633 }
432 }
634
433
635 tags = latesttags[2]
434 tags = latesttags[2]
636 templ = context.resource(mapping, 'templ')
435 templ = context.resource(mapping, 'templ')
637 f = _showlist('latesttag', tags, templ, mapping, separator=':')
436 f = _showlist('latesttag', tags, templ, mapping, separator=':')
638 return _hybrid(f, tags, makemap, pycompat.identity)
437 return _hybrid(f, tags, makemap, pycompat.identity)
639
438
640 @templatekeyword('latesttagdistance', requires={'repo', 'ctx', 'cache'})
439 @templatekeyword('latesttagdistance', requires={'repo', 'ctx', 'cache'})
641 def showlatesttagdistance(context, mapping):
440 def showlatesttagdistance(context, mapping):
642 """Integer. Longest path to the latest tag."""
441 """Integer. Longest path to the latest tag."""
643 return getlatesttags(context, mapping)[1]
442 return getlatesttags(context, mapping)[1]
644
443
645 @templatekeyword('changessincelatesttag', requires={'repo', 'ctx', 'cache'})
444 @templatekeyword('changessincelatesttag', requires={'repo', 'ctx', 'cache'})
646 def showchangessincelatesttag(context, mapping):
445 def showchangessincelatesttag(context, mapping):
647 """Integer. All ancestors not in the latest tag."""
446 """Integer. All ancestors not in the latest tag."""
648 mapping = mapping.copy()
447 mapping = mapping.copy()
649 mapping['tag'] = getlatesttags(context, mapping)[2][0]
448 mapping['tag'] = getlatesttags(context, mapping)[2][0]
650 return _showchangessincetag(context, mapping)
449 return _showchangessincetag(context, mapping)
651
450
652 def _showchangessincetag(context, mapping):
451 def _showchangessincetag(context, mapping):
653 repo = context.resource(mapping, 'repo')
452 repo = context.resource(mapping, 'repo')
654 ctx = context.resource(mapping, 'ctx')
453 ctx = context.resource(mapping, 'ctx')
655 offset = 0
454 offset = 0
656 revs = [ctx.rev()]
455 revs = [ctx.rev()]
657 tag = context.symbol(mapping, 'tag')
456 tag = context.symbol(mapping, 'tag')
658
457
659 # The only() revset doesn't currently support wdir()
458 # The only() revset doesn't currently support wdir()
660 if ctx.rev() is None:
459 if ctx.rev() is None:
661 offset = 1
460 offset = 1
662 revs = [p.rev() for p in ctx.parents()]
461 revs = [p.rev() for p in ctx.parents()]
663
462
664 return len(repo.revs('only(%ld, %s)', revs, tag)) + offset
463 return len(repo.revs('only(%ld, %s)', revs, tag)) + offset
665
464
666 # teach templater latesttags.changes is switched to (context, mapping) API
465 # teach templater latesttags.changes is switched to (context, mapping) API
667 _showchangessincetag._requires = {'repo', 'ctx'}
466 _showchangessincetag._requires = {'repo', 'ctx'}
668
467
669 @templatekeyword('manifest', requires={'repo', 'ctx', 'templ'})
468 @templatekeyword('manifest', requires={'repo', 'ctx', 'templ'})
670 def showmanifest(context, mapping):
469 def showmanifest(context, mapping):
671 repo = context.resource(mapping, 'repo')
470 repo = context.resource(mapping, 'repo')
672 ctx = context.resource(mapping, 'ctx')
471 ctx = context.resource(mapping, 'ctx')
673 templ = context.resource(mapping, 'templ')
472 templ = context.resource(mapping, 'templ')
674 mnode = ctx.manifestnode()
473 mnode = ctx.manifestnode()
675 if mnode is None:
474 if mnode is None:
676 # just avoid crash, we might want to use the 'ff...' hash in future
475 # just avoid crash, we might want to use the 'ff...' hash in future
677 return
476 return
678 mrev = repo.manifestlog._revlog.rev(mnode)
477 mrev = repo.manifestlog._revlog.rev(mnode)
679 mhex = hex(mnode)
478 mhex = hex(mnode)
680 mapping = mapping.copy()
479 mapping = mapping.copy()
681 mapping.update({'rev': mrev, 'node': mhex})
480 mapping.update({'rev': mrev, 'node': mhex})
682 f = templ('manifest', **pycompat.strkwargs(mapping))
481 f = templ('manifest', **pycompat.strkwargs(mapping))
683 # TODO: perhaps 'ctx' should be dropped from mapping because manifest
482 # TODO: perhaps 'ctx' should be dropped from mapping because manifest
684 # rev and node are completely different from changeset's.
483 # rev and node are completely different from changeset's.
685 return _mappable(f, None, f, lambda x: {'rev': mrev, 'node': mhex})
484 return _mappable(f, None, f, lambda x: {'rev': mrev, 'node': mhex})
686
485
687 @templatekeyword('obsfate', requires={'ui', 'repo', 'ctx', 'templ'})
486 @templatekeyword('obsfate', requires={'ui', 'repo', 'ctx', 'templ'})
688 def showobsfate(context, mapping):
487 def showobsfate(context, mapping):
689 # this function returns a list containing pre-formatted obsfate strings.
488 # this function returns a list containing pre-formatted obsfate strings.
690 #
489 #
691 # This function will be replaced by templates fragments when we will have
490 # This function will be replaced by templates fragments when we will have
692 # the verbosity templatekw available.
491 # the verbosity templatekw available.
693 succsandmarkers = showsuccsandmarkers(context, mapping)
492 succsandmarkers = showsuccsandmarkers(context, mapping)
694
493
695 ui = context.resource(mapping, 'ui')
494 ui = context.resource(mapping, 'ui')
696 values = []
495 values = []
697
496
698 for x in succsandmarkers:
497 for x in succsandmarkers:
699 values.append(obsutil.obsfateprinter(x['successors'], x['markers'], ui))
498 values.append(obsutil.obsfateprinter(x['successors'], x['markers'], ui))
700
499
701 return compatlist(context, mapping, "fate", values)
500 return compatlist(context, mapping, "fate", values)
702
501
703 def shownames(context, mapping, namespace):
502 def shownames(context, mapping, namespace):
704 """helper method to generate a template keyword for a namespace"""
503 """helper method to generate a template keyword for a namespace"""
705 repo = context.resource(mapping, 'repo')
504 repo = context.resource(mapping, 'repo')
706 ctx = context.resource(mapping, 'ctx')
505 ctx = context.resource(mapping, 'ctx')
707 ns = repo.names[namespace]
506 ns = repo.names[namespace]
708 names = ns.names(repo, ctx.node())
507 names = ns.names(repo, ctx.node())
709 return compatlist(context, mapping, ns.templatename, names,
508 return compatlist(context, mapping, ns.templatename, names,
710 plural=namespace)
509 plural=namespace)
711
510
712 @templatekeyword('namespaces', requires={'repo', 'ctx', 'templ'})
511 @templatekeyword('namespaces', requires={'repo', 'ctx', 'templ'})
713 def shownamespaces(context, mapping):
512 def shownamespaces(context, mapping):
714 """Dict of lists. Names attached to this changeset per
513 """Dict of lists. Names attached to this changeset per
715 namespace."""
514 namespace."""
716 repo = context.resource(mapping, 'repo')
515 repo = context.resource(mapping, 'repo')
717 ctx = context.resource(mapping, 'ctx')
516 ctx = context.resource(mapping, 'ctx')
718 templ = context.resource(mapping, 'templ')
517 templ = context.resource(mapping, 'templ')
719
518
720 namespaces = util.sortdict()
519 namespaces = util.sortdict()
721 def makensmapfn(ns):
520 def makensmapfn(ns):
722 # 'name' for iterating over namespaces, templatename for local reference
521 # 'name' for iterating over namespaces, templatename for local reference
723 return lambda v: {'name': v, ns.templatename: v}
522 return lambda v: {'name': v, ns.templatename: v}
724
523
725 for k, ns in repo.names.iteritems():
524 for k, ns in repo.names.iteritems():
726 names = ns.names(repo, ctx.node())
525 names = ns.names(repo, ctx.node())
727 f = _showlist('name', names, templ, mapping)
526 f = _showlist('name', names, templ, mapping)
728 namespaces[k] = _hybrid(f, names, makensmapfn(ns), pycompat.identity)
527 namespaces[k] = _hybrid(f, names, makensmapfn(ns), pycompat.identity)
729
528
730 f = _showlist('namespace', list(namespaces), templ, mapping)
529 f = _showlist('namespace', list(namespaces), templ, mapping)
731
530
732 def makemap(ns):
531 def makemap(ns):
733 return {
532 return {
734 'namespace': ns,
533 'namespace': ns,
735 'names': namespaces[ns],
534 'names': namespaces[ns],
736 'builtin': repo.names[ns].builtin,
535 'builtin': repo.names[ns].builtin,
737 'colorname': repo.names[ns].colorname,
536 'colorname': repo.names[ns].colorname,
738 }
537 }
739
538
740 return _hybrid(f, namespaces, makemap, pycompat.identity)
539 return _hybrid(f, namespaces, makemap, pycompat.identity)
741
540
742 @templatekeyword('node', requires={'ctx'})
541 @templatekeyword('node', requires={'ctx'})
743 def shownode(context, mapping):
542 def shownode(context, mapping):
744 """String. The changeset identification hash, as a 40 hexadecimal
543 """String. The changeset identification hash, as a 40 hexadecimal
745 digit string.
544 digit string.
746 """
545 """
747 ctx = context.resource(mapping, 'ctx')
546 ctx = context.resource(mapping, 'ctx')
748 return ctx.hex()
547 return ctx.hex()
749
548
750 @templatekeyword('obsolete', requires={'ctx'})
549 @templatekeyword('obsolete', requires={'ctx'})
751 def showobsolete(context, mapping):
550 def showobsolete(context, mapping):
752 """String. Whether the changeset is obsolete. (EXPERIMENTAL)"""
551 """String. Whether the changeset is obsolete. (EXPERIMENTAL)"""
753 ctx = context.resource(mapping, 'ctx')
552 ctx = context.resource(mapping, 'ctx')
754 if ctx.obsolete():
553 if ctx.obsolete():
755 return 'obsolete'
554 return 'obsolete'
756 return ''
555 return ''
757
556
758 @templatekeyword('peerurls', requires={'repo'})
557 @templatekeyword('peerurls', requires={'repo'})
759 def showpeerurls(context, mapping):
558 def showpeerurls(context, mapping):
760 """A dictionary of repository locations defined in the [paths] section
559 """A dictionary of repository locations defined in the [paths] section
761 of your configuration file."""
560 of your configuration file."""
762 repo = context.resource(mapping, 'repo')
561 repo = context.resource(mapping, 'repo')
763 # see commands.paths() for naming of dictionary keys
562 # see commands.paths() for naming of dictionary keys
764 paths = repo.ui.paths
563 paths = repo.ui.paths
765 urls = util.sortdict((k, p.rawloc) for k, p in sorted(paths.iteritems()))
564 urls = util.sortdict((k, p.rawloc) for k, p in sorted(paths.iteritems()))
766 def makemap(k):
565 def makemap(k):
767 p = paths[k]
566 p = paths[k]
768 d = {'name': k, 'url': p.rawloc}
567 d = {'name': k, 'url': p.rawloc}
769 d.update((o, v) for o, v in sorted(p.suboptions.iteritems()))
568 d.update((o, v) for o, v in sorted(p.suboptions.iteritems()))
770 return d
569 return d
771 return _hybrid(None, urls, makemap, lambda k: '%s=%s' % (k, urls[k]))
570 return _hybrid(None, urls, makemap, lambda k: '%s=%s' % (k, urls[k]))
772
571
773 @templatekeyword("predecessors", requires={'repo', 'ctx'})
572 @templatekeyword("predecessors", requires={'repo', 'ctx'})
774 def showpredecessors(context, mapping):
573 def showpredecessors(context, mapping):
775 """Returns the list if the closest visible successors. (EXPERIMENTAL)"""
574 """Returns the list if the closest visible successors. (EXPERIMENTAL)"""
776 repo = context.resource(mapping, 'repo')
575 repo = context.resource(mapping, 'repo')
777 ctx = context.resource(mapping, 'ctx')
576 ctx = context.resource(mapping, 'ctx')
778 predecessors = sorted(obsutil.closestpredecessors(repo, ctx.node()))
577 predecessors = sorted(obsutil.closestpredecessors(repo, ctx.node()))
779 predecessors = map(hex, predecessors)
578 predecessors = map(hex, predecessors)
780
579
781 return _hybrid(None, predecessors,
580 return _hybrid(None, predecessors,
782 lambda x: {'ctx': repo[x], 'revcache': {}},
581 lambda x: {'ctx': repo[x], 'revcache': {}},
783 lambda x: scmutil.formatchangeid(repo[x]))
582 lambda x: scmutil.formatchangeid(repo[x]))
784
583
785 @templatekeyword('reporoot', requires={'repo'})
584 @templatekeyword('reporoot', requires={'repo'})
786 def showreporoot(context, mapping):
585 def showreporoot(context, mapping):
787 """String. The root directory of the current repository."""
586 """String. The root directory of the current repository."""
788 repo = context.resource(mapping, 'repo')
587 repo = context.resource(mapping, 'repo')
789 return repo.root
588 return repo.root
790
589
791 @templatekeyword("successorssets", requires={'repo', 'ctx'})
590 @templatekeyword("successorssets", requires={'repo', 'ctx'})
792 def showsuccessorssets(context, mapping):
591 def showsuccessorssets(context, mapping):
793 """Returns a string of sets of successors for a changectx. Format used
592 """Returns a string of sets of successors for a changectx. Format used
794 is: [ctx1, ctx2], [ctx3] if ctx has been splitted into ctx1 and ctx2
593 is: [ctx1, ctx2], [ctx3] if ctx has been splitted into ctx1 and ctx2
795 while also diverged into ctx3. (EXPERIMENTAL)"""
594 while also diverged into ctx3. (EXPERIMENTAL)"""
796 repo = context.resource(mapping, 'repo')
595 repo = context.resource(mapping, 'repo')
797 ctx = context.resource(mapping, 'ctx')
596 ctx = context.resource(mapping, 'ctx')
798 if not ctx.obsolete():
597 if not ctx.obsolete():
799 return ''
598 return ''
800
599
801 ssets = obsutil.successorssets(repo, ctx.node(), closest=True)
600 ssets = obsutil.successorssets(repo, ctx.node(), closest=True)
802 ssets = [[hex(n) for n in ss] for ss in ssets]
601 ssets = [[hex(n) for n in ss] for ss in ssets]
803
602
804 data = []
603 data = []
805 for ss in ssets:
604 for ss in ssets:
806 h = _hybrid(None, ss, lambda x: {'ctx': repo[x], 'revcache': {}},
605 h = _hybrid(None, ss, lambda x: {'ctx': repo[x], 'revcache': {}},
807 lambda x: scmutil.formatchangeid(repo[x]))
606 lambda x: scmutil.formatchangeid(repo[x]))
808 data.append(h)
607 data.append(h)
809
608
810 # Format the successorssets
609 # Format the successorssets
811 def render(d):
610 def render(d):
812 t = []
611 t = []
813 for i in d.gen():
612 for i in d.gen():
814 t.append(i)
613 t.append(i)
815 return "".join(t)
614 return "".join(t)
816
615
817 def gen(data):
616 def gen(data):
818 yield "; ".join(render(d) for d in data)
617 yield "; ".join(render(d) for d in data)
819
618
820 return _hybrid(gen(data), data, lambda x: {'successorset': x},
619 return _hybrid(gen(data), data, lambda x: {'successorset': x},
821 pycompat.identity)
620 pycompat.identity)
822
621
823 @templatekeyword("succsandmarkers", requires={'repo', 'ctx', 'templ'})
622 @templatekeyword("succsandmarkers", requires={'repo', 'ctx', 'templ'})
824 def showsuccsandmarkers(context, mapping):
623 def showsuccsandmarkers(context, mapping):
825 """Returns a list of dict for each final successor of ctx. The dict
624 """Returns a list of dict for each final successor of ctx. The dict
826 contains successors node id in "successors" keys and the list of
625 contains successors node id in "successors" keys and the list of
827 obs-markers from ctx to the set of successors in "markers".
626 obs-markers from ctx to the set of successors in "markers".
828 (EXPERIMENTAL)
627 (EXPERIMENTAL)
829 """
628 """
830 repo = context.resource(mapping, 'repo')
629 repo = context.resource(mapping, 'repo')
831 ctx = context.resource(mapping, 'ctx')
630 ctx = context.resource(mapping, 'ctx')
832 templ = context.resource(mapping, 'templ')
631 templ = context.resource(mapping, 'templ')
833
632
834 values = obsutil.successorsandmarkers(repo, ctx)
633 values = obsutil.successorsandmarkers(repo, ctx)
835
634
836 if values is None:
635 if values is None:
837 values = []
636 values = []
838
637
839 # Format successors and markers to avoid exposing binary to templates
638 # Format successors and markers to avoid exposing binary to templates
840 data = []
639 data = []
841 for i in values:
640 for i in values:
842 # Format successors
641 # Format successors
843 successors = i['successors']
642 successors = i['successors']
844
643
845 successors = [hex(n) for n in successors]
644 successors = [hex(n) for n in successors]
846 successors = _hybrid(None, successors,
645 successors = _hybrid(None, successors,
847 lambda x: {'ctx': repo[x], 'revcache': {}},
646 lambda x: {'ctx': repo[x], 'revcache': {}},
848 lambda x: scmutil.formatchangeid(repo[x]))
647 lambda x: scmutil.formatchangeid(repo[x]))
849
648
850 # Format markers
649 # Format markers
851 finalmarkers = []
650 finalmarkers = []
852 for m in i['markers']:
651 for m in i['markers']:
853 hexprec = hex(m[0])
652 hexprec = hex(m[0])
854 hexsucs = tuple(hex(n) for n in m[1])
653 hexsucs = tuple(hex(n) for n in m[1])
855 hexparents = None
654 hexparents = None
856 if m[5] is not None:
655 if m[5] is not None:
857 hexparents = tuple(hex(n) for n in m[5])
656 hexparents = tuple(hex(n) for n in m[5])
858 newmarker = (hexprec, hexsucs) + m[2:5] + (hexparents,) + m[6:]
657 newmarker = (hexprec, hexsucs) + m[2:5] + (hexparents,) + m[6:]
859 finalmarkers.append(newmarker)
658 finalmarkers.append(newmarker)
860
659
861 data.append({'successors': successors, 'markers': finalmarkers})
660 data.append({'successors': successors, 'markers': finalmarkers})
862
661
863 f = _showlist('succsandmarkers', data, templ, mapping)
662 f = _showlist('succsandmarkers', data, templ, mapping)
864 return _hybrid(f, data, lambda x: x, pycompat.identity)
663 return _hybrid(f, data, lambda x: x, pycompat.identity)
865
664
866 @templatekeyword('p1rev', requires={'ctx'})
665 @templatekeyword('p1rev', requires={'ctx'})
867 def showp1rev(context, mapping):
666 def showp1rev(context, mapping):
868 """Integer. The repository-local revision number of the changeset's
667 """Integer. The repository-local revision number of the changeset's
869 first parent, or -1 if the changeset has no parents."""
668 first parent, or -1 if the changeset has no parents."""
870 ctx = context.resource(mapping, 'ctx')
669 ctx = context.resource(mapping, 'ctx')
871 return ctx.p1().rev()
670 return ctx.p1().rev()
872
671
873 @templatekeyword('p2rev', requires={'ctx'})
672 @templatekeyword('p2rev', requires={'ctx'})
874 def showp2rev(context, mapping):
673 def showp2rev(context, mapping):
875 """Integer. The repository-local revision number of the changeset's
674 """Integer. The repository-local revision number of the changeset's
876 second parent, or -1 if the changeset has no second parent."""
675 second parent, or -1 if the changeset has no second parent."""
877 ctx = context.resource(mapping, 'ctx')
676 ctx = context.resource(mapping, 'ctx')
878 return ctx.p2().rev()
677 return ctx.p2().rev()
879
678
880 @templatekeyword('p1node', requires={'ctx'})
679 @templatekeyword('p1node', requires={'ctx'})
881 def showp1node(context, mapping):
680 def showp1node(context, mapping):
882 """String. The identification hash of the changeset's first parent,
681 """String. The identification hash of the changeset's first parent,
883 as a 40 digit hexadecimal string. If the changeset has no parents, all
682 as a 40 digit hexadecimal string. If the changeset has no parents, all
884 digits are 0."""
683 digits are 0."""
885 ctx = context.resource(mapping, 'ctx')
684 ctx = context.resource(mapping, 'ctx')
886 return ctx.p1().hex()
685 return ctx.p1().hex()
887
686
888 @templatekeyword('p2node', requires={'ctx'})
687 @templatekeyword('p2node', requires={'ctx'})
889 def showp2node(context, mapping):
688 def showp2node(context, mapping):
890 """String. The identification hash of the changeset's second
689 """String. The identification hash of the changeset's second
891 parent, as a 40 digit hexadecimal string. If the changeset has no second
690 parent, as a 40 digit hexadecimal string. If the changeset has no second
892 parent, all digits are 0."""
691 parent, all digits are 0."""
893 ctx = context.resource(mapping, 'ctx')
692 ctx = context.resource(mapping, 'ctx')
894 return ctx.p2().hex()
693 return ctx.p2().hex()
895
694
896 @templatekeyword('parents', requires={'repo', 'ctx', 'templ'})
695 @templatekeyword('parents', requires={'repo', 'ctx', 'templ'})
897 def showparents(context, mapping):
696 def showparents(context, mapping):
898 """List of strings. The parents of the changeset in "rev:node"
697 """List of strings. The parents of the changeset in "rev:node"
899 format. If the changeset has only one "natural" parent (the predecessor
698 format. If the changeset has only one "natural" parent (the predecessor
900 revision) nothing is shown."""
699 revision) nothing is shown."""
901 repo = context.resource(mapping, 'repo')
700 repo = context.resource(mapping, 'repo')
902 ctx = context.resource(mapping, 'ctx')
701 ctx = context.resource(mapping, 'ctx')
903 templ = context.resource(mapping, 'templ')
702 templ = context.resource(mapping, 'templ')
904 pctxs = scmutil.meaningfulparents(repo, ctx)
703 pctxs = scmutil.meaningfulparents(repo, ctx)
905 prevs = [p.rev() for p in pctxs]
704 prevs = [p.rev() for p in pctxs]
906 parents = [[('rev', p.rev()),
705 parents = [[('rev', p.rev()),
907 ('node', p.hex()),
706 ('node', p.hex()),
908 ('phase', p.phasestr())]
707 ('phase', p.phasestr())]
909 for p in pctxs]
708 for p in pctxs]
910 f = _showlist('parent', parents, templ, mapping)
709 f = _showlist('parent', parents, templ, mapping)
911 return _hybrid(f, prevs, lambda x: {'ctx': repo[x], 'revcache': {}},
710 return _hybrid(f, prevs, lambda x: {'ctx': repo[x], 'revcache': {}},
912 lambda x: scmutil.formatchangeid(repo[x]), keytype=int)
711 lambda x: scmutil.formatchangeid(repo[x]), keytype=int)
913
712
914 @templatekeyword('phase', requires={'ctx'})
713 @templatekeyword('phase', requires={'ctx'})
915 def showphase(context, mapping):
714 def showphase(context, mapping):
916 """String. The changeset phase name."""
715 """String. The changeset phase name."""
917 ctx = context.resource(mapping, 'ctx')
716 ctx = context.resource(mapping, 'ctx')
918 return ctx.phasestr()
717 return ctx.phasestr()
919
718
920 @templatekeyword('phaseidx', requires={'ctx'})
719 @templatekeyword('phaseidx', requires={'ctx'})
921 def showphaseidx(context, mapping):
720 def showphaseidx(context, mapping):
922 """Integer. The changeset phase index. (ADVANCED)"""
721 """Integer. The changeset phase index. (ADVANCED)"""
923 ctx = context.resource(mapping, 'ctx')
722 ctx = context.resource(mapping, 'ctx')
924 return ctx.phase()
723 return ctx.phase()
925
724
926 @templatekeyword('rev', requires={'ctx'})
725 @templatekeyword('rev', requires={'ctx'})
927 def showrev(context, mapping):
726 def showrev(context, mapping):
928 """Integer. The repository-local changeset revision number."""
727 """Integer. The repository-local changeset revision number."""
929 ctx = context.resource(mapping, 'ctx')
728 ctx = context.resource(mapping, 'ctx')
930 return scmutil.intrev(ctx)
729 return scmutil.intrev(ctx)
931
730
932 def showrevslist(context, mapping, name, revs):
731 def showrevslist(context, mapping, name, revs):
933 """helper to generate a list of revisions in which a mapped template will
732 """helper to generate a list of revisions in which a mapped template will
934 be evaluated"""
733 be evaluated"""
935 repo = context.resource(mapping, 'repo')
734 repo = context.resource(mapping, 'repo')
936 templ = context.resource(mapping, 'templ')
735 templ = context.resource(mapping, 'templ')
937 f = _showlist(name, ['%d' % r for r in revs], templ, mapping)
736 f = _showlist(name, ['%d' % r for r in revs], templ, mapping)
938 return _hybrid(f, revs,
737 return _hybrid(f, revs,
939 lambda x: {name: x, 'ctx': repo[x], 'revcache': {}},
738 lambda x: {name: x, 'ctx': repo[x], 'revcache': {}},
940 pycompat.identity, keytype=int)
739 pycompat.identity, keytype=int)
941
740
942 @templatekeyword('subrepos', requires={'ctx', 'templ'})
741 @templatekeyword('subrepos', requires={'ctx', 'templ'})
943 def showsubrepos(context, mapping):
742 def showsubrepos(context, mapping):
944 """List of strings. Updated subrepositories in the changeset."""
743 """List of strings. Updated subrepositories in the changeset."""
945 ctx = context.resource(mapping, 'ctx')
744 ctx = context.resource(mapping, 'ctx')
946 substate = ctx.substate
745 substate = ctx.substate
947 if not substate:
746 if not substate:
948 return compatlist(context, mapping, 'subrepo', [])
747 return compatlist(context, mapping, 'subrepo', [])
949 psubstate = ctx.parents()[0].substate or {}
748 psubstate = ctx.parents()[0].substate or {}
950 subrepos = []
749 subrepos = []
951 for sub in substate:
750 for sub in substate:
952 if sub not in psubstate or substate[sub] != psubstate[sub]:
751 if sub not in psubstate or substate[sub] != psubstate[sub]:
953 subrepos.append(sub) # modified or newly added in ctx
752 subrepos.append(sub) # modified or newly added in ctx
954 for sub in psubstate:
753 for sub in psubstate:
955 if sub not in substate:
754 if sub not in substate:
956 subrepos.append(sub) # removed in ctx
755 subrepos.append(sub) # removed in ctx
957 return compatlist(context, mapping, 'subrepo', sorted(subrepos))
756 return compatlist(context, mapping, 'subrepo', sorted(subrepos))
958
757
959 # don't remove "showtags" definition, even though namespaces will put
758 # don't remove "showtags" definition, even though namespaces will put
960 # a helper function for "tags" keyword into "keywords" map automatically,
759 # a helper function for "tags" keyword into "keywords" map automatically,
961 # because online help text is built without namespaces initialization
760 # because online help text is built without namespaces initialization
962 @templatekeyword('tags', requires={'repo', 'ctx', 'templ'})
761 @templatekeyword('tags', requires={'repo', 'ctx', 'templ'})
963 def showtags(context, mapping):
762 def showtags(context, mapping):
964 """List of strings. Any tags associated with the changeset."""
763 """List of strings. Any tags associated with the changeset."""
965 return shownames(context, mapping, 'tags')
764 return shownames(context, mapping, 'tags')
966
765
967 @templatekeyword('termwidth', requires={'ui'})
766 @templatekeyword('termwidth', requires={'ui'})
968 def showtermwidth(context, mapping):
767 def showtermwidth(context, mapping):
969 """Integer. The width of the current terminal."""
768 """Integer. The width of the current terminal."""
970 ui = context.resource(mapping, 'ui')
769 ui = context.resource(mapping, 'ui')
971 return ui.termwidth()
770 return ui.termwidth()
972
771
973 @templatekeyword('instabilities', requires={'ctx', 'templ'})
772 @templatekeyword('instabilities', requires={'ctx', 'templ'})
974 def showinstabilities(context, mapping):
773 def showinstabilities(context, mapping):
975 """List of strings. Evolution instabilities affecting the changeset.
774 """List of strings. Evolution instabilities affecting the changeset.
976 (EXPERIMENTAL)
775 (EXPERIMENTAL)
977 """
776 """
978 ctx = context.resource(mapping, 'ctx')
777 ctx = context.resource(mapping, 'ctx')
979 return compatlist(context, mapping, 'instability', ctx.instabilities(),
778 return compatlist(context, mapping, 'instability', ctx.instabilities(),
980 plural='instabilities')
779 plural='instabilities')
981
780
982 @templatekeyword('verbosity', requires={'ui'})
781 @templatekeyword('verbosity', requires={'ui'})
983 def showverbosity(context, mapping):
782 def showverbosity(context, mapping):
984 """String. The current output verbosity in 'debug', 'quiet', 'verbose',
783 """String. The current output verbosity in 'debug', 'quiet', 'verbose',
985 or ''."""
784 or ''."""
986 ui = context.resource(mapping, 'ui')
785 ui = context.resource(mapping, 'ui')
987 # see logcmdutil.changesettemplater for priority of these flags
786 # see logcmdutil.changesettemplater for priority of these flags
988 if ui.debugflag:
787 if ui.debugflag:
989 return 'debug'
788 return 'debug'
990 elif ui.quiet:
789 elif ui.quiet:
991 return 'quiet'
790 return 'quiet'
992 elif ui.verbose:
791 elif ui.verbose:
993 return 'verbose'
792 return 'verbose'
994 return ''
793 return ''
995
794
996 def loadkeyword(ui, extname, registrarobj):
795 def loadkeyword(ui, extname, registrarobj):
997 """Load template keyword from specified registrarobj
796 """Load template keyword from specified registrarobj
998 """
797 """
999 for name, func in registrarobj._table.iteritems():
798 for name, func in registrarobj._table.iteritems():
1000 keywords[name] = func
799 keywords[name] = func
1001
800
1002 # tell hggettext to extract docstrings from these functions:
801 # tell hggettext to extract docstrings from these functions:
1003 i18nfunctions = keywords.values()
802 i18nfunctions = keywords.values()
@@ -1,1444 +1,1444 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
12
13 from .i18n import _
13 from .i18n import _
14 from . import (
14 from . import (
15 color,
15 color,
16 config,
16 config,
17 encoding,
17 encoding,
18 error,
18 error,
19 minirst,
19 minirst,
20 obsutil,
20 obsutil,
21 parser,
21 parser,
22 pycompat,
22 pycompat,
23 registrar,
23 registrar,
24 revset as revsetmod,
24 revset as revsetmod,
25 revsetlang,
25 revsetlang,
26 scmutil,
26 scmutil,
27 templatefilters,
27 templatefilters,
28 templatekw,
28 templatekw,
29 templateutil,
29 templateutil,
30 util,
30 util,
31 )
31 )
32 from .utils import dateutil
32 from .utils import dateutil
33
33
34 evalrawexp = templateutil.evalrawexp
34 evalrawexp = templateutil.evalrawexp
35 evalfuncarg = templateutil.evalfuncarg
35 evalfuncarg = templateutil.evalfuncarg
36 evalboolean = templateutil.evalboolean
36 evalboolean = templateutil.evalboolean
37 evalinteger = templateutil.evalinteger
37 evalinteger = templateutil.evalinteger
38 evalstring = templateutil.evalstring
38 evalstring = templateutil.evalstring
39 evalstringliteral = templateutil.evalstringliteral
39 evalstringliteral = templateutil.evalstringliteral
40 evalastype = templateutil.evalastype
40 evalastype = templateutil.evalastype
41
41
42 # template parsing
42 # template parsing
43
43
44 elements = {
44 elements = {
45 # token-type: binding-strength, primary, prefix, infix, suffix
45 # token-type: binding-strength, primary, prefix, infix, suffix
46 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
46 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
47 ".": (18, None, None, (".", 18), None),
47 ".": (18, None, None, (".", 18), None),
48 "%": (15, None, None, ("%", 15), None),
48 "%": (15, None, None, ("%", 15), None),
49 "|": (15, None, None, ("|", 15), None),
49 "|": (15, None, None, ("|", 15), None),
50 "*": (5, None, None, ("*", 5), None),
50 "*": (5, None, None, ("*", 5), None),
51 "/": (5, None, None, ("/", 5), None),
51 "/": (5, None, None, ("/", 5), None),
52 "+": (4, None, None, ("+", 4), None),
52 "+": (4, None, None, ("+", 4), None),
53 "-": (4, None, ("negate", 19), ("-", 4), None),
53 "-": (4, None, ("negate", 19), ("-", 4), None),
54 "=": (3, None, None, ("keyvalue", 3), None),
54 "=": (3, None, None, ("keyvalue", 3), None),
55 ",": (2, None, None, ("list", 2), None),
55 ",": (2, None, None, ("list", 2), None),
56 ")": (0, None, None, None, None),
56 ")": (0, None, None, None, None),
57 "integer": (0, "integer", None, None, None),
57 "integer": (0, "integer", None, None, None),
58 "symbol": (0, "symbol", None, None, None),
58 "symbol": (0, "symbol", None, None, None),
59 "string": (0, "string", None, None, None),
59 "string": (0, "string", None, None, None),
60 "template": (0, "template", None, None, None),
60 "template": (0, "template", None, None, None),
61 "end": (0, None, None, None, None),
61 "end": (0, None, None, None, None),
62 }
62 }
63
63
64 def tokenize(program, start, end, term=None):
64 def tokenize(program, start, end, term=None):
65 """Parse a template expression into a stream of tokens, which must end
65 """Parse a template expression into a stream of tokens, which must end
66 with term if specified"""
66 with term if specified"""
67 pos = start
67 pos = start
68 program = pycompat.bytestr(program)
68 program = pycompat.bytestr(program)
69 while pos < end:
69 while pos < end:
70 c = program[pos]
70 c = program[pos]
71 if c.isspace(): # skip inter-token whitespace
71 if c.isspace(): # skip inter-token whitespace
72 pass
72 pass
73 elif c in "(=,).%|+-*/": # handle simple operators
73 elif c in "(=,).%|+-*/": # handle simple operators
74 yield (c, None, pos)
74 yield (c, None, pos)
75 elif c in '"\'': # handle quoted templates
75 elif c in '"\'': # handle quoted templates
76 s = pos + 1
76 s = pos + 1
77 data, pos = _parsetemplate(program, s, end, c)
77 data, pos = _parsetemplate(program, s, end, c)
78 yield ('template', data, s)
78 yield ('template', data, s)
79 pos -= 1
79 pos -= 1
80 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
80 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
81 # handle quoted strings
81 # handle quoted strings
82 c = program[pos + 1]
82 c = program[pos + 1]
83 s = pos = pos + 2
83 s = pos = pos + 2
84 while pos < end: # find closing quote
84 while pos < end: # find closing quote
85 d = program[pos]
85 d = program[pos]
86 if d == '\\': # skip over escaped characters
86 if d == '\\': # skip over escaped characters
87 pos += 2
87 pos += 2
88 continue
88 continue
89 if d == c:
89 if d == c:
90 yield ('string', program[s:pos], s)
90 yield ('string', program[s:pos], s)
91 break
91 break
92 pos += 1
92 pos += 1
93 else:
93 else:
94 raise error.ParseError(_("unterminated string"), s)
94 raise error.ParseError(_("unterminated string"), s)
95 elif c.isdigit():
95 elif c.isdigit():
96 s = pos
96 s = pos
97 while pos < end:
97 while pos < end:
98 d = program[pos]
98 d = program[pos]
99 if not d.isdigit():
99 if not d.isdigit():
100 break
100 break
101 pos += 1
101 pos += 1
102 yield ('integer', program[s:pos], s)
102 yield ('integer', program[s:pos], s)
103 pos -= 1
103 pos -= 1
104 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
104 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
105 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
105 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
106 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
106 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
107 # where some of nested templates were preprocessed as strings and
107 # where some of nested templates were preprocessed as strings and
108 # then compiled. therefore, \"...\" was allowed. (issue4733)
108 # then compiled. therefore, \"...\" was allowed. (issue4733)
109 #
109 #
110 # processing flow of _evalifliteral() at 5ab28a2e9962:
110 # processing flow of _evalifliteral() at 5ab28a2e9962:
111 # outer template string -> stringify() -> compiletemplate()
111 # outer template string -> stringify() -> compiletemplate()
112 # ------------------------ ------------ ------------------
112 # ------------------------ ------------ ------------------
113 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
113 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
114 # ~~~~~~~~
114 # ~~~~~~~~
115 # escaped quoted string
115 # escaped quoted string
116 if c == 'r':
116 if c == 'r':
117 pos += 1
117 pos += 1
118 token = 'string'
118 token = 'string'
119 else:
119 else:
120 token = 'template'
120 token = 'template'
121 quote = program[pos:pos + 2]
121 quote = program[pos:pos + 2]
122 s = pos = pos + 2
122 s = pos = pos + 2
123 while pos < end: # find closing escaped quote
123 while pos < end: # find closing escaped quote
124 if program.startswith('\\\\\\', pos, end):
124 if program.startswith('\\\\\\', pos, end):
125 pos += 4 # skip over double escaped characters
125 pos += 4 # skip over double escaped characters
126 continue
126 continue
127 if program.startswith(quote, pos, end):
127 if program.startswith(quote, pos, end):
128 # interpret as if it were a part of an outer string
128 # interpret as if it were a part of an outer string
129 data = parser.unescapestr(program[s:pos])
129 data = parser.unescapestr(program[s:pos])
130 if token == 'template':
130 if token == 'template':
131 data = _parsetemplate(data, 0, len(data))[0]
131 data = _parsetemplate(data, 0, len(data))[0]
132 yield (token, data, s)
132 yield (token, data, s)
133 pos += 1
133 pos += 1
134 break
134 break
135 pos += 1
135 pos += 1
136 else:
136 else:
137 raise error.ParseError(_("unterminated string"), s)
137 raise error.ParseError(_("unterminated string"), s)
138 elif c.isalnum() or c in '_':
138 elif c.isalnum() or c in '_':
139 s = pos
139 s = pos
140 pos += 1
140 pos += 1
141 while pos < end: # find end of symbol
141 while pos < end: # find end of symbol
142 d = program[pos]
142 d = program[pos]
143 if not (d.isalnum() or d == "_"):
143 if not (d.isalnum() or d == "_"):
144 break
144 break
145 pos += 1
145 pos += 1
146 sym = program[s:pos]
146 sym = program[s:pos]
147 yield ('symbol', sym, s)
147 yield ('symbol', sym, s)
148 pos -= 1
148 pos -= 1
149 elif c == term:
149 elif c == term:
150 yield ('end', None, pos)
150 yield ('end', None, pos)
151 return
151 return
152 else:
152 else:
153 raise error.ParseError(_("syntax error"), pos)
153 raise error.ParseError(_("syntax error"), pos)
154 pos += 1
154 pos += 1
155 if term:
155 if term:
156 raise error.ParseError(_("unterminated template expansion"), start)
156 raise error.ParseError(_("unterminated template expansion"), start)
157 yield ('end', None, pos)
157 yield ('end', None, pos)
158
158
159 def _parsetemplate(tmpl, start, stop, quote=''):
159 def _parsetemplate(tmpl, start, stop, quote=''):
160 r"""
160 r"""
161 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
161 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
162 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
162 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
163 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
163 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
164 ([('string', 'foo'), ('symbol', 'bar')], 9)
164 ([('string', 'foo'), ('symbol', 'bar')], 9)
165 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
165 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
166 ([('string', 'foo')], 4)
166 ([('string', 'foo')], 4)
167 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
167 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
168 ([('string', 'foo"'), ('string', 'bar')], 9)
168 ([('string', 'foo"'), ('string', 'bar')], 9)
169 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
169 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
170 ([('string', 'foo\\')], 6)
170 ([('string', 'foo\\')], 6)
171 """
171 """
172 parsed = []
172 parsed = []
173 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
173 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
174 if typ == 'string':
174 if typ == 'string':
175 parsed.append((typ, val))
175 parsed.append((typ, val))
176 elif typ == 'template':
176 elif typ == 'template':
177 parsed.append(val)
177 parsed.append(val)
178 elif typ == 'end':
178 elif typ == 'end':
179 return parsed, pos
179 return parsed, pos
180 else:
180 else:
181 raise error.ProgrammingError('unexpected type: %s' % typ)
181 raise error.ProgrammingError('unexpected type: %s' % typ)
182 raise error.ProgrammingError('unterminated scanning of template')
182 raise error.ProgrammingError('unterminated scanning of template')
183
183
184 def scantemplate(tmpl, raw=False):
184 def scantemplate(tmpl, raw=False):
185 r"""Scan (type, start, end) positions of outermost elements in template
185 r"""Scan (type, start, end) positions of outermost elements in template
186
186
187 If raw=True, a backslash is not taken as an escape character just like
187 If raw=True, a backslash is not taken as an escape character just like
188 r'' string in Python. Note that this is different from r'' literal in
188 r'' string in Python. Note that this is different from r'' literal in
189 template in that no template fragment can appear in r'', e.g. r'{foo}'
189 template in that no template fragment can appear in r'', e.g. r'{foo}'
190 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
190 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
191 'foo'.
191 'foo'.
192
192
193 >>> list(scantemplate(b'foo{bar}"baz'))
193 >>> list(scantemplate(b'foo{bar}"baz'))
194 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
194 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
195 >>> list(scantemplate(b'outer{"inner"}outer'))
195 >>> list(scantemplate(b'outer{"inner"}outer'))
196 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
196 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
197 >>> list(scantemplate(b'foo\\{escaped}'))
197 >>> list(scantemplate(b'foo\\{escaped}'))
198 [('string', 0, 5), ('string', 5, 13)]
198 [('string', 0, 5), ('string', 5, 13)]
199 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
199 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
200 [('string', 0, 4), ('template', 4, 13)]
200 [('string', 0, 4), ('template', 4, 13)]
201 """
201 """
202 last = None
202 last = None
203 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
203 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
204 if last:
204 if last:
205 yield last + (pos,)
205 yield last + (pos,)
206 if typ == 'end':
206 if typ == 'end':
207 return
207 return
208 else:
208 else:
209 last = (typ, pos)
209 last = (typ, pos)
210 raise error.ProgrammingError('unterminated scanning of template')
210 raise error.ProgrammingError('unterminated scanning of template')
211
211
212 def _scantemplate(tmpl, start, stop, quote='', raw=False):
212 def _scantemplate(tmpl, start, stop, quote='', raw=False):
213 """Parse template string into chunks of strings and template expressions"""
213 """Parse template string into chunks of strings and template expressions"""
214 sepchars = '{' + quote
214 sepchars = '{' + quote
215 unescape = [parser.unescapestr, pycompat.identity][raw]
215 unescape = [parser.unescapestr, pycompat.identity][raw]
216 pos = start
216 pos = start
217 p = parser.parser(elements)
217 p = parser.parser(elements)
218 try:
218 try:
219 while pos < stop:
219 while pos < stop:
220 n = min((tmpl.find(c, pos, stop) for c in sepchars),
220 n = min((tmpl.find(c, pos, stop) for c in sepchars),
221 key=lambda n: (n < 0, n))
221 key=lambda n: (n < 0, n))
222 if n < 0:
222 if n < 0:
223 yield ('string', unescape(tmpl[pos:stop]), pos)
223 yield ('string', unescape(tmpl[pos:stop]), pos)
224 pos = stop
224 pos = stop
225 break
225 break
226 c = tmpl[n:n + 1]
226 c = tmpl[n:n + 1]
227 bs = 0 # count leading backslashes
227 bs = 0 # count leading backslashes
228 if not raw:
228 if not raw:
229 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
229 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
230 if bs % 2 == 1:
230 if bs % 2 == 1:
231 # escaped (e.g. '\{', '\\\{', but not '\\{')
231 # escaped (e.g. '\{', '\\\{', but not '\\{')
232 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
232 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
233 pos = n + 1
233 pos = n + 1
234 continue
234 continue
235 if n > pos:
235 if n > pos:
236 yield ('string', unescape(tmpl[pos:n]), pos)
236 yield ('string', unescape(tmpl[pos:n]), pos)
237 if c == quote:
237 if c == quote:
238 yield ('end', None, n + 1)
238 yield ('end', None, n + 1)
239 return
239 return
240
240
241 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
241 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
242 if not tmpl.startswith('}', pos):
242 if not tmpl.startswith('}', pos):
243 raise error.ParseError(_("invalid token"), pos)
243 raise error.ParseError(_("invalid token"), pos)
244 yield ('template', parseres, n)
244 yield ('template', parseres, n)
245 pos += 1
245 pos += 1
246
246
247 if quote:
247 if quote:
248 raise error.ParseError(_("unterminated string"), start)
248 raise error.ParseError(_("unterminated string"), start)
249 except error.ParseError as inst:
249 except error.ParseError as inst:
250 if len(inst.args) > 1: # has location
250 if len(inst.args) > 1: # has location
251 loc = inst.args[1]
251 loc = inst.args[1]
252 # Offset the caret location by the number of newlines before the
252 # Offset the caret location by the number of newlines before the
253 # location of the error, since we will replace one-char newlines
253 # location of the error, since we will replace one-char newlines
254 # with the two-char literal r'\n'.
254 # with the two-char literal r'\n'.
255 offset = tmpl[:loc].count('\n')
255 offset = tmpl[:loc].count('\n')
256 tmpl = tmpl.replace('\n', br'\n')
256 tmpl = tmpl.replace('\n', br'\n')
257 # We want the caret to point to the place in the template that
257 # We want the caret to point to the place in the template that
258 # failed to parse, but in a hint we get a open paren at the
258 # failed to parse, but in a hint we get a open paren at the
259 # start. Therefore, we print "loc + 1" spaces (instead of "loc")
259 # start. Therefore, we print "loc + 1" spaces (instead of "loc")
260 # to line up the caret with the location of the error.
260 # to line up the caret with the location of the error.
261 inst.hint = (tmpl + '\n'
261 inst.hint = (tmpl + '\n'
262 + ' ' * (loc + 1 + offset) + '^ ' + _('here'))
262 + ' ' * (loc + 1 + offset) + '^ ' + _('here'))
263 raise
263 raise
264 yield ('end', None, pos)
264 yield ('end', None, pos)
265
265
266 def _unnesttemplatelist(tree):
266 def _unnesttemplatelist(tree):
267 """Expand list of templates to node tuple
267 """Expand list of templates to node tuple
268
268
269 >>> def f(tree):
269 >>> def f(tree):
270 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
270 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
271 >>> f((b'template', []))
271 >>> f((b'template', []))
272 (string '')
272 (string '')
273 >>> f((b'template', [(b'string', b'foo')]))
273 >>> f((b'template', [(b'string', b'foo')]))
274 (string 'foo')
274 (string 'foo')
275 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
275 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
276 (template
276 (template
277 (string 'foo')
277 (string 'foo')
278 (symbol 'rev'))
278 (symbol 'rev'))
279 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
279 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
280 (template
280 (template
281 (symbol 'rev'))
281 (symbol 'rev'))
282 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
282 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
283 (string 'foo')
283 (string 'foo')
284 """
284 """
285 if not isinstance(tree, tuple):
285 if not isinstance(tree, tuple):
286 return tree
286 return tree
287 op = tree[0]
287 op = tree[0]
288 if op != 'template':
288 if op != 'template':
289 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
289 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
290
290
291 assert len(tree) == 2
291 assert len(tree) == 2
292 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
292 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
293 if not xs:
293 if not xs:
294 return ('string', '') # empty template ""
294 return ('string', '') # empty template ""
295 elif len(xs) == 1 and xs[0][0] == 'string':
295 elif len(xs) == 1 and xs[0][0] == 'string':
296 return xs[0] # fast path for string with no template fragment "x"
296 return xs[0] # fast path for string with no template fragment "x"
297 else:
297 else:
298 return (op,) + xs
298 return (op,) + xs
299
299
300 def parse(tmpl):
300 def parse(tmpl):
301 """Parse template string into tree"""
301 """Parse template string into tree"""
302 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
302 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
303 assert pos == len(tmpl), 'unquoted template should be consumed'
303 assert pos == len(tmpl), 'unquoted template should be consumed'
304 return _unnesttemplatelist(('template', parsed))
304 return _unnesttemplatelist(('template', parsed))
305
305
306 def _parseexpr(expr):
306 def _parseexpr(expr):
307 """Parse a template expression into tree
307 """Parse a template expression into tree
308
308
309 >>> _parseexpr(b'"foo"')
309 >>> _parseexpr(b'"foo"')
310 ('string', 'foo')
310 ('string', 'foo')
311 >>> _parseexpr(b'foo(bar)')
311 >>> _parseexpr(b'foo(bar)')
312 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
312 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
313 >>> _parseexpr(b'foo(')
313 >>> _parseexpr(b'foo(')
314 Traceback (most recent call last):
314 Traceback (most recent call last):
315 ...
315 ...
316 ParseError: ('not a prefix: end', 4)
316 ParseError: ('not a prefix: end', 4)
317 >>> _parseexpr(b'"foo" "bar"')
317 >>> _parseexpr(b'"foo" "bar"')
318 Traceback (most recent call last):
318 Traceback (most recent call last):
319 ...
319 ...
320 ParseError: ('invalid token', 7)
320 ParseError: ('invalid token', 7)
321 """
321 """
322 p = parser.parser(elements)
322 p = parser.parser(elements)
323 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
323 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
324 if pos != len(expr):
324 if pos != len(expr):
325 raise error.ParseError(_('invalid token'), pos)
325 raise error.ParseError(_('invalid token'), pos)
326 return _unnesttemplatelist(tree)
326 return _unnesttemplatelist(tree)
327
327
328 def prettyformat(tree):
328 def prettyformat(tree):
329 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
329 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
330
330
331 def compileexp(exp, context, curmethods):
331 def compileexp(exp, context, curmethods):
332 """Compile parsed template tree to (func, data) pair"""
332 """Compile parsed template tree to (func, data) pair"""
333 if not exp:
333 if not exp:
334 raise error.ParseError(_("missing argument"))
334 raise error.ParseError(_("missing argument"))
335 t = exp[0]
335 t = exp[0]
336 if t in curmethods:
336 if t in curmethods:
337 return curmethods[t](exp, context)
337 return curmethods[t](exp, context)
338 raise error.ParseError(_("unknown method '%s'") % t)
338 raise error.ParseError(_("unknown method '%s'") % t)
339
339
340 # template evaluation
340 # template evaluation
341
341
342 def getsymbol(exp):
342 def getsymbol(exp):
343 if exp[0] == 'symbol':
343 if exp[0] == 'symbol':
344 return exp[1]
344 return exp[1]
345 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
345 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
346
346
347 def getlist(x):
347 def getlist(x):
348 if not x:
348 if not x:
349 return []
349 return []
350 if x[0] == 'list':
350 if x[0] == 'list':
351 return getlist(x[1]) + [x[2]]
351 return getlist(x[1]) + [x[2]]
352 return [x]
352 return [x]
353
353
354 def gettemplate(exp, context):
354 def gettemplate(exp, context):
355 """Compile given template tree or load named template from map file;
355 """Compile given template tree or load named template from map file;
356 returns (func, data) pair"""
356 returns (func, data) pair"""
357 if exp[0] in ('template', 'string'):
357 if exp[0] in ('template', 'string'):
358 return compileexp(exp, context, methods)
358 return compileexp(exp, context, methods)
359 if exp[0] == 'symbol':
359 if exp[0] == 'symbol':
360 # unlike runsymbol(), here 'symbol' is always taken as template name
360 # unlike runsymbol(), here 'symbol' is always taken as template name
361 # even if it exists in mapping. this allows us to override mapping
361 # even if it exists in mapping. this allows us to override mapping
362 # by web templates, e.g. 'changelogtag' is redefined in map file.
362 # by web templates, e.g. 'changelogtag' is redefined in map file.
363 return context._load(exp[1])
363 return context._load(exp[1])
364 raise error.ParseError(_("expected template specifier"))
364 raise error.ParseError(_("expected template specifier"))
365
365
366 def _runrecursivesymbol(context, mapping, key):
366 def _runrecursivesymbol(context, mapping, key):
367 raise error.Abort(_("recursive reference '%s' in template") % key)
367 raise error.Abort(_("recursive reference '%s' in template") % key)
368
368
369 def buildtemplate(exp, context):
369 def buildtemplate(exp, context):
370 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
370 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
371 return (templateutil.runtemplate, ctmpl)
371 return (templateutil.runtemplate, ctmpl)
372
372
373 def buildfilter(exp, context):
373 def buildfilter(exp, context):
374 n = getsymbol(exp[2])
374 n = getsymbol(exp[2])
375 if n in context._filters:
375 if n in context._filters:
376 filt = context._filters[n]
376 filt = context._filters[n]
377 arg = compileexp(exp[1], context, methods)
377 arg = compileexp(exp[1], context, methods)
378 return (templateutil.runfilter, (arg, filt))
378 return (templateutil.runfilter, (arg, filt))
379 if n in context._funcs:
379 if n in context._funcs:
380 f = context._funcs[n]
380 f = context._funcs[n]
381 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
381 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
382 return (f, args)
382 return (f, args)
383 raise error.ParseError(_("unknown function '%s'") % n)
383 raise error.ParseError(_("unknown function '%s'") % n)
384
384
385 def buildmap(exp, context):
385 def buildmap(exp, context):
386 darg = compileexp(exp[1], context, methods)
386 darg = compileexp(exp[1], context, methods)
387 targ = gettemplate(exp[2], context)
387 targ = gettemplate(exp[2], context)
388 return (templateutil.runmap, (darg, targ))
388 return (templateutil.runmap, (darg, targ))
389
389
390 def buildmember(exp, context):
390 def buildmember(exp, context):
391 darg = compileexp(exp[1], context, methods)
391 darg = compileexp(exp[1], context, methods)
392 memb = getsymbol(exp[2])
392 memb = getsymbol(exp[2])
393 return (templateutil.runmember, (darg, memb))
393 return (templateutil.runmember, (darg, memb))
394
394
395 def buildnegate(exp, context):
395 def buildnegate(exp, context):
396 arg = compileexp(exp[1], context, exprmethods)
396 arg = compileexp(exp[1], context, exprmethods)
397 return (templateutil.runnegate, arg)
397 return (templateutil.runnegate, arg)
398
398
399 def buildarithmetic(exp, context, func):
399 def buildarithmetic(exp, context, func):
400 left = compileexp(exp[1], context, exprmethods)
400 left = compileexp(exp[1], context, exprmethods)
401 right = compileexp(exp[2], context, exprmethods)
401 right = compileexp(exp[2], context, exprmethods)
402 return (templateutil.runarithmetic, (func, left, right))
402 return (templateutil.runarithmetic, (func, left, right))
403
403
404 def buildfunc(exp, context):
404 def buildfunc(exp, context):
405 n = getsymbol(exp[1])
405 n = getsymbol(exp[1])
406 if n in context._funcs:
406 if n in context._funcs:
407 f = context._funcs[n]
407 f = context._funcs[n]
408 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
408 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
409 return (f, args)
409 return (f, args)
410 if n in context._filters:
410 if n in context._filters:
411 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
411 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
412 if len(args) != 1:
412 if len(args) != 1:
413 raise error.ParseError(_("filter %s expects one argument") % n)
413 raise error.ParseError(_("filter %s expects one argument") % n)
414 f = context._filters[n]
414 f = context._filters[n]
415 return (templateutil.runfilter, (args[0], f))
415 return (templateutil.runfilter, (args[0], f))
416 raise error.ParseError(_("unknown function '%s'") % n)
416 raise error.ParseError(_("unknown function '%s'") % n)
417
417
418 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
418 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
419 """Compile parsed tree of function arguments into list or dict of
419 """Compile parsed tree of function arguments into list or dict of
420 (func, data) pairs
420 (func, data) pairs
421
421
422 >>> context = engine(lambda t: (runsymbol, t))
422 >>> context = engine(lambda t: (runsymbol, t))
423 >>> def fargs(expr, argspec):
423 >>> def fargs(expr, argspec):
424 ... x = _parseexpr(expr)
424 ... x = _parseexpr(expr)
425 ... n = getsymbol(x[1])
425 ... n = getsymbol(x[1])
426 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
426 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
427 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
427 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
428 ['l', 'k']
428 ['l', 'k']
429 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
429 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
430 >>> list(args.keys()), list(args[b'opts'].keys())
430 >>> list(args.keys()), list(args[b'opts'].keys())
431 (['opts'], ['opts', 'k'])
431 (['opts'], ['opts', 'k'])
432 """
432 """
433 def compiledict(xs):
433 def compiledict(xs):
434 return util.sortdict((k, compileexp(x, context, curmethods))
434 return util.sortdict((k, compileexp(x, context, curmethods))
435 for k, x in xs.iteritems())
435 for k, x in xs.iteritems())
436 def compilelist(xs):
436 def compilelist(xs):
437 return [compileexp(x, context, curmethods) for x in xs]
437 return [compileexp(x, context, curmethods) for x in xs]
438
438
439 if not argspec:
439 if not argspec:
440 # filter or function with no argspec: return list of positional args
440 # filter or function with no argspec: return list of positional args
441 return compilelist(getlist(exp))
441 return compilelist(getlist(exp))
442
442
443 # function with argspec: return dict of named args
443 # function with argspec: return dict of named args
444 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
444 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
445 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
445 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
446 keyvaluenode='keyvalue', keynode='symbol')
446 keyvaluenode='keyvalue', keynode='symbol')
447 compargs = util.sortdict()
447 compargs = util.sortdict()
448 if varkey:
448 if varkey:
449 compargs[varkey] = compilelist(treeargs.pop(varkey))
449 compargs[varkey] = compilelist(treeargs.pop(varkey))
450 if optkey:
450 if optkey:
451 compargs[optkey] = compiledict(treeargs.pop(optkey))
451 compargs[optkey] = compiledict(treeargs.pop(optkey))
452 compargs.update(compiledict(treeargs))
452 compargs.update(compiledict(treeargs))
453 return compargs
453 return compargs
454
454
455 def buildkeyvaluepair(exp, content):
455 def buildkeyvaluepair(exp, content):
456 raise error.ParseError(_("can't use a key-value pair in this context"))
456 raise error.ParseError(_("can't use a key-value pair in this context"))
457
457
458 # dict of template built-in functions
458 # dict of template built-in functions
459 funcs = {}
459 funcs = {}
460
460
461 templatefunc = registrar.templatefunc(funcs)
461 templatefunc = registrar.templatefunc(funcs)
462
462
463 @templatefunc('date(date[, fmt])')
463 @templatefunc('date(date[, fmt])')
464 def date(context, mapping, args):
464 def date(context, mapping, args):
465 """Format a date. See :hg:`help dates` for formatting
465 """Format a date. See :hg:`help dates` for formatting
466 strings. The default is a Unix date format, including the timezone:
466 strings. The default is a Unix date format, including the timezone:
467 "Mon Sep 04 15:13:13 2006 0700"."""
467 "Mon Sep 04 15:13:13 2006 0700"."""
468 if not (1 <= len(args) <= 2):
468 if not (1 <= len(args) <= 2):
469 # i18n: "date" is a keyword
469 # i18n: "date" is a keyword
470 raise error.ParseError(_("date expects one or two arguments"))
470 raise error.ParseError(_("date expects one or two arguments"))
471
471
472 date = evalfuncarg(context, mapping, args[0])
472 date = evalfuncarg(context, mapping, args[0])
473 fmt = None
473 fmt = None
474 if len(args) == 2:
474 if len(args) == 2:
475 fmt = evalstring(context, mapping, args[1])
475 fmt = evalstring(context, mapping, args[1])
476 try:
476 try:
477 if fmt is None:
477 if fmt is None:
478 return dateutil.datestr(date)
478 return dateutil.datestr(date)
479 else:
479 else:
480 return dateutil.datestr(date, fmt)
480 return dateutil.datestr(date, fmt)
481 except (TypeError, ValueError):
481 except (TypeError, ValueError):
482 # i18n: "date" is a keyword
482 # i18n: "date" is a keyword
483 raise error.ParseError(_("date expects a date information"))
483 raise error.ParseError(_("date expects a date information"))
484
484
485 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
485 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
486 def dict_(context, mapping, args):
486 def dict_(context, mapping, args):
487 """Construct a dict from key-value pairs. A key may be omitted if
487 """Construct a dict from key-value pairs. A key may be omitted if
488 a value expression can provide an unambiguous name."""
488 a value expression can provide an unambiguous name."""
489 data = util.sortdict()
489 data = util.sortdict()
490
490
491 for v in args['args']:
491 for v in args['args']:
492 k = templateutil.findsymbolicname(v)
492 k = templateutil.findsymbolicname(v)
493 if not k:
493 if not k:
494 raise error.ParseError(_('dict key cannot be inferred'))
494 raise error.ParseError(_('dict key cannot be inferred'))
495 if k in data or k in args['kwargs']:
495 if k in data or k in args['kwargs']:
496 raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
496 raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
497 data[k] = evalfuncarg(context, mapping, v)
497 data[k] = evalfuncarg(context, mapping, v)
498
498
499 data.update((k, evalfuncarg(context, mapping, v))
499 data.update((k, evalfuncarg(context, mapping, v))
500 for k, v in args['kwargs'].iteritems())
500 for k, v in args['kwargs'].iteritems())
501 return templatekw.hybriddict(data)
501 return templateutil.hybriddict(data)
502
502
503 @templatefunc('diff([includepattern [, excludepattern]])')
503 @templatefunc('diff([includepattern [, excludepattern]])')
504 def diff(context, mapping, args):
504 def diff(context, mapping, args):
505 """Show a diff, optionally
505 """Show a diff, optionally
506 specifying files to include or exclude."""
506 specifying files to include or exclude."""
507 if len(args) > 2:
507 if len(args) > 2:
508 # i18n: "diff" is a keyword
508 # i18n: "diff" is a keyword
509 raise error.ParseError(_("diff expects zero, one, or two arguments"))
509 raise error.ParseError(_("diff expects zero, one, or two arguments"))
510
510
511 def getpatterns(i):
511 def getpatterns(i):
512 if i < len(args):
512 if i < len(args):
513 s = evalstring(context, mapping, args[i]).strip()
513 s = evalstring(context, mapping, args[i]).strip()
514 if s:
514 if s:
515 return [s]
515 return [s]
516 return []
516 return []
517
517
518 ctx = context.resource(mapping, 'ctx')
518 ctx = context.resource(mapping, 'ctx')
519 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
519 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
520
520
521 return ''.join(chunks)
521 return ''.join(chunks)
522
522
523 @templatefunc('extdata(source)', argspec='source')
523 @templatefunc('extdata(source)', argspec='source')
524 def extdata(context, mapping, args):
524 def extdata(context, mapping, args):
525 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
525 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
526 if 'source' not in args:
526 if 'source' not in args:
527 # i18n: "extdata" is a keyword
527 # i18n: "extdata" is a keyword
528 raise error.ParseError(_('extdata expects one argument'))
528 raise error.ParseError(_('extdata expects one argument'))
529
529
530 source = evalstring(context, mapping, args['source'])
530 source = evalstring(context, mapping, args['source'])
531 cache = context.resource(mapping, 'cache').setdefault('extdata', {})
531 cache = context.resource(mapping, 'cache').setdefault('extdata', {})
532 ctx = context.resource(mapping, 'ctx')
532 ctx = context.resource(mapping, 'ctx')
533 if source in cache:
533 if source in cache:
534 data = cache[source]
534 data = cache[source]
535 else:
535 else:
536 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
536 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
537 return data.get(ctx.rev(), '')
537 return data.get(ctx.rev(), '')
538
538
539 @templatefunc('files(pattern)')
539 @templatefunc('files(pattern)')
540 def files(context, mapping, args):
540 def files(context, mapping, args):
541 """All files of the current changeset matching the pattern. See
541 """All files of the current changeset matching the pattern. See
542 :hg:`help patterns`."""
542 :hg:`help patterns`."""
543 if not len(args) == 1:
543 if not len(args) == 1:
544 # i18n: "files" is a keyword
544 # i18n: "files" is a keyword
545 raise error.ParseError(_("files expects one argument"))
545 raise error.ParseError(_("files expects one argument"))
546
546
547 raw = evalstring(context, mapping, args[0])
547 raw = evalstring(context, mapping, args[0])
548 ctx = context.resource(mapping, 'ctx')
548 ctx = context.resource(mapping, 'ctx')
549 m = ctx.match([raw])
549 m = ctx.match([raw])
550 files = list(ctx.matches(m))
550 files = list(ctx.matches(m))
551 return templatekw.compatlist(context, mapping, "file", files)
551 return templateutil.compatlist(context, mapping, "file", files)
552
552
553 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
553 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
554 def fill(context, mapping, args):
554 def fill(context, mapping, args):
555 """Fill many
555 """Fill many
556 paragraphs with optional indentation. See the "fill" filter."""
556 paragraphs with optional indentation. See the "fill" filter."""
557 if not (1 <= len(args) <= 4):
557 if not (1 <= len(args) <= 4):
558 # i18n: "fill" is a keyword
558 # i18n: "fill" is a keyword
559 raise error.ParseError(_("fill expects one to four arguments"))
559 raise error.ParseError(_("fill expects one to four arguments"))
560
560
561 text = evalstring(context, mapping, args[0])
561 text = evalstring(context, mapping, args[0])
562 width = 76
562 width = 76
563 initindent = ''
563 initindent = ''
564 hangindent = ''
564 hangindent = ''
565 if 2 <= len(args) <= 4:
565 if 2 <= len(args) <= 4:
566 width = evalinteger(context, mapping, args[1],
566 width = evalinteger(context, mapping, args[1],
567 # i18n: "fill" is a keyword
567 # i18n: "fill" is a keyword
568 _("fill expects an integer width"))
568 _("fill expects an integer width"))
569 try:
569 try:
570 initindent = evalstring(context, mapping, args[2])
570 initindent = evalstring(context, mapping, args[2])
571 hangindent = evalstring(context, mapping, args[3])
571 hangindent = evalstring(context, mapping, args[3])
572 except IndexError:
572 except IndexError:
573 pass
573 pass
574
574
575 return templatefilters.fill(text, width, initindent, hangindent)
575 return templatefilters.fill(text, width, initindent, hangindent)
576
576
577 @templatefunc('formatnode(node)')
577 @templatefunc('formatnode(node)')
578 def formatnode(context, mapping, args):
578 def formatnode(context, mapping, args):
579 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
579 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
580 if len(args) != 1:
580 if len(args) != 1:
581 # i18n: "formatnode" is a keyword
581 # i18n: "formatnode" is a keyword
582 raise error.ParseError(_("formatnode expects one argument"))
582 raise error.ParseError(_("formatnode expects one argument"))
583
583
584 ui = context.resource(mapping, 'ui')
584 ui = context.resource(mapping, 'ui')
585 node = evalstring(context, mapping, args[0])
585 node = evalstring(context, mapping, args[0])
586 if ui.debugflag:
586 if ui.debugflag:
587 return node
587 return node
588 return templatefilters.short(node)
588 return templatefilters.short(node)
589
589
590 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
590 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
591 argspec='text width fillchar left')
591 argspec='text width fillchar left')
592 def pad(context, mapping, args):
592 def pad(context, mapping, args):
593 """Pad text with a
593 """Pad text with a
594 fill character."""
594 fill character."""
595 if 'text' not in args or 'width' not in args:
595 if 'text' not in args or 'width' not in args:
596 # i18n: "pad" is a keyword
596 # i18n: "pad" is a keyword
597 raise error.ParseError(_("pad() expects two to four arguments"))
597 raise error.ParseError(_("pad() expects two to four arguments"))
598
598
599 width = evalinteger(context, mapping, args['width'],
599 width = evalinteger(context, mapping, args['width'],
600 # i18n: "pad" is a keyword
600 # i18n: "pad" is a keyword
601 _("pad() expects an integer width"))
601 _("pad() expects an integer width"))
602
602
603 text = evalstring(context, mapping, args['text'])
603 text = evalstring(context, mapping, args['text'])
604
604
605 left = False
605 left = False
606 fillchar = ' '
606 fillchar = ' '
607 if 'fillchar' in args:
607 if 'fillchar' in args:
608 fillchar = evalstring(context, mapping, args['fillchar'])
608 fillchar = evalstring(context, mapping, args['fillchar'])
609 if len(color.stripeffects(fillchar)) != 1:
609 if len(color.stripeffects(fillchar)) != 1:
610 # i18n: "pad" is a keyword
610 # i18n: "pad" is a keyword
611 raise error.ParseError(_("pad() expects a single fill character"))
611 raise error.ParseError(_("pad() expects a single fill character"))
612 if 'left' in args:
612 if 'left' in args:
613 left = evalboolean(context, mapping, args['left'])
613 left = evalboolean(context, mapping, args['left'])
614
614
615 fillwidth = width - encoding.colwidth(color.stripeffects(text))
615 fillwidth = width - encoding.colwidth(color.stripeffects(text))
616 if fillwidth <= 0:
616 if fillwidth <= 0:
617 return text
617 return text
618 if left:
618 if left:
619 return fillchar * fillwidth + text
619 return fillchar * fillwidth + text
620 else:
620 else:
621 return text + fillchar * fillwidth
621 return text + fillchar * fillwidth
622
622
623 @templatefunc('indent(text, indentchars[, firstline])')
623 @templatefunc('indent(text, indentchars[, firstline])')
624 def indent(context, mapping, args):
624 def indent(context, mapping, args):
625 """Indents all non-empty lines
625 """Indents all non-empty lines
626 with the characters given in the indentchars string. An optional
626 with the characters given in the indentchars string. An optional
627 third parameter will override the indent for the first line only
627 third parameter will override the indent for the first line only
628 if present."""
628 if present."""
629 if not (2 <= len(args) <= 3):
629 if not (2 <= len(args) <= 3):
630 # i18n: "indent" is a keyword
630 # i18n: "indent" is a keyword
631 raise error.ParseError(_("indent() expects two or three arguments"))
631 raise error.ParseError(_("indent() expects two or three arguments"))
632
632
633 text = evalstring(context, mapping, args[0])
633 text = evalstring(context, mapping, args[0])
634 indent = evalstring(context, mapping, args[1])
634 indent = evalstring(context, mapping, args[1])
635
635
636 if len(args) == 3:
636 if len(args) == 3:
637 firstline = evalstring(context, mapping, args[2])
637 firstline = evalstring(context, mapping, args[2])
638 else:
638 else:
639 firstline = indent
639 firstline = indent
640
640
641 # the indent function doesn't indent the first line, so we do it here
641 # the indent function doesn't indent the first line, so we do it here
642 return templatefilters.indent(firstline + text, indent)
642 return templatefilters.indent(firstline + text, indent)
643
643
644 @templatefunc('get(dict, key)')
644 @templatefunc('get(dict, key)')
645 def get(context, mapping, args):
645 def get(context, mapping, args):
646 """Get an attribute/key from an object. Some keywords
646 """Get an attribute/key from an object. Some keywords
647 are complex types. This function allows you to obtain the value of an
647 are complex types. This function allows you to obtain the value of an
648 attribute on these types."""
648 attribute on these types."""
649 if len(args) != 2:
649 if len(args) != 2:
650 # i18n: "get" is a keyword
650 # i18n: "get" is a keyword
651 raise error.ParseError(_("get() expects two arguments"))
651 raise error.ParseError(_("get() expects two arguments"))
652
652
653 dictarg = evalfuncarg(context, mapping, args[0])
653 dictarg = evalfuncarg(context, mapping, args[0])
654 if not util.safehasattr(dictarg, 'get'):
654 if not util.safehasattr(dictarg, 'get'):
655 # i18n: "get" is a keyword
655 # i18n: "get" is a keyword
656 raise error.ParseError(_("get() expects a dict as first argument"))
656 raise error.ParseError(_("get() expects a dict as first argument"))
657
657
658 key = evalfuncarg(context, mapping, args[1])
658 key = evalfuncarg(context, mapping, args[1])
659 return templateutil.getdictitem(dictarg, key)
659 return templateutil.getdictitem(dictarg, key)
660
660
661 @templatefunc('if(expr, then[, else])')
661 @templatefunc('if(expr, then[, else])')
662 def if_(context, mapping, args):
662 def if_(context, mapping, args):
663 """Conditionally execute based on the result of
663 """Conditionally execute based on the result of
664 an expression."""
664 an expression."""
665 if not (2 <= len(args) <= 3):
665 if not (2 <= len(args) <= 3):
666 # i18n: "if" is a keyword
666 # i18n: "if" is a keyword
667 raise error.ParseError(_("if expects two or three arguments"))
667 raise error.ParseError(_("if expects two or three arguments"))
668
668
669 test = evalboolean(context, mapping, args[0])
669 test = evalboolean(context, mapping, args[0])
670 if test:
670 if test:
671 yield evalrawexp(context, mapping, args[1])
671 yield evalrawexp(context, mapping, args[1])
672 elif len(args) == 3:
672 elif len(args) == 3:
673 yield evalrawexp(context, mapping, args[2])
673 yield evalrawexp(context, mapping, args[2])
674
674
675 @templatefunc('ifcontains(needle, haystack, then[, else])')
675 @templatefunc('ifcontains(needle, haystack, then[, else])')
676 def ifcontains(context, mapping, args):
676 def ifcontains(context, mapping, args):
677 """Conditionally execute based
677 """Conditionally execute based
678 on whether the item "needle" is in "haystack"."""
678 on whether the item "needle" is in "haystack"."""
679 if not (3 <= len(args) <= 4):
679 if not (3 <= len(args) <= 4):
680 # i18n: "ifcontains" is a keyword
680 # i18n: "ifcontains" is a keyword
681 raise error.ParseError(_("ifcontains expects three or four arguments"))
681 raise error.ParseError(_("ifcontains expects three or four arguments"))
682
682
683 haystack = evalfuncarg(context, mapping, args[1])
683 haystack = evalfuncarg(context, mapping, args[1])
684 try:
684 try:
685 needle = evalastype(context, mapping, args[0],
685 needle = evalastype(context, mapping, args[0],
686 getattr(haystack, 'keytype', None) or bytes)
686 getattr(haystack, 'keytype', None) or bytes)
687 found = (needle in haystack)
687 found = (needle in haystack)
688 except error.ParseError:
688 except error.ParseError:
689 found = False
689 found = False
690
690
691 if found:
691 if found:
692 yield evalrawexp(context, mapping, args[2])
692 yield evalrawexp(context, mapping, args[2])
693 elif len(args) == 4:
693 elif len(args) == 4:
694 yield evalrawexp(context, mapping, args[3])
694 yield evalrawexp(context, mapping, args[3])
695
695
696 @templatefunc('ifeq(expr1, expr2, then[, else])')
696 @templatefunc('ifeq(expr1, expr2, then[, else])')
697 def ifeq(context, mapping, args):
697 def ifeq(context, mapping, args):
698 """Conditionally execute based on
698 """Conditionally execute based on
699 whether 2 items are equivalent."""
699 whether 2 items are equivalent."""
700 if not (3 <= len(args) <= 4):
700 if not (3 <= len(args) <= 4):
701 # i18n: "ifeq" is a keyword
701 # i18n: "ifeq" is a keyword
702 raise error.ParseError(_("ifeq expects three or four arguments"))
702 raise error.ParseError(_("ifeq expects three or four arguments"))
703
703
704 test = evalstring(context, mapping, args[0])
704 test = evalstring(context, mapping, args[0])
705 match = evalstring(context, mapping, args[1])
705 match = evalstring(context, mapping, args[1])
706 if test == match:
706 if test == match:
707 yield evalrawexp(context, mapping, args[2])
707 yield evalrawexp(context, mapping, args[2])
708 elif len(args) == 4:
708 elif len(args) == 4:
709 yield evalrawexp(context, mapping, args[3])
709 yield evalrawexp(context, mapping, args[3])
710
710
711 @templatefunc('join(list, sep)')
711 @templatefunc('join(list, sep)')
712 def join(context, mapping, args):
712 def join(context, mapping, args):
713 """Join items in a list with a delimiter."""
713 """Join items in a list with a delimiter."""
714 if not (1 <= len(args) <= 2):
714 if not (1 <= len(args) <= 2):
715 # i18n: "join" is a keyword
715 # i18n: "join" is a keyword
716 raise error.ParseError(_("join expects one or two arguments"))
716 raise error.ParseError(_("join expects one or two arguments"))
717
717
718 # TODO: perhaps this should be evalfuncarg(), but it can't because hgweb
718 # TODO: perhaps this should be evalfuncarg(), but it can't because hgweb
719 # abuses generator as a keyword that returns a list of dicts.
719 # abuses generator as a keyword that returns a list of dicts.
720 joinset = evalrawexp(context, mapping, args[0])
720 joinset = evalrawexp(context, mapping, args[0])
721 joinset = templatekw.unwrapvalue(joinset)
721 joinset = templateutil.unwrapvalue(joinset)
722 joinfmt = getattr(joinset, 'joinfmt', pycompat.identity)
722 joinfmt = getattr(joinset, 'joinfmt', pycompat.identity)
723 joiner = " "
723 joiner = " "
724 if len(args) > 1:
724 if len(args) > 1:
725 joiner = evalstring(context, mapping, args[1])
725 joiner = evalstring(context, mapping, args[1])
726
726
727 first = True
727 first = True
728 for x in pycompat.maybebytestr(joinset):
728 for x in pycompat.maybebytestr(joinset):
729 if first:
729 if first:
730 first = False
730 first = False
731 else:
731 else:
732 yield joiner
732 yield joiner
733 yield joinfmt(x)
733 yield joinfmt(x)
734
734
735 @templatefunc('label(label, expr)')
735 @templatefunc('label(label, expr)')
736 def label(context, mapping, args):
736 def label(context, mapping, args):
737 """Apply a label to generated content. Content with
737 """Apply a label to generated content. Content with
738 a label applied can result in additional post-processing, such as
738 a label applied can result in additional post-processing, such as
739 automatic colorization."""
739 automatic colorization."""
740 if len(args) != 2:
740 if len(args) != 2:
741 # i18n: "label" is a keyword
741 # i18n: "label" is a keyword
742 raise error.ParseError(_("label expects two arguments"))
742 raise error.ParseError(_("label expects two arguments"))
743
743
744 ui = context.resource(mapping, 'ui')
744 ui = context.resource(mapping, 'ui')
745 thing = evalstring(context, mapping, args[1])
745 thing = evalstring(context, mapping, args[1])
746 # preserve unknown symbol as literal so effects like 'red', 'bold',
746 # preserve unknown symbol as literal so effects like 'red', 'bold',
747 # etc. don't need to be quoted
747 # etc. don't need to be quoted
748 label = evalstringliteral(context, mapping, args[0])
748 label = evalstringliteral(context, mapping, args[0])
749
749
750 return ui.label(thing, label)
750 return ui.label(thing, label)
751
751
752 @templatefunc('latesttag([pattern])')
752 @templatefunc('latesttag([pattern])')
753 def latesttag(context, mapping, args):
753 def latesttag(context, mapping, args):
754 """The global tags matching the given pattern on the
754 """The global tags matching the given pattern on the
755 most recent globally tagged ancestor of this changeset.
755 most recent globally tagged ancestor of this changeset.
756 If no such tags exist, the "{tag}" template resolves to
756 If no such tags exist, the "{tag}" template resolves to
757 the string "null"."""
757 the string "null"."""
758 if len(args) > 1:
758 if len(args) > 1:
759 # i18n: "latesttag" is a keyword
759 # i18n: "latesttag" is a keyword
760 raise error.ParseError(_("latesttag expects at most one argument"))
760 raise error.ParseError(_("latesttag expects at most one argument"))
761
761
762 pattern = None
762 pattern = None
763 if len(args) == 1:
763 if len(args) == 1:
764 pattern = evalstring(context, mapping, args[0])
764 pattern = evalstring(context, mapping, args[0])
765 return templatekw.showlatesttags(context, mapping, pattern)
765 return templatekw.showlatesttags(context, mapping, pattern)
766
766
767 @templatefunc('localdate(date[, tz])')
767 @templatefunc('localdate(date[, tz])')
768 def localdate(context, mapping, args):
768 def localdate(context, mapping, args):
769 """Converts a date to the specified timezone.
769 """Converts a date to the specified timezone.
770 The default is local date."""
770 The default is local date."""
771 if not (1 <= len(args) <= 2):
771 if not (1 <= len(args) <= 2):
772 # i18n: "localdate" is a keyword
772 # i18n: "localdate" is a keyword
773 raise error.ParseError(_("localdate expects one or two arguments"))
773 raise error.ParseError(_("localdate expects one or two arguments"))
774
774
775 date = evalfuncarg(context, mapping, args[0])
775 date = evalfuncarg(context, mapping, args[0])
776 try:
776 try:
777 date = dateutil.parsedate(date)
777 date = dateutil.parsedate(date)
778 except AttributeError: # not str nor date tuple
778 except AttributeError: # not str nor date tuple
779 # i18n: "localdate" is a keyword
779 # i18n: "localdate" is a keyword
780 raise error.ParseError(_("localdate expects a date information"))
780 raise error.ParseError(_("localdate expects a date information"))
781 if len(args) >= 2:
781 if len(args) >= 2:
782 tzoffset = None
782 tzoffset = None
783 tz = evalfuncarg(context, mapping, args[1])
783 tz = evalfuncarg(context, mapping, args[1])
784 if isinstance(tz, bytes):
784 if isinstance(tz, bytes):
785 tzoffset, remainder = dateutil.parsetimezone(tz)
785 tzoffset, remainder = dateutil.parsetimezone(tz)
786 if remainder:
786 if remainder:
787 tzoffset = None
787 tzoffset = None
788 if tzoffset is None:
788 if tzoffset is None:
789 try:
789 try:
790 tzoffset = int(tz)
790 tzoffset = int(tz)
791 except (TypeError, ValueError):
791 except (TypeError, ValueError):
792 # i18n: "localdate" is a keyword
792 # i18n: "localdate" is a keyword
793 raise error.ParseError(_("localdate expects a timezone"))
793 raise error.ParseError(_("localdate expects a timezone"))
794 else:
794 else:
795 tzoffset = dateutil.makedate()[1]
795 tzoffset = dateutil.makedate()[1]
796 return (date[0], tzoffset)
796 return (date[0], tzoffset)
797
797
798 @templatefunc('max(iterable)')
798 @templatefunc('max(iterable)')
799 def max_(context, mapping, args, **kwargs):
799 def max_(context, mapping, args, **kwargs):
800 """Return the max of an iterable"""
800 """Return the max of an iterable"""
801 if len(args) != 1:
801 if len(args) != 1:
802 # i18n: "max" is a keyword
802 # i18n: "max" is a keyword
803 raise error.ParseError(_("max expects one argument"))
803 raise error.ParseError(_("max expects one argument"))
804
804
805 iterable = evalfuncarg(context, mapping, args[0])
805 iterable = evalfuncarg(context, mapping, args[0])
806 try:
806 try:
807 x = max(pycompat.maybebytestr(iterable))
807 x = max(pycompat.maybebytestr(iterable))
808 except (TypeError, ValueError):
808 except (TypeError, ValueError):
809 # i18n: "max" is a keyword
809 # i18n: "max" is a keyword
810 raise error.ParseError(_("max first argument should be an iterable"))
810 raise error.ParseError(_("max first argument should be an iterable"))
811 return templatekw.wraphybridvalue(iterable, x, x)
811 return templateutil.wraphybridvalue(iterable, x, x)
812
812
813 @templatefunc('min(iterable)')
813 @templatefunc('min(iterable)')
814 def min_(context, mapping, args, **kwargs):
814 def min_(context, mapping, args, **kwargs):
815 """Return the min of an iterable"""
815 """Return the min of an iterable"""
816 if len(args) != 1:
816 if len(args) != 1:
817 # i18n: "min" is a keyword
817 # i18n: "min" is a keyword
818 raise error.ParseError(_("min expects one argument"))
818 raise error.ParseError(_("min expects one argument"))
819
819
820 iterable = evalfuncarg(context, mapping, args[0])
820 iterable = evalfuncarg(context, mapping, args[0])
821 try:
821 try:
822 x = min(pycompat.maybebytestr(iterable))
822 x = min(pycompat.maybebytestr(iterable))
823 except (TypeError, ValueError):
823 except (TypeError, ValueError):
824 # i18n: "min" is a keyword
824 # i18n: "min" is a keyword
825 raise error.ParseError(_("min first argument should be an iterable"))
825 raise error.ParseError(_("min first argument should be an iterable"))
826 return templatekw.wraphybridvalue(iterable, x, x)
826 return templateutil.wraphybridvalue(iterable, x, x)
827
827
828 @templatefunc('mod(a, b)')
828 @templatefunc('mod(a, b)')
829 def mod(context, mapping, args):
829 def mod(context, mapping, args):
830 """Calculate a mod b such that a / b + a mod b == a"""
830 """Calculate a mod b such that a / b + a mod b == a"""
831 if not len(args) == 2:
831 if not len(args) == 2:
832 # i18n: "mod" is a keyword
832 # i18n: "mod" is a keyword
833 raise error.ParseError(_("mod expects two arguments"))
833 raise error.ParseError(_("mod expects two arguments"))
834
834
835 func = lambda a, b: a % b
835 func = lambda a, b: a % b
836 return templateutil.runarithmetic(context, mapping,
836 return templateutil.runarithmetic(context, mapping,
837 (func, args[0], args[1]))
837 (func, args[0], args[1]))
838
838
839 @templatefunc('obsfateoperations(markers)')
839 @templatefunc('obsfateoperations(markers)')
840 def obsfateoperations(context, mapping, args):
840 def obsfateoperations(context, mapping, args):
841 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
841 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
842 if len(args) != 1:
842 if len(args) != 1:
843 # i18n: "obsfateoperations" is a keyword
843 # i18n: "obsfateoperations" is a keyword
844 raise error.ParseError(_("obsfateoperations expects one argument"))
844 raise error.ParseError(_("obsfateoperations expects one argument"))
845
845
846 markers = evalfuncarg(context, mapping, args[0])
846 markers = evalfuncarg(context, mapping, args[0])
847
847
848 try:
848 try:
849 data = obsutil.markersoperations(markers)
849 data = obsutil.markersoperations(markers)
850 return templatekw.hybridlist(data, name='operation')
850 return templateutil.hybridlist(data, name='operation')
851 except (TypeError, KeyError):
851 except (TypeError, KeyError):
852 # i18n: "obsfateoperations" is a keyword
852 # i18n: "obsfateoperations" is a keyword
853 errmsg = _("obsfateoperations first argument should be an iterable")
853 errmsg = _("obsfateoperations first argument should be an iterable")
854 raise error.ParseError(errmsg)
854 raise error.ParseError(errmsg)
855
855
856 @templatefunc('obsfatedate(markers)')
856 @templatefunc('obsfatedate(markers)')
857 def obsfatedate(context, mapping, args):
857 def obsfatedate(context, mapping, args):
858 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
858 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
859 if len(args) != 1:
859 if len(args) != 1:
860 # i18n: "obsfatedate" is a keyword
860 # i18n: "obsfatedate" is a keyword
861 raise error.ParseError(_("obsfatedate expects one argument"))
861 raise error.ParseError(_("obsfatedate expects one argument"))
862
862
863 markers = evalfuncarg(context, mapping, args[0])
863 markers = evalfuncarg(context, mapping, args[0])
864
864
865 try:
865 try:
866 data = obsutil.markersdates(markers)
866 data = obsutil.markersdates(markers)
867 return templatekw.hybridlist(data, name='date', fmt='%d %d')
867 return templateutil.hybridlist(data, name='date', fmt='%d %d')
868 except (TypeError, KeyError):
868 except (TypeError, KeyError):
869 # i18n: "obsfatedate" is a keyword
869 # i18n: "obsfatedate" is a keyword
870 errmsg = _("obsfatedate first argument should be an iterable")
870 errmsg = _("obsfatedate first argument should be an iterable")
871 raise error.ParseError(errmsg)
871 raise error.ParseError(errmsg)
872
872
873 @templatefunc('obsfateusers(markers)')
873 @templatefunc('obsfateusers(markers)')
874 def obsfateusers(context, mapping, args):
874 def obsfateusers(context, mapping, args):
875 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
875 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
876 if len(args) != 1:
876 if len(args) != 1:
877 # i18n: "obsfateusers" is a keyword
877 # i18n: "obsfateusers" is a keyword
878 raise error.ParseError(_("obsfateusers expects one argument"))
878 raise error.ParseError(_("obsfateusers expects one argument"))
879
879
880 markers = evalfuncarg(context, mapping, args[0])
880 markers = evalfuncarg(context, mapping, args[0])
881
881
882 try:
882 try:
883 data = obsutil.markersusers(markers)
883 data = obsutil.markersusers(markers)
884 return templatekw.hybridlist(data, name='user')
884 return templateutil.hybridlist(data, name='user')
885 except (TypeError, KeyError, ValueError):
885 except (TypeError, KeyError, ValueError):
886 # i18n: "obsfateusers" is a keyword
886 # i18n: "obsfateusers" is a keyword
887 msg = _("obsfateusers first argument should be an iterable of "
887 msg = _("obsfateusers first argument should be an iterable of "
888 "obsmakers")
888 "obsmakers")
889 raise error.ParseError(msg)
889 raise error.ParseError(msg)
890
890
891 @templatefunc('obsfateverb(successors, markers)')
891 @templatefunc('obsfateverb(successors, markers)')
892 def obsfateverb(context, mapping, args):
892 def obsfateverb(context, mapping, args):
893 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
893 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
894 if len(args) != 2:
894 if len(args) != 2:
895 # i18n: "obsfateverb" is a keyword
895 # i18n: "obsfateverb" is a keyword
896 raise error.ParseError(_("obsfateverb expects two arguments"))
896 raise error.ParseError(_("obsfateverb expects two arguments"))
897
897
898 successors = evalfuncarg(context, mapping, args[0])
898 successors = evalfuncarg(context, mapping, args[0])
899 markers = evalfuncarg(context, mapping, args[1])
899 markers = evalfuncarg(context, mapping, args[1])
900
900
901 try:
901 try:
902 return obsutil.obsfateverb(successors, markers)
902 return obsutil.obsfateverb(successors, markers)
903 except TypeError:
903 except TypeError:
904 # i18n: "obsfateverb" is a keyword
904 # i18n: "obsfateverb" is a keyword
905 errmsg = _("obsfateverb first argument should be countable")
905 errmsg = _("obsfateverb first argument should be countable")
906 raise error.ParseError(errmsg)
906 raise error.ParseError(errmsg)
907
907
908 @templatefunc('relpath(path)')
908 @templatefunc('relpath(path)')
909 def relpath(context, mapping, args):
909 def relpath(context, mapping, args):
910 """Convert a repository-absolute path into a filesystem path relative to
910 """Convert a repository-absolute path into a filesystem path relative to
911 the current working directory."""
911 the current working directory."""
912 if len(args) != 1:
912 if len(args) != 1:
913 # i18n: "relpath" is a keyword
913 # i18n: "relpath" is a keyword
914 raise error.ParseError(_("relpath expects one argument"))
914 raise error.ParseError(_("relpath expects one argument"))
915
915
916 repo = context.resource(mapping, 'ctx').repo()
916 repo = context.resource(mapping, 'ctx').repo()
917 path = evalstring(context, mapping, args[0])
917 path = evalstring(context, mapping, args[0])
918 return repo.pathto(path)
918 return repo.pathto(path)
919
919
920 @templatefunc('revset(query[, formatargs...])')
920 @templatefunc('revset(query[, formatargs...])')
921 def revset(context, mapping, args):
921 def revset(context, mapping, args):
922 """Execute a revision set query. See
922 """Execute a revision set query. See
923 :hg:`help revset`."""
923 :hg:`help revset`."""
924 if not len(args) > 0:
924 if not len(args) > 0:
925 # i18n: "revset" is a keyword
925 # i18n: "revset" is a keyword
926 raise error.ParseError(_("revset expects one or more arguments"))
926 raise error.ParseError(_("revset expects one or more arguments"))
927
927
928 raw = evalstring(context, mapping, args[0])
928 raw = evalstring(context, mapping, args[0])
929 ctx = context.resource(mapping, 'ctx')
929 ctx = context.resource(mapping, 'ctx')
930 repo = ctx.repo()
930 repo = ctx.repo()
931
931
932 def query(expr):
932 def query(expr):
933 m = revsetmod.match(repo.ui, expr, repo=repo)
933 m = revsetmod.match(repo.ui, expr, repo=repo)
934 return m(repo)
934 return m(repo)
935
935
936 if len(args) > 1:
936 if len(args) > 1:
937 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
937 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
938 revs = query(revsetlang.formatspec(raw, *formatargs))
938 revs = query(revsetlang.formatspec(raw, *formatargs))
939 revs = list(revs)
939 revs = list(revs)
940 else:
940 else:
941 cache = context.resource(mapping, 'cache')
941 cache = context.resource(mapping, 'cache')
942 revsetcache = cache.setdefault("revsetcache", {})
942 revsetcache = cache.setdefault("revsetcache", {})
943 if raw in revsetcache:
943 if raw in revsetcache:
944 revs = revsetcache[raw]
944 revs = revsetcache[raw]
945 else:
945 else:
946 revs = query(raw)
946 revs = query(raw)
947 revs = list(revs)
947 revs = list(revs)
948 revsetcache[raw] = revs
948 revsetcache[raw] = revs
949 return templatekw.showrevslist(context, mapping, "revision", revs)
949 return templatekw.showrevslist(context, mapping, "revision", revs)
950
950
951 @templatefunc('rstdoc(text, style)')
951 @templatefunc('rstdoc(text, style)')
952 def rstdoc(context, mapping, args):
952 def rstdoc(context, mapping, args):
953 """Format reStructuredText."""
953 """Format reStructuredText."""
954 if len(args) != 2:
954 if len(args) != 2:
955 # i18n: "rstdoc" is a keyword
955 # i18n: "rstdoc" is a keyword
956 raise error.ParseError(_("rstdoc expects two arguments"))
956 raise error.ParseError(_("rstdoc expects two arguments"))
957
957
958 text = evalstring(context, mapping, args[0])
958 text = evalstring(context, mapping, args[0])
959 style = evalstring(context, mapping, args[1])
959 style = evalstring(context, mapping, args[1])
960
960
961 return minirst.format(text, style=style, keep=['verbose'])
961 return minirst.format(text, style=style, keep=['verbose'])
962
962
963 @templatefunc('separate(sep, args)', argspec='sep *args')
963 @templatefunc('separate(sep, args)', argspec='sep *args')
964 def separate(context, mapping, args):
964 def separate(context, mapping, args):
965 """Add a separator between non-empty arguments."""
965 """Add a separator between non-empty arguments."""
966 if 'sep' not in args:
966 if 'sep' not in args:
967 # i18n: "separate" is a keyword
967 # i18n: "separate" is a keyword
968 raise error.ParseError(_("separate expects at least one argument"))
968 raise error.ParseError(_("separate expects at least one argument"))
969
969
970 sep = evalstring(context, mapping, args['sep'])
970 sep = evalstring(context, mapping, args['sep'])
971 first = True
971 first = True
972 for arg in args['args']:
972 for arg in args['args']:
973 argstr = evalstring(context, mapping, arg)
973 argstr = evalstring(context, mapping, arg)
974 if not argstr:
974 if not argstr:
975 continue
975 continue
976 if first:
976 if first:
977 first = False
977 first = False
978 else:
978 else:
979 yield sep
979 yield sep
980 yield argstr
980 yield argstr
981
981
982 @templatefunc('shortest(node, minlength=4)')
982 @templatefunc('shortest(node, minlength=4)')
983 def shortest(context, mapping, args):
983 def shortest(context, mapping, args):
984 """Obtain the shortest representation of
984 """Obtain the shortest representation of
985 a node."""
985 a node."""
986 if not (1 <= len(args) <= 2):
986 if not (1 <= len(args) <= 2):
987 # i18n: "shortest" is a keyword
987 # i18n: "shortest" is a keyword
988 raise error.ParseError(_("shortest() expects one or two arguments"))
988 raise error.ParseError(_("shortest() expects one or two arguments"))
989
989
990 node = evalstring(context, mapping, args[0])
990 node = evalstring(context, mapping, args[0])
991
991
992 minlength = 4
992 minlength = 4
993 if len(args) > 1:
993 if len(args) > 1:
994 minlength = evalinteger(context, mapping, args[1],
994 minlength = evalinteger(context, mapping, args[1],
995 # i18n: "shortest" is a keyword
995 # i18n: "shortest" is a keyword
996 _("shortest() expects an integer minlength"))
996 _("shortest() expects an integer minlength"))
997
997
998 # _partialmatch() of filtered changelog could take O(len(repo)) time,
998 # _partialmatch() of filtered changelog could take O(len(repo)) time,
999 # which would be unacceptably slow. so we look for hash collision in
999 # which would be unacceptably slow. so we look for hash collision in
1000 # unfiltered space, which means some hashes may be slightly longer.
1000 # unfiltered space, which means some hashes may be slightly longer.
1001 cl = context.resource(mapping, 'ctx')._repo.unfiltered().changelog
1001 cl = context.resource(mapping, 'ctx')._repo.unfiltered().changelog
1002 return cl.shortest(node, minlength)
1002 return cl.shortest(node, minlength)
1003
1003
1004 @templatefunc('strip(text[, chars])')
1004 @templatefunc('strip(text[, chars])')
1005 def strip(context, mapping, args):
1005 def strip(context, mapping, args):
1006 """Strip characters from a string. By default,
1006 """Strip characters from a string. By default,
1007 strips all leading and trailing whitespace."""
1007 strips all leading and trailing whitespace."""
1008 if not (1 <= len(args) <= 2):
1008 if not (1 <= len(args) <= 2):
1009 # i18n: "strip" is a keyword
1009 # i18n: "strip" is a keyword
1010 raise error.ParseError(_("strip expects one or two arguments"))
1010 raise error.ParseError(_("strip expects one or two arguments"))
1011
1011
1012 text = evalstring(context, mapping, args[0])
1012 text = evalstring(context, mapping, args[0])
1013 if len(args) == 2:
1013 if len(args) == 2:
1014 chars = evalstring(context, mapping, args[1])
1014 chars = evalstring(context, mapping, args[1])
1015 return text.strip(chars)
1015 return text.strip(chars)
1016 return text.strip()
1016 return text.strip()
1017
1017
1018 @templatefunc('sub(pattern, replacement, expression)')
1018 @templatefunc('sub(pattern, replacement, expression)')
1019 def sub(context, mapping, args):
1019 def sub(context, mapping, args):
1020 """Perform text substitution
1020 """Perform text substitution
1021 using regular expressions."""
1021 using regular expressions."""
1022 if len(args) != 3:
1022 if len(args) != 3:
1023 # i18n: "sub" is a keyword
1023 # i18n: "sub" is a keyword
1024 raise error.ParseError(_("sub expects three arguments"))
1024 raise error.ParseError(_("sub expects three arguments"))
1025
1025
1026 pat = evalstring(context, mapping, args[0])
1026 pat = evalstring(context, mapping, args[0])
1027 rpl = evalstring(context, mapping, args[1])
1027 rpl = evalstring(context, mapping, args[1])
1028 src = evalstring(context, mapping, args[2])
1028 src = evalstring(context, mapping, args[2])
1029 try:
1029 try:
1030 patre = re.compile(pat)
1030 patre = re.compile(pat)
1031 except re.error:
1031 except re.error:
1032 # i18n: "sub" is a keyword
1032 # i18n: "sub" is a keyword
1033 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
1033 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
1034 try:
1034 try:
1035 yield patre.sub(rpl, src)
1035 yield patre.sub(rpl, src)
1036 except re.error:
1036 except re.error:
1037 # i18n: "sub" is a keyword
1037 # i18n: "sub" is a keyword
1038 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
1038 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
1039
1039
1040 @templatefunc('startswith(pattern, text)')
1040 @templatefunc('startswith(pattern, text)')
1041 def startswith(context, mapping, args):
1041 def startswith(context, mapping, args):
1042 """Returns the value from the "text" argument
1042 """Returns the value from the "text" argument
1043 if it begins with the content from the "pattern" argument."""
1043 if it begins with the content from the "pattern" argument."""
1044 if len(args) != 2:
1044 if len(args) != 2:
1045 # i18n: "startswith" is a keyword
1045 # i18n: "startswith" is a keyword
1046 raise error.ParseError(_("startswith expects two arguments"))
1046 raise error.ParseError(_("startswith expects two arguments"))
1047
1047
1048 patn = evalstring(context, mapping, args[0])
1048 patn = evalstring(context, mapping, args[0])
1049 text = evalstring(context, mapping, args[1])
1049 text = evalstring(context, mapping, args[1])
1050 if text.startswith(patn):
1050 if text.startswith(patn):
1051 return text
1051 return text
1052 return ''
1052 return ''
1053
1053
1054 @templatefunc('word(number, text[, separator])')
1054 @templatefunc('word(number, text[, separator])')
1055 def word(context, mapping, args):
1055 def word(context, mapping, args):
1056 """Return the nth word from a string."""
1056 """Return the nth word from a string."""
1057 if not (2 <= len(args) <= 3):
1057 if not (2 <= len(args) <= 3):
1058 # i18n: "word" is a keyword
1058 # i18n: "word" is a keyword
1059 raise error.ParseError(_("word expects two or three arguments, got %d")
1059 raise error.ParseError(_("word expects two or three arguments, got %d")
1060 % len(args))
1060 % len(args))
1061
1061
1062 num = evalinteger(context, mapping, args[0],
1062 num = evalinteger(context, mapping, args[0],
1063 # i18n: "word" is a keyword
1063 # i18n: "word" is a keyword
1064 _("word expects an integer index"))
1064 _("word expects an integer index"))
1065 text = evalstring(context, mapping, args[1])
1065 text = evalstring(context, mapping, args[1])
1066 if len(args) == 3:
1066 if len(args) == 3:
1067 splitter = evalstring(context, mapping, args[2])
1067 splitter = evalstring(context, mapping, args[2])
1068 else:
1068 else:
1069 splitter = None
1069 splitter = None
1070
1070
1071 tokens = text.split(splitter)
1071 tokens = text.split(splitter)
1072 if num >= len(tokens) or num < -len(tokens):
1072 if num >= len(tokens) or num < -len(tokens):
1073 return ''
1073 return ''
1074 else:
1074 else:
1075 return tokens[num]
1075 return tokens[num]
1076
1076
1077 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
1077 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
1078 exprmethods = {
1078 exprmethods = {
1079 "integer": lambda e, c: (templateutil.runinteger, e[1]),
1079 "integer": lambda e, c: (templateutil.runinteger, e[1]),
1080 "string": lambda e, c: (templateutil.runstring, e[1]),
1080 "string": lambda e, c: (templateutil.runstring, e[1]),
1081 "symbol": lambda e, c: (templateutil.runsymbol, e[1]),
1081 "symbol": lambda e, c: (templateutil.runsymbol, e[1]),
1082 "template": buildtemplate,
1082 "template": buildtemplate,
1083 "group": lambda e, c: compileexp(e[1], c, exprmethods),
1083 "group": lambda e, c: compileexp(e[1], c, exprmethods),
1084 ".": buildmember,
1084 ".": buildmember,
1085 "|": buildfilter,
1085 "|": buildfilter,
1086 "%": buildmap,
1086 "%": buildmap,
1087 "func": buildfunc,
1087 "func": buildfunc,
1088 "keyvalue": buildkeyvaluepair,
1088 "keyvalue": buildkeyvaluepair,
1089 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
1089 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
1090 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
1090 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
1091 "negate": buildnegate,
1091 "negate": buildnegate,
1092 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
1092 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
1093 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
1093 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
1094 }
1094 }
1095
1095
1096 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
1096 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
1097 methods = exprmethods.copy()
1097 methods = exprmethods.copy()
1098 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
1098 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
1099
1099
1100 class _aliasrules(parser.basealiasrules):
1100 class _aliasrules(parser.basealiasrules):
1101 """Parsing and expansion rule set of template aliases"""
1101 """Parsing and expansion rule set of template aliases"""
1102 _section = _('template alias')
1102 _section = _('template alias')
1103 _parse = staticmethod(_parseexpr)
1103 _parse = staticmethod(_parseexpr)
1104
1104
1105 @staticmethod
1105 @staticmethod
1106 def _trygetfunc(tree):
1106 def _trygetfunc(tree):
1107 """Return (name, args) if tree is func(...) or ...|filter; otherwise
1107 """Return (name, args) if tree is func(...) or ...|filter; otherwise
1108 None"""
1108 None"""
1109 if tree[0] == 'func' and tree[1][0] == 'symbol':
1109 if tree[0] == 'func' and tree[1][0] == 'symbol':
1110 return tree[1][1], getlist(tree[2])
1110 return tree[1][1], getlist(tree[2])
1111 if tree[0] == '|' and tree[2][0] == 'symbol':
1111 if tree[0] == '|' and tree[2][0] == 'symbol':
1112 return tree[2][1], [tree[1]]
1112 return tree[2][1], [tree[1]]
1113
1113
1114 def expandaliases(tree, aliases):
1114 def expandaliases(tree, aliases):
1115 """Return new tree of aliases are expanded"""
1115 """Return new tree of aliases are expanded"""
1116 aliasmap = _aliasrules.buildmap(aliases)
1116 aliasmap = _aliasrules.buildmap(aliases)
1117 return _aliasrules.expand(aliasmap, tree)
1117 return _aliasrules.expand(aliasmap, tree)
1118
1118
1119 # template engine
1119 # template engine
1120
1120
1121 def _flatten(thing):
1121 def _flatten(thing):
1122 '''yield a single stream from a possibly nested set of iterators'''
1122 '''yield a single stream from a possibly nested set of iterators'''
1123 thing = templatekw.unwraphybrid(thing)
1123 thing = templateutil.unwraphybrid(thing)
1124 if isinstance(thing, bytes):
1124 if isinstance(thing, bytes):
1125 yield thing
1125 yield thing
1126 elif isinstance(thing, str):
1126 elif isinstance(thing, str):
1127 # We can only hit this on Python 3, and it's here to guard
1127 # We can only hit this on Python 3, and it's here to guard
1128 # against infinite recursion.
1128 # against infinite recursion.
1129 raise error.ProgrammingError('Mercurial IO including templates is done'
1129 raise error.ProgrammingError('Mercurial IO including templates is done'
1130 ' with bytes, not strings, got %r' % thing)
1130 ' with bytes, not strings, got %r' % thing)
1131 elif thing is None:
1131 elif thing is None:
1132 pass
1132 pass
1133 elif not util.safehasattr(thing, '__iter__'):
1133 elif not util.safehasattr(thing, '__iter__'):
1134 yield pycompat.bytestr(thing)
1134 yield pycompat.bytestr(thing)
1135 else:
1135 else:
1136 for i in thing:
1136 for i in thing:
1137 i = templatekw.unwraphybrid(i)
1137 i = templateutil.unwraphybrid(i)
1138 if isinstance(i, bytes):
1138 if isinstance(i, bytes):
1139 yield i
1139 yield i
1140 elif i is None:
1140 elif i is None:
1141 pass
1141 pass
1142 elif not util.safehasattr(i, '__iter__'):
1142 elif not util.safehasattr(i, '__iter__'):
1143 yield pycompat.bytestr(i)
1143 yield pycompat.bytestr(i)
1144 else:
1144 else:
1145 for j in _flatten(i):
1145 for j in _flatten(i):
1146 yield j
1146 yield j
1147
1147
1148 def unquotestring(s):
1148 def unquotestring(s):
1149 '''unwrap quotes if any; otherwise returns unmodified string'''
1149 '''unwrap quotes if any; otherwise returns unmodified string'''
1150 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
1150 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
1151 return s
1151 return s
1152 return s[1:-1]
1152 return s[1:-1]
1153
1153
1154 class engine(object):
1154 class engine(object):
1155 '''template expansion engine.
1155 '''template expansion engine.
1156
1156
1157 template expansion works like this. a map file contains key=value
1157 template expansion works like this. a map file contains key=value
1158 pairs. if value is quoted, it is treated as string. otherwise, it
1158 pairs. if value is quoted, it is treated as string. otherwise, it
1159 is treated as name of template file.
1159 is treated as name of template file.
1160
1160
1161 templater is asked to expand a key in map. it looks up key, and
1161 templater is asked to expand a key in map. it looks up key, and
1162 looks for strings like this: {foo}. it expands {foo} by looking up
1162 looks for strings like this: {foo}. it expands {foo} by looking up
1163 foo in map, and substituting it. expansion is recursive: it stops
1163 foo in map, and substituting it. expansion is recursive: it stops
1164 when there is no more {foo} to replace.
1164 when there is no more {foo} to replace.
1165
1165
1166 expansion also allows formatting and filtering.
1166 expansion also allows formatting and filtering.
1167
1167
1168 format uses key to expand each item in list. syntax is
1168 format uses key to expand each item in list. syntax is
1169 {key%format}.
1169 {key%format}.
1170
1170
1171 filter uses function to transform value. syntax is
1171 filter uses function to transform value. syntax is
1172 {key|filter1|filter2|...}.'''
1172 {key|filter1|filter2|...}.'''
1173
1173
1174 def __init__(self, loader, filters=None, defaults=None, resources=None,
1174 def __init__(self, loader, filters=None, defaults=None, resources=None,
1175 aliases=()):
1175 aliases=()):
1176 self._loader = loader
1176 self._loader = loader
1177 if filters is None:
1177 if filters is None:
1178 filters = {}
1178 filters = {}
1179 self._filters = filters
1179 self._filters = filters
1180 self._funcs = funcs # make this a parameter if needed
1180 self._funcs = funcs # make this a parameter if needed
1181 if defaults is None:
1181 if defaults is None:
1182 defaults = {}
1182 defaults = {}
1183 if resources is None:
1183 if resources is None:
1184 resources = {}
1184 resources = {}
1185 self._defaults = defaults
1185 self._defaults = defaults
1186 self._resources = resources
1186 self._resources = resources
1187 self._aliasmap = _aliasrules.buildmap(aliases)
1187 self._aliasmap = _aliasrules.buildmap(aliases)
1188 self._cache = {} # key: (func, data)
1188 self._cache = {} # key: (func, data)
1189
1189
1190 def symbol(self, mapping, key):
1190 def symbol(self, mapping, key):
1191 """Resolve symbol to value or function; None if nothing found"""
1191 """Resolve symbol to value or function; None if nothing found"""
1192 v = None
1192 v = None
1193 if key not in self._resources:
1193 if key not in self._resources:
1194 v = mapping.get(key)
1194 v = mapping.get(key)
1195 if v is None:
1195 if v is None:
1196 v = self._defaults.get(key)
1196 v = self._defaults.get(key)
1197 return v
1197 return v
1198
1198
1199 def resource(self, mapping, key):
1199 def resource(self, mapping, key):
1200 """Return internal data (e.g. cache) used for keyword/function
1200 """Return internal data (e.g. cache) used for keyword/function
1201 evaluation"""
1201 evaluation"""
1202 v = None
1202 v = None
1203 if key in self._resources:
1203 if key in self._resources:
1204 v = mapping.get(key)
1204 v = mapping.get(key)
1205 if v is None:
1205 if v is None:
1206 v = self._resources.get(key)
1206 v = self._resources.get(key)
1207 if v is None:
1207 if v is None:
1208 raise templateutil.ResourceUnavailable(
1208 raise templateutil.ResourceUnavailable(
1209 _('template resource not available: %s') % key)
1209 _('template resource not available: %s') % key)
1210 return v
1210 return v
1211
1211
1212 def _load(self, t):
1212 def _load(self, t):
1213 '''load, parse, and cache a template'''
1213 '''load, parse, and cache a template'''
1214 if t not in self._cache:
1214 if t not in self._cache:
1215 # put poison to cut recursion while compiling 't'
1215 # put poison to cut recursion while compiling 't'
1216 self._cache[t] = (_runrecursivesymbol, t)
1216 self._cache[t] = (_runrecursivesymbol, t)
1217 try:
1217 try:
1218 x = parse(self._loader(t))
1218 x = parse(self._loader(t))
1219 if self._aliasmap:
1219 if self._aliasmap:
1220 x = _aliasrules.expand(self._aliasmap, x)
1220 x = _aliasrules.expand(self._aliasmap, x)
1221 self._cache[t] = compileexp(x, self, methods)
1221 self._cache[t] = compileexp(x, self, methods)
1222 except: # re-raises
1222 except: # re-raises
1223 del self._cache[t]
1223 del self._cache[t]
1224 raise
1224 raise
1225 return self._cache[t]
1225 return self._cache[t]
1226
1226
1227 def process(self, t, mapping):
1227 def process(self, t, mapping):
1228 '''Perform expansion. t is name of map element to expand.
1228 '''Perform expansion. t is name of map element to expand.
1229 mapping contains added elements for use during expansion. Is a
1229 mapping contains added elements for use during expansion. Is a
1230 generator.'''
1230 generator.'''
1231 func, data = self._load(t)
1231 func, data = self._load(t)
1232 return _flatten(func(self, mapping, data))
1232 return _flatten(func(self, mapping, data))
1233
1233
1234 engines = {'default': engine}
1234 engines = {'default': engine}
1235
1235
1236 def stylelist():
1236 def stylelist():
1237 paths = templatepaths()
1237 paths = templatepaths()
1238 if not paths:
1238 if not paths:
1239 return _('no templates found, try `hg debuginstall` for more info')
1239 return _('no templates found, try `hg debuginstall` for more info')
1240 dirlist = os.listdir(paths[0])
1240 dirlist = os.listdir(paths[0])
1241 stylelist = []
1241 stylelist = []
1242 for file in dirlist:
1242 for file in dirlist:
1243 split = file.split(".")
1243 split = file.split(".")
1244 if split[-1] in ('orig', 'rej'):
1244 if split[-1] in ('orig', 'rej'):
1245 continue
1245 continue
1246 if split[0] == "map-cmdline":
1246 if split[0] == "map-cmdline":
1247 stylelist.append(split[1])
1247 stylelist.append(split[1])
1248 return ", ".join(sorted(stylelist))
1248 return ", ".join(sorted(stylelist))
1249
1249
1250 def _readmapfile(mapfile):
1250 def _readmapfile(mapfile):
1251 """Load template elements from the given map file"""
1251 """Load template elements from the given map file"""
1252 if not os.path.exists(mapfile):
1252 if not os.path.exists(mapfile):
1253 raise error.Abort(_("style '%s' not found") % mapfile,
1253 raise error.Abort(_("style '%s' not found") % mapfile,
1254 hint=_("available styles: %s") % stylelist())
1254 hint=_("available styles: %s") % stylelist())
1255
1255
1256 base = os.path.dirname(mapfile)
1256 base = os.path.dirname(mapfile)
1257 conf = config.config(includepaths=templatepaths())
1257 conf = config.config(includepaths=templatepaths())
1258 conf.read(mapfile, remap={'': 'templates'})
1258 conf.read(mapfile, remap={'': 'templates'})
1259
1259
1260 cache = {}
1260 cache = {}
1261 tmap = {}
1261 tmap = {}
1262 aliases = []
1262 aliases = []
1263
1263
1264 val = conf.get('templates', '__base__')
1264 val = conf.get('templates', '__base__')
1265 if val and val[0] not in "'\"":
1265 if val and val[0] not in "'\"":
1266 # treat as a pointer to a base class for this style
1266 # treat as a pointer to a base class for this style
1267 path = util.normpath(os.path.join(base, val))
1267 path = util.normpath(os.path.join(base, val))
1268
1268
1269 # fallback check in template paths
1269 # fallback check in template paths
1270 if not os.path.exists(path):
1270 if not os.path.exists(path):
1271 for p in templatepaths():
1271 for p in templatepaths():
1272 p2 = util.normpath(os.path.join(p, val))
1272 p2 = util.normpath(os.path.join(p, val))
1273 if os.path.isfile(p2):
1273 if os.path.isfile(p2):
1274 path = p2
1274 path = p2
1275 break
1275 break
1276 p3 = util.normpath(os.path.join(p2, "map"))
1276 p3 = util.normpath(os.path.join(p2, "map"))
1277 if os.path.isfile(p3):
1277 if os.path.isfile(p3):
1278 path = p3
1278 path = p3
1279 break
1279 break
1280
1280
1281 cache, tmap, aliases = _readmapfile(path)
1281 cache, tmap, aliases = _readmapfile(path)
1282
1282
1283 for key, val in conf['templates'].items():
1283 for key, val in conf['templates'].items():
1284 if not val:
1284 if not val:
1285 raise error.ParseError(_('missing value'),
1285 raise error.ParseError(_('missing value'),
1286 conf.source('templates', key))
1286 conf.source('templates', key))
1287 if val[0] in "'\"":
1287 if val[0] in "'\"":
1288 if val[0] != val[-1]:
1288 if val[0] != val[-1]:
1289 raise error.ParseError(_('unmatched quotes'),
1289 raise error.ParseError(_('unmatched quotes'),
1290 conf.source('templates', key))
1290 conf.source('templates', key))
1291 cache[key] = unquotestring(val)
1291 cache[key] = unquotestring(val)
1292 elif key != '__base__':
1292 elif key != '__base__':
1293 val = 'default', val
1293 val = 'default', val
1294 if ':' in val[1]:
1294 if ':' in val[1]:
1295 val = val[1].split(':', 1)
1295 val = val[1].split(':', 1)
1296 tmap[key] = val[0], os.path.join(base, val[1])
1296 tmap[key] = val[0], os.path.join(base, val[1])
1297 aliases.extend(conf['templatealias'].items())
1297 aliases.extend(conf['templatealias'].items())
1298 return cache, tmap, aliases
1298 return cache, tmap, aliases
1299
1299
1300 class templater(object):
1300 class templater(object):
1301
1301
1302 def __init__(self, filters=None, defaults=None, resources=None,
1302 def __init__(self, filters=None, defaults=None, resources=None,
1303 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
1303 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
1304 """Create template engine optionally with preloaded template fragments
1304 """Create template engine optionally with preloaded template fragments
1305
1305
1306 - ``filters``: a dict of functions to transform a value into another.
1306 - ``filters``: a dict of functions to transform a value into another.
1307 - ``defaults``: a dict of symbol values/functions; may be overridden
1307 - ``defaults``: a dict of symbol values/functions; may be overridden
1308 by a ``mapping`` dict.
1308 by a ``mapping`` dict.
1309 - ``resources``: a dict of internal data (e.g. cache), inaccessible
1309 - ``resources``: a dict of internal data (e.g. cache), inaccessible
1310 from user template; may be overridden by a ``mapping`` dict.
1310 from user template; may be overridden by a ``mapping`` dict.
1311 - ``cache``: a dict of preloaded template fragments.
1311 - ``cache``: a dict of preloaded template fragments.
1312 - ``aliases``: a list of alias (name, replacement) pairs.
1312 - ``aliases``: a list of alias (name, replacement) pairs.
1313
1313
1314 self.cache may be updated later to register additional template
1314 self.cache may be updated later to register additional template
1315 fragments.
1315 fragments.
1316 """
1316 """
1317 if filters is None:
1317 if filters is None:
1318 filters = {}
1318 filters = {}
1319 if defaults is None:
1319 if defaults is None:
1320 defaults = {}
1320 defaults = {}
1321 if resources is None:
1321 if resources is None:
1322 resources = {}
1322 resources = {}
1323 if cache is None:
1323 if cache is None:
1324 cache = {}
1324 cache = {}
1325 self.cache = cache.copy()
1325 self.cache = cache.copy()
1326 self.map = {}
1326 self.map = {}
1327 self.filters = templatefilters.filters.copy()
1327 self.filters = templatefilters.filters.copy()
1328 self.filters.update(filters)
1328 self.filters.update(filters)
1329 self.defaults = defaults
1329 self.defaults = defaults
1330 self._resources = {'templ': self}
1330 self._resources = {'templ': self}
1331 self._resources.update(resources)
1331 self._resources.update(resources)
1332 self._aliases = aliases
1332 self._aliases = aliases
1333 self.minchunk, self.maxchunk = minchunk, maxchunk
1333 self.minchunk, self.maxchunk = minchunk, maxchunk
1334 self.ecache = {}
1334 self.ecache = {}
1335
1335
1336 @classmethod
1336 @classmethod
1337 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
1337 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
1338 cache=None, minchunk=1024, maxchunk=65536):
1338 cache=None, minchunk=1024, maxchunk=65536):
1339 """Create templater from the specified map file"""
1339 """Create templater from the specified map file"""
1340 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
1340 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
1341 cache, tmap, aliases = _readmapfile(mapfile)
1341 cache, tmap, aliases = _readmapfile(mapfile)
1342 t.cache.update(cache)
1342 t.cache.update(cache)
1343 t.map = tmap
1343 t.map = tmap
1344 t._aliases = aliases
1344 t._aliases = aliases
1345 return t
1345 return t
1346
1346
1347 def __contains__(self, key):
1347 def __contains__(self, key):
1348 return key in self.cache or key in self.map
1348 return key in self.cache or key in self.map
1349
1349
1350 def load(self, t):
1350 def load(self, t):
1351 '''Get the template for the given template name. Use a local cache.'''
1351 '''Get the template for the given template name. Use a local cache.'''
1352 if t not in self.cache:
1352 if t not in self.cache:
1353 try:
1353 try:
1354 self.cache[t] = util.readfile(self.map[t][1])
1354 self.cache[t] = util.readfile(self.map[t][1])
1355 except KeyError as inst:
1355 except KeyError as inst:
1356 raise templateutil.TemplateNotFound(
1356 raise templateutil.TemplateNotFound(
1357 _('"%s" not in template map') % inst.args[0])
1357 _('"%s" not in template map') % inst.args[0])
1358 except IOError as inst:
1358 except IOError as inst:
1359 reason = (_('template file %s: %s')
1359 reason = (_('template file %s: %s')
1360 % (self.map[t][1], util.forcebytestr(inst.args[1])))
1360 % (self.map[t][1], util.forcebytestr(inst.args[1])))
1361 raise IOError(inst.args[0], encoding.strfromlocal(reason))
1361 raise IOError(inst.args[0], encoding.strfromlocal(reason))
1362 return self.cache[t]
1362 return self.cache[t]
1363
1363
1364 def render(self, mapping):
1364 def render(self, mapping):
1365 """Render the default unnamed template and return result as string"""
1365 """Render the default unnamed template and return result as string"""
1366 mapping = pycompat.strkwargs(mapping)
1366 mapping = pycompat.strkwargs(mapping)
1367 return templateutil.stringify(self('', **mapping))
1367 return templateutil.stringify(self('', **mapping))
1368
1368
1369 def __call__(self, t, **mapping):
1369 def __call__(self, t, **mapping):
1370 mapping = pycompat.byteskwargs(mapping)
1370 mapping = pycompat.byteskwargs(mapping)
1371 ttype = t in self.map and self.map[t][0] or 'default'
1371 ttype = t in self.map and self.map[t][0] or 'default'
1372 if ttype not in self.ecache:
1372 if ttype not in self.ecache:
1373 try:
1373 try:
1374 ecls = engines[ttype]
1374 ecls = engines[ttype]
1375 except KeyError:
1375 except KeyError:
1376 raise error.Abort(_('invalid template engine: %s') % ttype)
1376 raise error.Abort(_('invalid template engine: %s') % ttype)
1377 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
1377 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
1378 self._resources, self._aliases)
1378 self._resources, self._aliases)
1379 proc = self.ecache[ttype]
1379 proc = self.ecache[ttype]
1380
1380
1381 stream = proc.process(t, mapping)
1381 stream = proc.process(t, mapping)
1382 if self.minchunk:
1382 if self.minchunk:
1383 stream = util.increasingchunks(stream, min=self.minchunk,
1383 stream = util.increasingchunks(stream, min=self.minchunk,
1384 max=self.maxchunk)
1384 max=self.maxchunk)
1385 return stream
1385 return stream
1386
1386
1387 def templatepaths():
1387 def templatepaths():
1388 '''return locations used for template files.'''
1388 '''return locations used for template files.'''
1389 pathsrel = ['templates']
1389 pathsrel = ['templates']
1390 paths = [os.path.normpath(os.path.join(util.datapath, f))
1390 paths = [os.path.normpath(os.path.join(util.datapath, f))
1391 for f in pathsrel]
1391 for f in pathsrel]
1392 return [p for p in paths if os.path.isdir(p)]
1392 return [p for p in paths if os.path.isdir(p)]
1393
1393
1394 def templatepath(name):
1394 def templatepath(name):
1395 '''return location of template file. returns None if not found.'''
1395 '''return location of template file. returns None if not found.'''
1396 for p in templatepaths():
1396 for p in templatepaths():
1397 f = os.path.join(p, name)
1397 f = os.path.join(p, name)
1398 if os.path.exists(f):
1398 if os.path.exists(f):
1399 return f
1399 return f
1400 return None
1400 return None
1401
1401
1402 def stylemap(styles, paths=None):
1402 def stylemap(styles, paths=None):
1403 """Return path to mapfile for a given style.
1403 """Return path to mapfile for a given style.
1404
1404
1405 Searches mapfile in the following locations:
1405 Searches mapfile in the following locations:
1406 1. templatepath/style/map
1406 1. templatepath/style/map
1407 2. templatepath/map-style
1407 2. templatepath/map-style
1408 3. templatepath/map
1408 3. templatepath/map
1409 """
1409 """
1410
1410
1411 if paths is None:
1411 if paths is None:
1412 paths = templatepaths()
1412 paths = templatepaths()
1413 elif isinstance(paths, bytes):
1413 elif isinstance(paths, bytes):
1414 paths = [paths]
1414 paths = [paths]
1415
1415
1416 if isinstance(styles, bytes):
1416 if isinstance(styles, bytes):
1417 styles = [styles]
1417 styles = [styles]
1418
1418
1419 for style in styles:
1419 for style in styles:
1420 # only plain name is allowed to honor template paths
1420 # only plain name is allowed to honor template paths
1421 if (not style
1421 if (not style
1422 or style in (pycompat.oscurdir, pycompat.ospardir)
1422 or style in (pycompat.oscurdir, pycompat.ospardir)
1423 or pycompat.ossep in style
1423 or pycompat.ossep in style
1424 or pycompat.osaltsep and pycompat.osaltsep in style):
1424 or pycompat.osaltsep and pycompat.osaltsep in style):
1425 continue
1425 continue
1426 locations = [os.path.join(style, 'map'), 'map-' + style]
1426 locations = [os.path.join(style, 'map'), 'map-' + style]
1427 locations.append('map')
1427 locations.append('map')
1428
1428
1429 for path in paths:
1429 for path in paths:
1430 for location in locations:
1430 for location in locations:
1431 mapfile = os.path.join(path, location)
1431 mapfile = os.path.join(path, location)
1432 if os.path.isfile(mapfile):
1432 if os.path.isfile(mapfile):
1433 return style, mapfile
1433 return style, mapfile
1434
1434
1435 raise RuntimeError("No hgweb templates found in %r" % paths)
1435 raise RuntimeError("No hgweb templates found in %r" % paths)
1436
1436
1437 def loadfunction(ui, extname, registrarobj):
1437 def loadfunction(ui, extname, registrarobj):
1438 """Load template function from specified registrarobj
1438 """Load template function from specified registrarobj
1439 """
1439 """
1440 for name, func in registrarobj._table.iteritems():
1440 for name, func in registrarobj._table.iteritems():
1441 funcs[name] = func
1441 funcs[name] = func
1442
1442
1443 # tell hggettext to extract docstrings from these functions:
1443 # tell hggettext to extract docstrings from these functions:
1444 i18nfunctions = funcs.values()
1444 i18nfunctions = funcs.values()
@@ -1,239 +1,448 b''
1 # templateutil.py - utility for template evaluation
1 # templateutil.py - utility for template evaluation
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
8 from __future__ import absolute_import
9
9
10 import types
10 import types
11
11
12 from .i18n import _
12 from .i18n import _
13 from . import (
13 from . import (
14 error,
14 error,
15 pycompat,
15 pycompat,
16 templatekw,
17 util,
16 util,
18 )
17 )
19
18
20 class ResourceUnavailable(error.Abort):
19 class ResourceUnavailable(error.Abort):
21 pass
20 pass
22
21
23 class TemplateNotFound(error.Abort):
22 class TemplateNotFound(error.Abort):
24 pass
23 pass
25
24
25 class hybrid(object):
26 """Wrapper for list or dict to support legacy template
27
28 This class allows us to handle both:
29 - "{files}" (legacy command-line-specific list hack) and
30 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
31 and to access raw values:
32 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
33 - "{get(extras, key)}"
34 - "{files|json}"
35 """
36
37 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
38 if gen is not None:
39 self.gen = gen # generator or function returning generator
40 self._values = values
41 self._makemap = makemap
42 self.joinfmt = joinfmt
43 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
44 def gen(self):
45 """Default generator to stringify this as {join(self, ' ')}"""
46 for i, x in enumerate(self._values):
47 if i > 0:
48 yield ' '
49 yield self.joinfmt(x)
50 def itermaps(self):
51 makemap = self._makemap
52 for x in self._values:
53 yield makemap(x)
54 def __contains__(self, x):
55 return x in self._values
56 def __getitem__(self, key):
57 return self._values[key]
58 def __len__(self):
59 return len(self._values)
60 def __iter__(self):
61 return iter(self._values)
62 def __getattr__(self, name):
63 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
64 r'itervalues', r'keys', r'values'):
65 raise AttributeError(name)
66 return getattr(self._values, name)
67
68 class mappable(object):
69 """Wrapper for non-list/dict object to support map operation
70
71 This class allows us to handle both:
72 - "{manifest}"
73 - "{manifest % '{rev}:{node}'}"
74 - "{manifest.rev}"
75
76 Unlike a hybrid, this does not simulate the behavior of the underling
77 value. Use unwrapvalue() or unwraphybrid() to obtain the inner object.
78 """
79
80 def __init__(self, gen, key, value, makemap):
81 if gen is not None:
82 self.gen = gen # generator or function returning generator
83 self._key = key
84 self._value = value # may be generator of strings
85 self._makemap = makemap
86
87 def gen(self):
88 yield pycompat.bytestr(self._value)
89
90 def tomap(self):
91 return self._makemap(self._key)
92
93 def itermaps(self):
94 yield self.tomap()
95
96 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
97 """Wrap data to support both dict-like and string-like operations"""
98 prefmt = pycompat.identity
99 if fmt is None:
100 fmt = '%s=%s'
101 prefmt = pycompat.bytestr
102 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
103 lambda k: fmt % (prefmt(k), prefmt(data[k])))
104
105 def hybridlist(data, name, fmt=None, gen=None):
106 """Wrap data to support both list-like and string-like operations"""
107 prefmt = pycompat.identity
108 if fmt is None:
109 fmt = '%s'
110 prefmt = pycompat.bytestr
111 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
112
113 def unwraphybrid(thing):
114 """Return an object which can be stringified possibly by using a legacy
115 template"""
116 gen = getattr(thing, 'gen', None)
117 if gen is None:
118 return thing
119 if callable(gen):
120 return gen()
121 return gen
122
123 def unwrapvalue(thing):
124 """Move the inner value object out of the wrapper"""
125 if not util.safehasattr(thing, '_value'):
126 return thing
127 return thing._value
128
129 def wraphybridvalue(container, key, value):
130 """Wrap an element of hybrid container to be mappable
131
132 The key is passed to the makemap function of the given container, which
133 should be an item generated by iter(container).
134 """
135 makemap = getattr(container, '_makemap', None)
136 if makemap is None:
137 return value
138 if util.safehasattr(value, '_makemap'):
139 # a nested hybrid list/dict, which has its own way of map operation
140 return value
141 return mappable(None, key, value, makemap)
142
143 def compatdict(context, mapping, name, data, key='key', value='value',
144 fmt=None, plural=None, separator=' '):
145 """Wrap data like hybriddict(), but also supports old-style list template
146
147 This exists for backward compatibility with the old-style template. Use
148 hybriddict() for new template keywords.
149 """
150 c = [{key: k, value: v} for k, v in data.iteritems()]
151 t = context.resource(mapping, 'templ')
152 f = _showlist(name, c, t, mapping, plural, separator)
153 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
154
155 def compatlist(context, mapping, name, data, element=None, fmt=None,
156 plural=None, separator=' '):
157 """Wrap data like hybridlist(), but also supports old-style list template
158
159 This exists for backward compatibility with the old-style template. Use
160 hybridlist() for new template keywords.
161 """
162 t = context.resource(mapping, 'templ')
163 f = _showlist(name, data, t, mapping, plural, separator)
164 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
165
166 def _showlist(name, values, templ, mapping, plural=None, separator=' '):
167 '''expand set of values.
168 name is name of key in template map.
169 values is list of strings or dicts.
170 plural is plural of name, if not simply name + 's'.
171 separator is used to join values as a string
172
173 expansion works like this, given name 'foo'.
174
175 if values is empty, expand 'no_foos'.
176
177 if 'foo' not in template map, return values as a string,
178 joined by 'separator'.
179
180 expand 'start_foos'.
181
182 for each value, expand 'foo'. if 'last_foo' in template
183 map, expand it instead of 'foo' for last key.
184
185 expand 'end_foos'.
186 '''
187 strmapping = pycompat.strkwargs(mapping)
188 if not plural:
189 plural = name + 's'
190 if not values:
191 noname = 'no_' + plural
192 if noname in templ:
193 yield templ(noname, **strmapping)
194 return
195 if name not in templ:
196 if isinstance(values[0], bytes):
197 yield separator.join(values)
198 else:
199 for v in values:
200 r = dict(v)
201 r.update(mapping)
202 yield r
203 return
204 startname = 'start_' + plural
205 if startname in templ:
206 yield templ(startname, **strmapping)
207 vmapping = mapping.copy()
208 def one(v, tag=name):
209 try:
210 vmapping.update(v)
211 # Python 2 raises ValueError if the type of v is wrong. Python
212 # 3 raises TypeError.
213 except (AttributeError, TypeError, ValueError):
214 try:
215 # Python 2 raises ValueError trying to destructure an e.g.
216 # bytes. Python 3 raises TypeError.
217 for a, b in v:
218 vmapping[a] = b
219 except (TypeError, ValueError):
220 vmapping[name] = v
221 return templ(tag, **pycompat.strkwargs(vmapping))
222 lastname = 'last_' + name
223 if lastname in templ:
224 last = values.pop()
225 else:
226 last = None
227 for v in values:
228 yield one(v)
229 if last is not None:
230 yield one(last, tag=lastname)
231 endname = 'end_' + plural
232 if endname in templ:
233 yield templ(endname, **strmapping)
234
26 def stringify(thing):
235 def stringify(thing):
27 """Turn values into bytes by converting into text and concatenating them"""
236 """Turn values into bytes by converting into text and concatenating them"""
28 thing = templatekw.unwraphybrid(thing)
237 thing = unwraphybrid(thing)
29 if util.safehasattr(thing, '__iter__') and not isinstance(thing, bytes):
238 if util.safehasattr(thing, '__iter__') and not isinstance(thing, bytes):
30 if isinstance(thing, str):
239 if isinstance(thing, str):
31 # This is only reachable on Python 3 (otherwise
240 # This is only reachable on Python 3 (otherwise
32 # isinstance(thing, bytes) would have been true), and is
241 # isinstance(thing, bytes) would have been true), and is
33 # here to prevent infinite recursion bugs on Python 3.
242 # here to prevent infinite recursion bugs on Python 3.
34 raise error.ProgrammingError(
243 raise error.ProgrammingError(
35 'stringify got unexpected unicode string: %r' % thing)
244 'stringify got unexpected unicode string: %r' % thing)
36 return "".join([stringify(t) for t in thing if t is not None])
245 return "".join([stringify(t) for t in thing if t is not None])
37 if thing is None:
246 if thing is None:
38 return ""
247 return ""
39 return pycompat.bytestr(thing)
248 return pycompat.bytestr(thing)
40
249
41 def findsymbolicname(arg):
250 def findsymbolicname(arg):
42 """Find symbolic name for the given compiled expression; returns None
251 """Find symbolic name for the given compiled expression; returns None
43 if nothing found reliably"""
252 if nothing found reliably"""
44 while True:
253 while True:
45 func, data = arg
254 func, data = arg
46 if func is runsymbol:
255 if func is runsymbol:
47 return data
256 return data
48 elif func is runfilter:
257 elif func is runfilter:
49 arg = data[0]
258 arg = data[0]
50 else:
259 else:
51 return None
260 return None
52
261
53 def evalrawexp(context, mapping, arg):
262 def evalrawexp(context, mapping, arg):
54 """Evaluate given argument as a bare template object which may require
263 """Evaluate given argument as a bare template object which may require
55 further processing (such as folding generator of strings)"""
264 further processing (such as folding generator of strings)"""
56 func, data = arg
265 func, data = arg
57 return func(context, mapping, data)
266 return func(context, mapping, data)
58
267
59 def evalfuncarg(context, mapping, arg):
268 def evalfuncarg(context, mapping, arg):
60 """Evaluate given argument as value type"""
269 """Evaluate given argument as value type"""
61 thing = evalrawexp(context, mapping, arg)
270 thing = evalrawexp(context, mapping, arg)
62 thing = templatekw.unwrapvalue(thing)
271 thing = unwrapvalue(thing)
63 # evalrawexp() may return string, generator of strings or arbitrary object
272 # evalrawexp() may return string, generator of strings or arbitrary object
64 # such as date tuple, but filter does not want generator.
273 # such as date tuple, but filter does not want generator.
65 if isinstance(thing, types.GeneratorType):
274 if isinstance(thing, types.GeneratorType):
66 thing = stringify(thing)
275 thing = stringify(thing)
67 return thing
276 return thing
68
277
69 def evalboolean(context, mapping, arg):
278 def evalboolean(context, mapping, arg):
70 """Evaluate given argument as boolean, but also takes boolean literals"""
279 """Evaluate given argument as boolean, but also takes boolean literals"""
71 func, data = arg
280 func, data = arg
72 if func is runsymbol:
281 if func is runsymbol:
73 thing = func(context, mapping, data, default=None)
282 thing = func(context, mapping, data, default=None)
74 if thing is None:
283 if thing is None:
75 # not a template keyword, takes as a boolean literal
284 # not a template keyword, takes as a boolean literal
76 thing = util.parsebool(data)
285 thing = util.parsebool(data)
77 else:
286 else:
78 thing = func(context, mapping, data)
287 thing = func(context, mapping, data)
79 thing = templatekw.unwrapvalue(thing)
288 thing = unwrapvalue(thing)
80 if isinstance(thing, bool):
289 if isinstance(thing, bool):
81 return thing
290 return thing
82 # other objects are evaluated as strings, which means 0 is True, but
291 # other objects are evaluated as strings, which means 0 is True, but
83 # empty dict/list should be False as they are expected to be ''
292 # empty dict/list should be False as they are expected to be ''
84 return bool(stringify(thing))
293 return bool(stringify(thing))
85
294
86 def evalinteger(context, mapping, arg, err=None):
295 def evalinteger(context, mapping, arg, err=None):
87 v = evalfuncarg(context, mapping, arg)
296 v = evalfuncarg(context, mapping, arg)
88 try:
297 try:
89 return int(v)
298 return int(v)
90 except (TypeError, ValueError):
299 except (TypeError, ValueError):
91 raise error.ParseError(err or _('not an integer'))
300 raise error.ParseError(err or _('not an integer'))
92
301
93 def evalstring(context, mapping, arg):
302 def evalstring(context, mapping, arg):
94 return stringify(evalrawexp(context, mapping, arg))
303 return stringify(evalrawexp(context, mapping, arg))
95
304
96 def evalstringliteral(context, mapping, arg):
305 def evalstringliteral(context, mapping, arg):
97 """Evaluate given argument as string template, but returns symbol name
306 """Evaluate given argument as string template, but returns symbol name
98 if it is unknown"""
307 if it is unknown"""
99 func, data = arg
308 func, data = arg
100 if func is runsymbol:
309 if func is runsymbol:
101 thing = func(context, mapping, data, default=data)
310 thing = func(context, mapping, data, default=data)
102 else:
311 else:
103 thing = func(context, mapping, data)
312 thing = func(context, mapping, data)
104 return stringify(thing)
313 return stringify(thing)
105
314
106 _evalfuncbytype = {
315 _evalfuncbytype = {
107 bool: evalboolean,
316 bool: evalboolean,
108 bytes: evalstring,
317 bytes: evalstring,
109 int: evalinteger,
318 int: evalinteger,
110 }
319 }
111
320
112 def evalastype(context, mapping, arg, typ):
321 def evalastype(context, mapping, arg, typ):
113 """Evaluate given argument and coerce its type"""
322 """Evaluate given argument and coerce its type"""
114 try:
323 try:
115 f = _evalfuncbytype[typ]
324 f = _evalfuncbytype[typ]
116 except KeyError:
325 except KeyError:
117 raise error.ProgrammingError('invalid type specified: %r' % typ)
326 raise error.ProgrammingError('invalid type specified: %r' % typ)
118 return f(context, mapping, arg)
327 return f(context, mapping, arg)
119
328
120 def runinteger(context, mapping, data):
329 def runinteger(context, mapping, data):
121 return int(data)
330 return int(data)
122
331
123 def runstring(context, mapping, data):
332 def runstring(context, mapping, data):
124 return data
333 return data
125
334
126 def _recursivesymbolblocker(key):
335 def _recursivesymbolblocker(key):
127 def showrecursion(**args):
336 def showrecursion(**args):
128 raise error.Abort(_("recursive reference '%s' in template") % key)
337 raise error.Abort(_("recursive reference '%s' in template") % key)
129 return showrecursion
338 return showrecursion
130
339
131 def runsymbol(context, mapping, key, default=''):
340 def runsymbol(context, mapping, key, default=''):
132 v = context.symbol(mapping, key)
341 v = context.symbol(mapping, key)
133 if v is None:
342 if v is None:
134 # put poison to cut recursion. we can't move this to parsing phase
343 # put poison to cut recursion. we can't move this to parsing phase
135 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
344 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
136 safemapping = mapping.copy()
345 safemapping = mapping.copy()
137 safemapping[key] = _recursivesymbolblocker(key)
346 safemapping[key] = _recursivesymbolblocker(key)
138 try:
347 try:
139 v = context.process(key, safemapping)
348 v = context.process(key, safemapping)
140 except TemplateNotFound:
349 except TemplateNotFound:
141 v = default
350 v = default
142 if callable(v) and getattr(v, '_requires', None) is None:
351 if callable(v) and getattr(v, '_requires', None) is None:
143 # old templatekw: expand all keywords and resources
352 # old templatekw: expand all keywords and resources
144 props = context._resources.copy()
353 props = context._resources.copy()
145 props.update(mapping)
354 props.update(mapping)
146 return v(**pycompat.strkwargs(props))
355 return v(**pycompat.strkwargs(props))
147 if callable(v):
356 if callable(v):
148 # new templatekw
357 # new templatekw
149 try:
358 try:
150 return v(context, mapping)
359 return v(context, mapping)
151 except ResourceUnavailable:
360 except ResourceUnavailable:
152 # unsupported keyword is mapped to empty just like unknown keyword
361 # unsupported keyword is mapped to empty just like unknown keyword
153 return None
362 return None
154 return v
363 return v
155
364
156 def runtemplate(context, mapping, template):
365 def runtemplate(context, mapping, template):
157 for arg in template:
366 for arg in template:
158 yield evalrawexp(context, mapping, arg)
367 yield evalrawexp(context, mapping, arg)
159
368
160 def runfilter(context, mapping, data):
369 def runfilter(context, mapping, data):
161 arg, filt = data
370 arg, filt = data
162 thing = evalfuncarg(context, mapping, arg)
371 thing = evalfuncarg(context, mapping, arg)
163 try:
372 try:
164 return filt(thing)
373 return filt(thing)
165 except (ValueError, AttributeError, TypeError):
374 except (ValueError, AttributeError, TypeError):
166 sym = findsymbolicname(arg)
375 sym = findsymbolicname(arg)
167 if sym:
376 if sym:
168 msg = (_("template filter '%s' is not compatible with keyword '%s'")
377 msg = (_("template filter '%s' is not compatible with keyword '%s'")
169 % (pycompat.sysbytes(filt.__name__), sym))
378 % (pycompat.sysbytes(filt.__name__), sym))
170 else:
379 else:
171 msg = (_("incompatible use of template filter '%s'")
380 msg = (_("incompatible use of template filter '%s'")
172 % pycompat.sysbytes(filt.__name__))
381 % pycompat.sysbytes(filt.__name__))
173 raise error.Abort(msg)
382 raise error.Abort(msg)
174
383
175 def runmap(context, mapping, data):
384 def runmap(context, mapping, data):
176 darg, targ = data
385 darg, targ = data
177 d = evalrawexp(context, mapping, darg)
386 d = evalrawexp(context, mapping, darg)
178 if util.safehasattr(d, 'itermaps'):
387 if util.safehasattr(d, 'itermaps'):
179 diter = d.itermaps()
388 diter = d.itermaps()
180 else:
389 else:
181 try:
390 try:
182 diter = iter(d)
391 diter = iter(d)
183 except TypeError:
392 except TypeError:
184 sym = findsymbolicname(darg)
393 sym = findsymbolicname(darg)
185 if sym:
394 if sym:
186 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
395 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
187 else:
396 else:
188 raise error.ParseError(_("%r is not iterable") % d)
397 raise error.ParseError(_("%r is not iterable") % d)
189
398
190 for i, v in enumerate(diter):
399 for i, v in enumerate(diter):
191 lm = mapping.copy()
400 lm = mapping.copy()
192 lm['index'] = i
401 lm['index'] = i
193 if isinstance(v, dict):
402 if isinstance(v, dict):
194 lm.update(v)
403 lm.update(v)
195 lm['originalnode'] = mapping.get('node')
404 lm['originalnode'] = mapping.get('node')
196 yield evalrawexp(context, lm, targ)
405 yield evalrawexp(context, lm, targ)
197 else:
406 else:
198 # v is not an iterable of dicts, this happen when 'key'
407 # v is not an iterable of dicts, this happen when 'key'
199 # has been fully expanded already and format is useless.
408 # has been fully expanded already and format is useless.
200 # If so, return the expanded value.
409 # If so, return the expanded value.
201 yield v
410 yield v
202
411
203 def runmember(context, mapping, data):
412 def runmember(context, mapping, data):
204 darg, memb = data
413 darg, memb = data
205 d = evalrawexp(context, mapping, darg)
414 d = evalrawexp(context, mapping, darg)
206 if util.safehasattr(d, 'tomap'):
415 if util.safehasattr(d, 'tomap'):
207 lm = mapping.copy()
416 lm = mapping.copy()
208 lm.update(d.tomap())
417 lm.update(d.tomap())
209 return runsymbol(context, lm, memb)
418 return runsymbol(context, lm, memb)
210 if util.safehasattr(d, 'get'):
419 if util.safehasattr(d, 'get'):
211 return getdictitem(d, memb)
420 return getdictitem(d, memb)
212
421
213 sym = findsymbolicname(darg)
422 sym = findsymbolicname(darg)
214 if sym:
423 if sym:
215 raise error.ParseError(_("keyword '%s' has no member") % sym)
424 raise error.ParseError(_("keyword '%s' has no member") % sym)
216 else:
425 else:
217 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
426 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
218
427
219 def runnegate(context, mapping, data):
428 def runnegate(context, mapping, data):
220 data = evalinteger(context, mapping, data,
429 data = evalinteger(context, mapping, data,
221 _('negation needs an integer argument'))
430 _('negation needs an integer argument'))
222 return -data
431 return -data
223
432
224 def runarithmetic(context, mapping, data):
433 def runarithmetic(context, mapping, data):
225 func, left, right = data
434 func, left, right = data
226 left = evalinteger(context, mapping, left,
435 left = evalinteger(context, mapping, left,
227 _('arithmetic only defined on integers'))
436 _('arithmetic only defined on integers'))
228 right = evalinteger(context, mapping, right,
437 right = evalinteger(context, mapping, right,
229 _('arithmetic only defined on integers'))
438 _('arithmetic only defined on integers'))
230 try:
439 try:
231 return func(left, right)
440 return func(left, right)
232 except ZeroDivisionError:
441 except ZeroDivisionError:
233 raise error.Abort(_('division by zero is not defined'))
442 raise error.Abort(_('division by zero is not defined'))
234
443
235 def getdictitem(dictarg, key):
444 def getdictitem(dictarg, key):
236 val = dictarg.get(key)
445 val = dictarg.get(key)
237 if val is None:
446 if val is None:
238 return
447 return
239 return templatekw.wraphybridvalue(dictarg, key, val)
448 return wraphybridvalue(dictarg, key, val)
General Comments 0
You need to be logged in to leave comments. Login now