##// END OF EJS Templates
templater: use template context to render old-style list template...
Yuya Nishihara -
r37086:aa97e06a default
parent child Browse files
Show More
@@ -1,391 +1,390
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 templateutil,
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'})
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')
365
364
366 pointers = wrapper.pointersfromctx(ctx, removed=True) # {path: pointer}
365 pointers = wrapper.pointersfromctx(ctx, removed=True) # {path: pointer}
367 files = sorted(pointers.keys())
366 files = sorted(pointers.keys())
368
367
369 def pointer(v):
368 def pointer(v):
370 # In the file spec, version is first and the other keys are sorted.
369 # In the file spec, version is first and the other keys are sorted.
371 sortkeyfunc = lambda x: (x[0] != 'version', x)
370 sortkeyfunc = lambda x: (x[0] != 'version', x)
372 items = sorted(pointers[v].iteritems(), key=sortkeyfunc)
371 items = sorted(pointers[v].iteritems(), key=sortkeyfunc)
373 return util.sortdict(items)
372 return util.sortdict(items)
374
373
375 makemap = lambda v: {
374 makemap = lambda v: {
376 'file': v,
375 'file': v,
377 'lfsoid': pointers[v].oid() if pointers[v] else None,
376 'lfsoid': pointers[v].oid() if pointers[v] else None,
378 'lfspointer': templateutil.hybriddict(pointer(v)),
377 'lfspointer': templateutil.hybriddict(pointer(v)),
379 }
378 }
380
379
381 # TODO: make the separator ', '?
380 # TODO: make the separator ', '?
382 f = templateutil._showlist('lfs_file', files, templ, mapping)
381 f = templateutil._showcompatlist(context, mapping, 'lfs_file', files)
383 return templateutil.hybrid(f, files, makemap, pycompat.identity)
382 return templateutil.hybrid(f, files, makemap, pycompat.identity)
384
383
385 @command('debuglfsupload',
384 @command('debuglfsupload',
386 [('r', 'rev', [], _('upload large files introduced by REV'))])
385 [('r', 'rev', [], _('upload large files introduced by REV'))])
387 def debuglfsupload(ui, repo, **opts):
386 def debuglfsupload(ui, repo, **opts):
388 """upload lfs blobs added by the working copy parent or given revisions"""
387 """upload lfs blobs added by the working copy parent or given revisions"""
389 revs = opts.get(r'rev', [])
388 revs = opts.get(r'rev', [])
390 pointers = wrapper.extractpointers(repo, scmutil.revrange(repo, revs))
389 pointers = wrapper.extractpointers(repo, scmutil.revrange(repo, revs))
391 wrapper.uploadblobs(repo, pointers)
390 wrapper.uploadblobs(repo, pointers)
@@ -1,305 +1,305
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 from mercurial.i18n import _
25 from mercurial.i18n import _
26
26
27 from mercurial.node import (
27 from mercurial.node import (
28 bin,
28 bin,
29 )
29 )
30 from mercurial import (
30 from mercurial import (
31 logexchange,
31 logexchange,
32 namespaces,
32 namespaces,
33 pycompat,
33 pycompat,
34 registrar,
34 registrar,
35 revsetlang,
35 revsetlang,
36 smartset,
36 smartset,
37 templateutil,
37 templateutil,
38 )
38 )
39
39
40 if pycompat.ispy3:
40 if pycompat.ispy3:
41 import collections.abc
41 import collections.abc
42 mutablemapping = collections.abc.MutableMapping
42 mutablemapping = collections.abc.MutableMapping
43 else:
43 else:
44 import collections
44 import collections
45 mutablemapping = collections.MutableMapping
45 mutablemapping = collections.MutableMapping
46
46
47 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
47 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
48 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
48 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
49 # be specifying the version(s) of Mercurial they are tested with, or
49 # be specifying the version(s) of Mercurial they are tested with, or
50 # leave the attribute unspecified.
50 # leave the attribute unspecified.
51 testedwith = 'ships-with-hg-core'
51 testedwith = 'ships-with-hg-core'
52
52
53 configtable = {}
53 configtable = {}
54 configitem = registrar.configitem(configtable)
54 configitem = registrar.configitem(configtable)
55 templatekeyword = registrar.templatekeyword()
55 templatekeyword = registrar.templatekeyword()
56 revsetpredicate = registrar.revsetpredicate()
56 revsetpredicate = registrar.revsetpredicate()
57
57
58 configitem('remotenames', 'bookmarks',
58 configitem('remotenames', 'bookmarks',
59 default=True,
59 default=True,
60 )
60 )
61 configitem('remotenames', 'branches',
61 configitem('remotenames', 'branches',
62 default=True,
62 default=True,
63 )
63 )
64
64
65 class lazyremotenamedict(mutablemapping):
65 class lazyremotenamedict(mutablemapping):
66 """
66 """
67 Read-only dict-like Class to lazily resolve remotename entries
67 Read-only dict-like Class to lazily resolve remotename entries
68
68
69 We are doing that because remotenames startup was slow.
69 We are doing that because remotenames startup was slow.
70 We lazily read the remotenames file once to figure out the potential entries
70 We lazily read the remotenames file once to figure out the potential entries
71 and store them in self.potentialentries. Then when asked to resolve an
71 and store them in self.potentialentries. Then when asked to resolve an
72 entry, if it is not in self.potentialentries, then it isn't there, if it
72 entry, if it is not in self.potentialentries, then it isn't there, if it
73 is in self.potentialentries we resolve it and store the result in
73 is in self.potentialentries we resolve it and store the result in
74 self.cache. We cannot be lazy is when asked all the entries (keys).
74 self.cache. We cannot be lazy is when asked all the entries (keys).
75 """
75 """
76 def __init__(self, kind, repo):
76 def __init__(self, kind, repo):
77 self.cache = {}
77 self.cache = {}
78 self.potentialentries = {}
78 self.potentialentries = {}
79 self._kind = kind # bookmarks or branches
79 self._kind = kind # bookmarks or branches
80 self._repo = repo
80 self._repo = repo
81 self.loaded = False
81 self.loaded = False
82
82
83 def _load(self):
83 def _load(self):
84 """ Read the remotenames file, store entries matching selected kind """
84 """ Read the remotenames file, store entries matching selected kind """
85 self.loaded = True
85 self.loaded = True
86 repo = self._repo
86 repo = self._repo
87 for node, rpath, rname in logexchange.readremotenamefile(repo,
87 for node, rpath, rname in logexchange.readremotenamefile(repo,
88 self._kind):
88 self._kind):
89 name = rpath + '/' + rname
89 name = rpath + '/' + rname
90 self.potentialentries[name] = (node, rpath, name)
90 self.potentialentries[name] = (node, rpath, name)
91
91
92 def _resolvedata(self, potentialentry):
92 def _resolvedata(self, potentialentry):
93 """ Check that the node for potentialentry exists and return it """
93 """ Check that the node for potentialentry exists and return it """
94 if not potentialentry in self.potentialentries:
94 if not potentialentry in self.potentialentries:
95 return None
95 return None
96 node, remote, name = self.potentialentries[potentialentry]
96 node, remote, name = self.potentialentries[potentialentry]
97 repo = self._repo
97 repo = self._repo
98 binnode = bin(node)
98 binnode = bin(node)
99 # if the node doesn't exist, skip it
99 # if the node doesn't exist, skip it
100 try:
100 try:
101 repo.changelog.rev(binnode)
101 repo.changelog.rev(binnode)
102 except LookupError:
102 except LookupError:
103 return None
103 return None
104 # Skip closed branches
104 # Skip closed branches
105 if (self._kind == 'branches' and repo[binnode].closesbranch()):
105 if (self._kind == 'branches' and repo[binnode].closesbranch()):
106 return None
106 return None
107 return [binnode]
107 return [binnode]
108
108
109 def __getitem__(self, key):
109 def __getitem__(self, key):
110 if not self.loaded:
110 if not self.loaded:
111 self._load()
111 self._load()
112 val = self._fetchandcache(key)
112 val = self._fetchandcache(key)
113 if val is not None:
113 if val is not None:
114 return val
114 return val
115 else:
115 else:
116 raise KeyError()
116 raise KeyError()
117
117
118 def __iter__(self):
118 def __iter__(self):
119 return iter(self.potentialentries)
119 return iter(self.potentialentries)
120
120
121 def __len__(self):
121 def __len__(self):
122 return len(self.potentialentries)
122 return len(self.potentialentries)
123
123
124 def __setitem__(self):
124 def __setitem__(self):
125 raise NotImplementedError
125 raise NotImplementedError
126
126
127 def __delitem__(self):
127 def __delitem__(self):
128 raise NotImplementedError
128 raise NotImplementedError
129
129
130 def _fetchandcache(self, key):
130 def _fetchandcache(self, key):
131 if key in self.cache:
131 if key in self.cache:
132 return self.cache[key]
132 return self.cache[key]
133 val = self._resolvedata(key)
133 val = self._resolvedata(key)
134 if val is not None:
134 if val is not None:
135 self.cache[key] = val
135 self.cache[key] = val
136 return val
136 return val
137 else:
137 else:
138 return None
138 return None
139
139
140 def keys(self):
140 def keys(self):
141 """ Get a list of bookmark or branch names """
141 """ Get a list of bookmark or branch names """
142 if not self.loaded:
142 if not self.loaded:
143 self._load()
143 self._load()
144 return self.potentialentries.keys()
144 return self.potentialentries.keys()
145
145
146 def iteritems(self):
146 def iteritems(self):
147 """ Iterate over (name, node) tuples """
147 """ Iterate over (name, node) tuples """
148
148
149 if not self.loaded:
149 if not self.loaded:
150 self._load()
150 self._load()
151
151
152 for k, vtup in self.potentialentries.iteritems():
152 for k, vtup in self.potentialentries.iteritems():
153 yield (k, [bin(vtup[0])])
153 yield (k, [bin(vtup[0])])
154
154
155 class remotenames(object):
155 class remotenames(object):
156 """
156 """
157 This class encapsulates all the remotenames state. It also contains
157 This class encapsulates all the remotenames state. It also contains
158 methods to access that state in convenient ways. Remotenames are lazy
158 methods to access that state in convenient ways. Remotenames are lazy
159 loaded. Whenever client code needs to ensure the freshest copy of
159 loaded. Whenever client code needs to ensure the freshest copy of
160 remotenames, use the `clearnames` method to force an eventual load.
160 remotenames, use the `clearnames` method to force an eventual load.
161 """
161 """
162
162
163 def __init__(self, repo, *args):
163 def __init__(self, repo, *args):
164 self._repo = repo
164 self._repo = repo
165 self.clearnames()
165 self.clearnames()
166
166
167 def clearnames(self):
167 def clearnames(self):
168 """ Clear all remote names state """
168 """ Clear all remote names state """
169 self.bookmarks = lazyremotenamedict("bookmarks", self._repo)
169 self.bookmarks = lazyremotenamedict("bookmarks", self._repo)
170 self.branches = lazyremotenamedict("branches", self._repo)
170 self.branches = lazyremotenamedict("branches", self._repo)
171 self._invalidatecache()
171 self._invalidatecache()
172
172
173 def _invalidatecache(self):
173 def _invalidatecache(self):
174 self._nodetobmarks = None
174 self._nodetobmarks = None
175 self._nodetobranch = None
175 self._nodetobranch = None
176
176
177 def bmarktonodes(self):
177 def bmarktonodes(self):
178 return self.bookmarks
178 return self.bookmarks
179
179
180 def nodetobmarks(self):
180 def nodetobmarks(self):
181 if not self._nodetobmarks:
181 if not self._nodetobmarks:
182 bmarktonodes = self.bmarktonodes()
182 bmarktonodes = self.bmarktonodes()
183 self._nodetobmarks = {}
183 self._nodetobmarks = {}
184 for name, node in bmarktonodes.iteritems():
184 for name, node in bmarktonodes.iteritems():
185 self._nodetobmarks.setdefault(node[0], []).append(name)
185 self._nodetobmarks.setdefault(node[0], []).append(name)
186 return self._nodetobmarks
186 return self._nodetobmarks
187
187
188 def branchtonodes(self):
188 def branchtonodes(self):
189 return self.branches
189 return self.branches
190
190
191 def nodetobranch(self):
191 def nodetobranch(self):
192 if not self._nodetobranch:
192 if not self._nodetobranch:
193 branchtonodes = self.branchtonodes()
193 branchtonodes = self.branchtonodes()
194 self._nodetobranch = {}
194 self._nodetobranch = {}
195 for name, nodes in branchtonodes.iteritems():
195 for name, nodes in branchtonodes.iteritems():
196 for node in nodes:
196 for node in nodes:
197 self._nodetobranch.setdefault(node, []).append(name)
197 self._nodetobranch.setdefault(node, []).append(name)
198 return self._nodetobranch
198 return self._nodetobranch
199
199
200 def reposetup(ui, repo):
200 def reposetup(ui, repo):
201 if not repo.local():
201 if not repo.local():
202 return
202 return
203
203
204 repo._remotenames = remotenames(repo)
204 repo._remotenames = remotenames(repo)
205 ns = namespaces.namespace
205 ns = namespaces.namespace
206
206
207 if ui.configbool('remotenames', 'bookmarks'):
207 if ui.configbool('remotenames', 'bookmarks'):
208 remotebookmarkns = ns(
208 remotebookmarkns = ns(
209 'remotebookmarks',
209 'remotebookmarks',
210 templatename='remotebookmarks',
210 templatename='remotebookmarks',
211 colorname='remotebookmark',
211 colorname='remotebookmark',
212 logfmt='remote bookmark: %s\n',
212 logfmt='remote bookmark: %s\n',
213 listnames=lambda repo: repo._remotenames.bmarktonodes().keys(),
213 listnames=lambda repo: repo._remotenames.bmarktonodes().keys(),
214 namemap=lambda repo, name:
214 namemap=lambda repo, name:
215 repo._remotenames.bmarktonodes().get(name, []),
215 repo._remotenames.bmarktonodes().get(name, []),
216 nodemap=lambda repo, node:
216 nodemap=lambda repo, node:
217 repo._remotenames.nodetobmarks().get(node, []))
217 repo._remotenames.nodetobmarks().get(node, []))
218 repo.names.addnamespace(remotebookmarkns)
218 repo.names.addnamespace(remotebookmarkns)
219
219
220 if ui.configbool('remotenames', 'branches'):
220 if ui.configbool('remotenames', 'branches'):
221 remotebranchns = ns(
221 remotebranchns = ns(
222 'remotebranches',
222 'remotebranches',
223 templatename='remotebranches',
223 templatename='remotebranches',
224 colorname='remotebranch',
224 colorname='remotebranch',
225 logfmt='remote branch: %s\n',
225 logfmt='remote branch: %s\n',
226 listnames = lambda repo: repo._remotenames.branchtonodes().keys(),
226 listnames = lambda repo: repo._remotenames.branchtonodes().keys(),
227 namemap = lambda repo, name:
227 namemap = lambda repo, name:
228 repo._remotenames.branchtonodes().get(name, []),
228 repo._remotenames.branchtonodes().get(name, []),
229 nodemap = lambda repo, node:
229 nodemap = lambda repo, node:
230 repo._remotenames.nodetobranch().get(node, []))
230 repo._remotenames.nodetobranch().get(node, []))
231 repo.names.addnamespace(remotebranchns)
231 repo.names.addnamespace(remotebranchns)
232
232
233 @templatekeyword('remotenames', requires={'repo', 'ctx', 'templ'})
233 @templatekeyword('remotenames', requires={'repo', 'ctx'})
234 def remotenameskw(context, mapping):
234 def remotenameskw(context, mapping):
235 """List of strings. Remote names associated with the changeset."""
235 """List of strings. Remote names associated with the changeset."""
236 repo = context.resource(mapping, 'repo')
236 repo = context.resource(mapping, 'repo')
237 ctx = context.resource(mapping, 'ctx')
237 ctx = context.resource(mapping, 'ctx')
238
238
239 remotenames = []
239 remotenames = []
240 if 'remotebookmarks' in repo.names:
240 if 'remotebookmarks' in repo.names:
241 remotenames = repo.names['remotebookmarks'].names(repo, ctx.node())
241 remotenames = repo.names['remotebookmarks'].names(repo, ctx.node())
242
242
243 if 'remotebranches' in repo.names:
243 if 'remotebranches' in repo.names:
244 remotenames += repo.names['remotebranches'].names(repo, ctx.node())
244 remotenames += repo.names['remotebranches'].names(repo, ctx.node())
245
245
246 return templateutil.compatlist(context, mapping, 'remotename', remotenames,
246 return templateutil.compatlist(context, mapping, 'remotename', remotenames,
247 plural='remotenames')
247 plural='remotenames')
248
248
249 @templatekeyword('remotebookmarks', requires={'repo', 'ctx', 'templ'})
249 @templatekeyword('remotebookmarks', requires={'repo', 'ctx'})
250 def remotebookmarkskw(context, mapping):
250 def remotebookmarkskw(context, mapping):
251 """List of strings. Remote bookmarks associated with the changeset."""
251 """List of strings. Remote bookmarks associated with the changeset."""
252 repo = context.resource(mapping, 'repo')
252 repo = context.resource(mapping, 'repo')
253 ctx = context.resource(mapping, 'ctx')
253 ctx = context.resource(mapping, 'ctx')
254
254
255 remotebmarks = []
255 remotebmarks = []
256 if 'remotebookmarks' in repo.names:
256 if 'remotebookmarks' in repo.names:
257 remotebmarks = repo.names['remotebookmarks'].names(repo, ctx.node())
257 remotebmarks = repo.names['remotebookmarks'].names(repo, ctx.node())
258
258
259 return templateutil.compatlist(context, mapping, 'remotebookmark',
259 return templateutil.compatlist(context, mapping, 'remotebookmark',
260 remotebmarks, plural='remotebookmarks')
260 remotebmarks, plural='remotebookmarks')
261
261
262 @templatekeyword('remotebranches', requires={'repo', 'ctx', 'templ'})
262 @templatekeyword('remotebranches', requires={'repo', 'ctx'})
263 def remotebrancheskw(context, mapping):
263 def remotebrancheskw(context, mapping):
264 """List of strings. Remote branches associated with the changeset."""
264 """List of strings. Remote branches associated with the changeset."""
265 repo = context.resource(mapping, 'repo')
265 repo = context.resource(mapping, 'repo')
266 ctx = context.resource(mapping, 'ctx')
266 ctx = context.resource(mapping, 'ctx')
267
267
268 remotebranches = []
268 remotebranches = []
269 if 'remotebranches' in repo.names:
269 if 'remotebranches' in repo.names:
270 remotebranches = repo.names['remotebranches'].names(repo, ctx.node())
270 remotebranches = repo.names['remotebranches'].names(repo, ctx.node())
271
271
272 return templateutil.compatlist(context, mapping, 'remotebranch',
272 return templateutil.compatlist(context, mapping, 'remotebranch',
273 remotebranches, plural='remotebranches')
273 remotebranches, plural='remotebranches')
274
274
275 def _revsetutil(repo, subset, x, rtypes):
275 def _revsetutil(repo, subset, x, rtypes):
276 """utility function to return a set of revs based on the rtypes"""
276 """utility function to return a set of revs based on the rtypes"""
277
277
278 revs = set()
278 revs = set()
279 cl = repo.changelog
279 cl = repo.changelog
280 for rtype in rtypes:
280 for rtype in rtypes:
281 if rtype in repo.names:
281 if rtype in repo.names:
282 ns = repo.names[rtype]
282 ns = repo.names[rtype]
283 for name in ns.listnames(repo):
283 for name in ns.listnames(repo):
284 revs.update(ns.nodes(repo, name))
284 revs.update(ns.nodes(repo, name))
285
285
286 results = (cl.rev(n) for n in revs if cl.hasnode(n))
286 results = (cl.rev(n) for n in revs if cl.hasnode(n))
287 return subset & smartset.baseset(sorted(results))
287 return subset & smartset.baseset(sorted(results))
288
288
289 @revsetpredicate('remotenames()')
289 @revsetpredicate('remotenames()')
290 def remotenamesrevset(repo, subset, x):
290 def remotenamesrevset(repo, subset, x):
291 """All changesets which have a remotename on them."""
291 """All changesets which have a remotename on them."""
292 revsetlang.getargs(x, 0, 0, _("remotenames takes no arguments"))
292 revsetlang.getargs(x, 0, 0, _("remotenames takes no arguments"))
293 return _revsetutil(repo, subset, x, ('remotebookmarks', 'remotebranches'))
293 return _revsetutil(repo, subset, x, ('remotebookmarks', 'remotebranches'))
294
294
295 @revsetpredicate('remotebranches()')
295 @revsetpredicate('remotebranches()')
296 def remotebranchesrevset(repo, subset, x):
296 def remotebranchesrevset(repo, subset, x):
297 """All changesets which are branch heads on remotes."""
297 """All changesets which are branch heads on remotes."""
298 revsetlang.getargs(x, 0, 0, _("remotebranches takes no arguments"))
298 revsetlang.getargs(x, 0, 0, _("remotebranches takes no arguments"))
299 return _revsetutil(repo, subset, x, ('remotebranches',))
299 return _revsetutil(repo, subset, x, ('remotebranches',))
300
300
301 @revsetpredicate('remotebookmarks()')
301 @revsetpredicate('remotebookmarks()')
302 def remotebmarksrevset(repo, subset, x):
302 def remotebmarksrevset(repo, subset, x):
303 """All changesets which have bookmarks on remotes."""
303 """All changesets which have bookmarks on remotes."""
304 revsetlang.getargs(x, 0, 0, _("remotebookmarks takes no arguments"))
304 revsetlang.getargs(x, 0, 0, _("remotebookmarks takes no arguments"))
305 return _revsetutil(repo, subset, x, ('remotebookmarks',))
305 return _revsetutil(repo, subset, x, ('remotebookmarks',))
@@ -1,201 +1,201
1 from __future__ import absolute_import
1 from __future__ import absolute_import
2
2
3 from .i18n import _
3 from .i18n import _
4 from . import (
4 from . import (
5 registrar,
5 registrar,
6 templatekw,
6 templatekw,
7 util,
7 util,
8 )
8 )
9
9
10 def tolist(val):
10 def tolist(val):
11 """
11 """
12 a convenience method to return an empty list instead of None
12 a convenience method to return an empty list instead of None
13 """
13 """
14 if val is None:
14 if val is None:
15 return []
15 return []
16 else:
16 else:
17 return [val]
17 return [val]
18
18
19 class namespaces(object):
19 class namespaces(object):
20 """provides an interface to register and operate on multiple namespaces. See
20 """provides an interface to register and operate on multiple namespaces. See
21 the namespace class below for details on the namespace object.
21 the namespace class below for details on the namespace object.
22
22
23 """
23 """
24
24
25 _names_version = 0
25 _names_version = 0
26
26
27 def __init__(self):
27 def __init__(self):
28 self._names = util.sortdict()
28 self._names = util.sortdict()
29 columns = templatekw.getlogcolumns()
29 columns = templatekw.getlogcolumns()
30
30
31 # we need current mercurial named objects (bookmarks, tags, and
31 # we need current mercurial named objects (bookmarks, tags, and
32 # branches) to be initialized somewhere, so that place is here
32 # branches) to be initialized somewhere, so that place is here
33 bmknames = lambda repo: repo._bookmarks.keys()
33 bmknames = lambda repo: repo._bookmarks.keys()
34 bmknamemap = lambda repo, name: tolist(repo._bookmarks.get(name))
34 bmknamemap = lambda repo, name: tolist(repo._bookmarks.get(name))
35 bmknodemap = lambda repo, node: repo.nodebookmarks(node)
35 bmknodemap = lambda repo, node: repo.nodebookmarks(node)
36 n = namespace("bookmarks", templatename="bookmark",
36 n = namespace("bookmarks", templatename="bookmark",
37 logfmt=columns['bookmark'],
37 logfmt=columns['bookmark'],
38 listnames=bmknames,
38 listnames=bmknames,
39 namemap=bmknamemap, nodemap=bmknodemap,
39 namemap=bmknamemap, nodemap=bmknodemap,
40 builtin=True)
40 builtin=True)
41 self.addnamespace(n)
41 self.addnamespace(n)
42
42
43 tagnames = lambda repo: [t for t, n in repo.tagslist()]
43 tagnames = lambda repo: [t for t, n in repo.tagslist()]
44 tagnamemap = lambda repo, name: tolist(repo._tagscache.tags.get(name))
44 tagnamemap = lambda repo, name: tolist(repo._tagscache.tags.get(name))
45 tagnodemap = lambda repo, node: repo.nodetags(node)
45 tagnodemap = lambda repo, node: repo.nodetags(node)
46 n = namespace("tags", templatename="tag",
46 n = namespace("tags", templatename="tag",
47 logfmt=columns['tag'],
47 logfmt=columns['tag'],
48 listnames=tagnames,
48 listnames=tagnames,
49 namemap=tagnamemap, nodemap=tagnodemap,
49 namemap=tagnamemap, nodemap=tagnodemap,
50 deprecated={'tip'},
50 deprecated={'tip'},
51 builtin=True)
51 builtin=True)
52 self.addnamespace(n)
52 self.addnamespace(n)
53
53
54 bnames = lambda repo: repo.branchmap().keys()
54 bnames = lambda repo: repo.branchmap().keys()
55 bnamemap = lambda repo, name: tolist(repo.branchtip(name, True))
55 bnamemap = lambda repo, name: tolist(repo.branchtip(name, True))
56 bnodemap = lambda repo, node: [repo[node].branch()]
56 bnodemap = lambda repo, node: [repo[node].branch()]
57 n = namespace("branches", templatename="branch",
57 n = namespace("branches", templatename="branch",
58 logfmt=columns['branch'],
58 logfmt=columns['branch'],
59 listnames=bnames,
59 listnames=bnames,
60 namemap=bnamemap, nodemap=bnodemap,
60 namemap=bnamemap, nodemap=bnodemap,
61 builtin=True)
61 builtin=True)
62 self.addnamespace(n)
62 self.addnamespace(n)
63
63
64 def __getitem__(self, namespace):
64 def __getitem__(self, namespace):
65 """returns the namespace object"""
65 """returns the namespace object"""
66 return self._names[namespace]
66 return self._names[namespace]
67
67
68 def __iter__(self):
68 def __iter__(self):
69 return self._names.__iter__()
69 return self._names.__iter__()
70
70
71 def items(self):
71 def items(self):
72 return self._names.iteritems()
72 return self._names.iteritems()
73
73
74 iteritems = items
74 iteritems = items
75
75
76 def addnamespace(self, namespace, order=None):
76 def addnamespace(self, namespace, order=None):
77 """register a namespace
77 """register a namespace
78
78
79 namespace: the name to be registered (in plural form)
79 namespace: the name to be registered (in plural form)
80 order: optional argument to specify the order of namespaces
80 order: optional argument to specify the order of namespaces
81 (e.g. 'branches' should be listed before 'bookmarks')
81 (e.g. 'branches' should be listed before 'bookmarks')
82
82
83 """
83 """
84 if order is not None:
84 if order is not None:
85 self._names.insert(order, namespace.name, namespace)
85 self._names.insert(order, namespace.name, namespace)
86 else:
86 else:
87 self._names[namespace.name] = namespace
87 self._names[namespace.name] = namespace
88
88
89 # we only generate a template keyword if one does not already exist
89 # we only generate a template keyword if one does not already exist
90 if namespace.name not in templatekw.keywords:
90 if namespace.name not in templatekw.keywords:
91 templatekeyword = registrar.templatekeyword(templatekw.keywords)
91 templatekeyword = registrar.templatekeyword(templatekw.keywords)
92 @templatekeyword(namespace.name, requires={'repo', 'ctx', 'templ'})
92 @templatekeyword(namespace.name, requires={'repo', 'ctx'})
93 def generatekw(context, mapping):
93 def generatekw(context, mapping):
94 return templatekw.shownames(context, mapping, namespace.name)
94 return templatekw.shownames(context, mapping, namespace.name)
95
95
96 def singlenode(self, repo, name):
96 def singlenode(self, repo, name):
97 """
97 """
98 Return the 'best' node for the given name. Best means the first node
98 Return the 'best' node for the given name. Best means the first node
99 in the first nonempty list returned by a name-to-nodes mapping function
99 in the first nonempty list returned by a name-to-nodes mapping function
100 in the defined precedence order.
100 in the defined precedence order.
101
101
102 Raises a KeyError if there is no such node.
102 Raises a KeyError if there is no such node.
103 """
103 """
104 for ns, v in self._names.iteritems():
104 for ns, v in self._names.iteritems():
105 n = v.namemap(repo, name)
105 n = v.namemap(repo, name)
106 if n:
106 if n:
107 # return max revision number
107 # return max revision number
108 if len(n) > 1:
108 if len(n) > 1:
109 cl = repo.changelog
109 cl = repo.changelog
110 maxrev = max(cl.rev(node) for node in n)
110 maxrev = max(cl.rev(node) for node in n)
111 return cl.node(maxrev)
111 return cl.node(maxrev)
112 return n[0]
112 return n[0]
113 raise KeyError(_('no such name: %s') % name)
113 raise KeyError(_('no such name: %s') % name)
114
114
115 class namespace(object):
115 class namespace(object):
116 """provides an interface to a namespace
116 """provides an interface to a namespace
117
117
118 Namespaces are basically generic many-to-many mapping between some
118 Namespaces are basically generic many-to-many mapping between some
119 (namespaced) names and nodes. The goal here is to control the pollution of
119 (namespaced) names and nodes. The goal here is to control the pollution of
120 jamming things into tags or bookmarks (in extension-land) and to simplify
120 jamming things into tags or bookmarks (in extension-land) and to simplify
121 internal bits of mercurial: log output, tab completion, etc.
121 internal bits of mercurial: log output, tab completion, etc.
122
122
123 More precisely, we define a mapping of names to nodes, and a mapping from
123 More precisely, we define a mapping of names to nodes, and a mapping from
124 nodes to names. Each mapping returns a list.
124 nodes to names. Each mapping returns a list.
125
125
126 Furthermore, each name mapping will be passed a name to lookup which might
126 Furthermore, each name mapping will be passed a name to lookup which might
127 not be in its domain. In this case, each method should return an empty list
127 not be in its domain. In this case, each method should return an empty list
128 and not raise an error.
128 and not raise an error.
129
129
130 This namespace object will define the properties we need:
130 This namespace object will define the properties we need:
131 'name': the namespace (plural form)
131 'name': the namespace (plural form)
132 'templatename': name to use for templating (usually the singular form
132 'templatename': name to use for templating (usually the singular form
133 of the plural namespace name)
133 of the plural namespace name)
134 'listnames': list of all names in the namespace (usually the keys of a
134 'listnames': list of all names in the namespace (usually the keys of a
135 dictionary)
135 dictionary)
136 'namemap': function that takes a name and returns a list of nodes
136 'namemap': function that takes a name and returns a list of nodes
137 'nodemap': function that takes a node and returns a list of names
137 'nodemap': function that takes a node and returns a list of names
138 'deprecated': set of names to be masked for ordinary use
138 'deprecated': set of names to be masked for ordinary use
139 'builtin': bool indicating if this namespace is supported by core
139 'builtin': bool indicating if this namespace is supported by core
140 Mercurial.
140 Mercurial.
141 """
141 """
142
142
143 def __init__(self, name, templatename=None, logname=None, colorname=None,
143 def __init__(self, name, templatename=None, logname=None, colorname=None,
144 logfmt=None, listnames=None, namemap=None, nodemap=None,
144 logfmt=None, listnames=None, namemap=None, nodemap=None,
145 deprecated=None, builtin=False):
145 deprecated=None, builtin=False):
146 """create a namespace
146 """create a namespace
147
147
148 name: the namespace to be registered (in plural form)
148 name: the namespace to be registered (in plural form)
149 templatename: the name to use for templating
149 templatename: the name to use for templating
150 logname: the name to use for log output; if not specified templatename
150 logname: the name to use for log output; if not specified templatename
151 is used
151 is used
152 colorname: the name to use for colored log output; if not specified
152 colorname: the name to use for colored log output; if not specified
153 logname is used
153 logname is used
154 logfmt: the format to use for (i18n-ed) log output; if not specified
154 logfmt: the format to use for (i18n-ed) log output; if not specified
155 it is composed from logname
155 it is composed from logname
156 listnames: function to list all names
156 listnames: function to list all names
157 namemap: function that inputs a name, output node(s)
157 namemap: function that inputs a name, output node(s)
158 nodemap: function that inputs a node, output name(s)
158 nodemap: function that inputs a node, output name(s)
159 deprecated: set of names to be masked for ordinary use
159 deprecated: set of names to be masked for ordinary use
160 builtin: whether namespace is implemented by core Mercurial
160 builtin: whether namespace is implemented by core Mercurial
161 """
161 """
162 self.name = name
162 self.name = name
163 self.templatename = templatename
163 self.templatename = templatename
164 self.logname = logname
164 self.logname = logname
165 self.colorname = colorname
165 self.colorname = colorname
166 self.logfmt = logfmt
166 self.logfmt = logfmt
167 self.listnames = listnames
167 self.listnames = listnames
168 self.namemap = namemap
168 self.namemap = namemap
169 self.nodemap = nodemap
169 self.nodemap = nodemap
170
170
171 # if logname is not specified, use the template name as backup
171 # if logname is not specified, use the template name as backup
172 if self.logname is None:
172 if self.logname is None:
173 self.logname = self.templatename
173 self.logname = self.templatename
174
174
175 # if colorname is not specified, just use the logname as a backup
175 # if colorname is not specified, just use the logname as a backup
176 if self.colorname is None:
176 if self.colorname is None:
177 self.colorname = self.logname
177 self.colorname = self.logname
178
178
179 # if logfmt is not specified, compose it from logname as backup
179 # if logfmt is not specified, compose it from logname as backup
180 if self.logfmt is None:
180 if self.logfmt is None:
181 # i18n: column positioning for "hg log"
181 # i18n: column positioning for "hg log"
182 self.logfmt = ("%s:" % self.logname).ljust(13) + "%s\n"
182 self.logfmt = ("%s:" % self.logname).ljust(13) + "%s\n"
183
183
184 if deprecated is None:
184 if deprecated is None:
185 self.deprecated = set()
185 self.deprecated = set()
186 else:
186 else:
187 self.deprecated = deprecated
187 self.deprecated = deprecated
188
188
189 self.builtin = builtin
189 self.builtin = builtin
190
190
191 def names(self, repo, node):
191 def names(self, repo, node):
192 """method that returns a (sorted) list of names in a namespace that
192 """method that returns a (sorted) list of names in a namespace that
193 match a given node"""
193 match a given node"""
194 return sorted(self.nodemap(repo, node))
194 return sorted(self.nodemap(repo, node))
195
195
196 def nodes(self, repo, name):
196 def nodes(self, repo, name):
197 """method that returns a list of nodes in a namespace that
197 """method that returns a list of nodes in a namespace that
198 match a given name.
198 match a given name.
199
199
200 """
200 """
201 return sorted(self.namemap(repo, name))
201 return sorted(self.namemap(repo, name))
@@ -1,802 +1,816
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 templateutil,
27 util,
27 util,
28 )
28 )
29
29
30 _hybrid = templateutil.hybrid
30 _hybrid = templateutil.hybrid
31 _mappable = templateutil.mappable
31 _mappable = templateutil.mappable
32 _showlist = templateutil._showlist
33 hybriddict = templateutil.hybriddict
32 hybriddict = templateutil.hybriddict
34 hybridlist = templateutil.hybridlist
33 hybridlist = templateutil.hybridlist
35 compatdict = templateutil.compatdict
34 compatdict = templateutil.compatdict
36 compatlist = templateutil.compatlist
35 compatlist = templateutil.compatlist
36 _showcompatlist = templateutil._showcompatlist
37
38 # TODO: temporary hack for porting; will be removed soon
39 class _fakecontextwrapper(object):
40 def __init__(self, templ):
41 self._templ = templ
42
43 def preload(self, t):
44 return t in self._templ
45
46 def process(self, t, mapping):
47 return self._templ.generatenamed(t, mapping)
48
49 def _showlist(name, values, templ, mapping, plural=None, separator=' '):
50 context = _fakecontextwrapper(templ)
51 return _showcompatlist(context, mapping, name, values, plural, separator)
37
52
38 def showdict(name, data, mapping, plural=None, key='key', value='value',
53 def showdict(name, data, mapping, plural=None, key='key', value='value',
39 fmt=None, separator=' '):
54 fmt=None, separator=' '):
40 ui = mapping.get('ui')
55 ui = mapping.get('ui')
41 if ui:
56 if ui:
42 ui.deprecwarn("templatekw.showdict() is deprecated, use "
57 ui.deprecwarn("templatekw.showdict() is deprecated, use "
43 "templateutil.compatdict()", '4.6')
58 "templateutil.compatdict()", '4.6')
44 c = [{key: k, value: v} for k, v in data.iteritems()]
59 c = [{key: k, value: v} for k, v in data.iteritems()]
45 f = _showlist(name, c, mapping['templ'], mapping, plural, separator)
60 f = _showlist(name, c, mapping['templ'], mapping, plural, separator)
46 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
61 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
47
62
48 def showlist(name, values, mapping, plural=None, element=None, separator=' '):
63 def showlist(name, values, mapping, plural=None, element=None, separator=' '):
49 ui = mapping.get('ui')
64 ui = mapping.get('ui')
50 if ui:
65 if ui:
51 ui.deprecwarn("templatekw.showlist() is deprecated, use "
66 ui.deprecwarn("templatekw.showlist() is deprecated, use "
52 "templateutil.compatlist()", '4.6')
67 "templateutil.compatlist()", '4.6')
53 if not element:
68 if not element:
54 element = name
69 element = name
55 f = _showlist(name, values, mapping['templ'], mapping, plural, separator)
70 f = _showlist(name, values, mapping['templ'], mapping, plural, separator)
56 return hybridlist(values, name=element, gen=f)
71 return hybridlist(values, name=element, gen=f)
57
72
58 def getlatesttags(context, mapping, pattern=None):
73 def getlatesttags(context, mapping, pattern=None):
59 '''return date, distance and name for the latest tag of rev'''
74 '''return date, distance and name for the latest tag of rev'''
60 repo = context.resource(mapping, 'repo')
75 repo = context.resource(mapping, 'repo')
61 ctx = context.resource(mapping, 'ctx')
76 ctx = context.resource(mapping, 'ctx')
62 cache = context.resource(mapping, 'cache')
77 cache = context.resource(mapping, 'cache')
63
78
64 cachename = 'latesttags'
79 cachename = 'latesttags'
65 if pattern is not None:
80 if pattern is not None:
66 cachename += '-' + pattern
81 cachename += '-' + pattern
67 match = util.stringmatcher(pattern)[2]
82 match = util.stringmatcher(pattern)[2]
68 else:
83 else:
69 match = util.always
84 match = util.always
70
85
71 if cachename not in cache:
86 if cachename not in cache:
72 # Cache mapping from rev to a tuple with tag date, tag
87 # Cache mapping from rev to a tuple with tag date, tag
73 # distance and tag name
88 # distance and tag name
74 cache[cachename] = {-1: (0, 0, ['null'])}
89 cache[cachename] = {-1: (0, 0, ['null'])}
75 latesttags = cache[cachename]
90 latesttags = cache[cachename]
76
91
77 rev = ctx.rev()
92 rev = ctx.rev()
78 todo = [rev]
93 todo = [rev]
79 while todo:
94 while todo:
80 rev = todo.pop()
95 rev = todo.pop()
81 if rev in latesttags:
96 if rev in latesttags:
82 continue
97 continue
83 ctx = repo[rev]
98 ctx = repo[rev]
84 tags = [t for t in ctx.tags()
99 tags = [t for t in ctx.tags()
85 if (repo.tagtype(t) and repo.tagtype(t) != 'local'
100 if (repo.tagtype(t) and repo.tagtype(t) != 'local'
86 and match(t))]
101 and match(t))]
87 if tags:
102 if tags:
88 latesttags[rev] = ctx.date()[0], 0, [t for t in sorted(tags)]
103 latesttags[rev] = ctx.date()[0], 0, [t for t in sorted(tags)]
89 continue
104 continue
90 try:
105 try:
91 ptags = [latesttags[p.rev()] for p in ctx.parents()]
106 ptags = [latesttags[p.rev()] for p in ctx.parents()]
92 if len(ptags) > 1:
107 if len(ptags) > 1:
93 if ptags[0][2] == ptags[1][2]:
108 if ptags[0][2] == ptags[1][2]:
94 # The tuples are laid out so the right one can be found by
109 # The tuples are laid out so the right one can be found by
95 # comparison in this case.
110 # comparison in this case.
96 pdate, pdist, ptag = max(ptags)
111 pdate, pdist, ptag = max(ptags)
97 else:
112 else:
98 def key(x):
113 def key(x):
99 changessincetag = len(repo.revs('only(%d, %s)',
114 changessincetag = len(repo.revs('only(%d, %s)',
100 ctx.rev(), x[2][0]))
115 ctx.rev(), x[2][0]))
101 # Smallest number of changes since tag wins. Date is
116 # Smallest number of changes since tag wins. Date is
102 # used as tiebreaker.
117 # used as tiebreaker.
103 return [-changessincetag, x[0]]
118 return [-changessincetag, x[0]]
104 pdate, pdist, ptag = max(ptags, key=key)
119 pdate, pdist, ptag = max(ptags, key=key)
105 else:
120 else:
106 pdate, pdist, ptag = ptags[0]
121 pdate, pdist, ptag = ptags[0]
107 except KeyError:
122 except KeyError:
108 # Cache miss - recurse
123 # Cache miss - recurse
109 todo.append(rev)
124 todo.append(rev)
110 todo.extend(p.rev() for p in ctx.parents())
125 todo.extend(p.rev() for p in ctx.parents())
111 continue
126 continue
112 latesttags[rev] = pdate, pdist + 1, ptag
127 latesttags[rev] = pdate, pdist + 1, ptag
113 return latesttags[rev]
128 return latesttags[rev]
114
129
115 def getrenamedfn(repo, endrev=None):
130 def getrenamedfn(repo, endrev=None):
116 rcache = {}
131 rcache = {}
117 if endrev is None:
132 if endrev is None:
118 endrev = len(repo)
133 endrev = len(repo)
119
134
120 def getrenamed(fn, rev):
135 def getrenamed(fn, rev):
121 '''looks up all renames for a file (up to endrev) the first
136 '''looks up all renames for a file (up to endrev) the first
122 time the file is given. It indexes on the changerev and only
137 time the file is given. It indexes on the changerev and only
123 parses the manifest if linkrev != changerev.
138 parses the manifest if linkrev != changerev.
124 Returns rename info for fn at changerev rev.'''
139 Returns rename info for fn at changerev rev.'''
125 if fn not in rcache:
140 if fn not in rcache:
126 rcache[fn] = {}
141 rcache[fn] = {}
127 fl = repo.file(fn)
142 fl = repo.file(fn)
128 for i in fl:
143 for i in fl:
129 lr = fl.linkrev(i)
144 lr = fl.linkrev(i)
130 renamed = fl.renamed(fl.node(i))
145 renamed = fl.renamed(fl.node(i))
131 rcache[fn][lr] = renamed
146 rcache[fn][lr] = renamed
132 if lr >= endrev:
147 if lr >= endrev:
133 break
148 break
134 if rev in rcache[fn]:
149 if rev in rcache[fn]:
135 return rcache[fn][rev]
150 return rcache[fn][rev]
136
151
137 # If linkrev != rev (i.e. rev not found in rcache) fallback to
152 # If linkrev != rev (i.e. rev not found in rcache) fallback to
138 # filectx logic.
153 # filectx logic.
139 try:
154 try:
140 return repo[rev][fn].renamed()
155 return repo[rev][fn].renamed()
141 except error.LookupError:
156 except error.LookupError:
142 return None
157 return None
143
158
144 return getrenamed
159 return getrenamed
145
160
146 def getlogcolumns():
161 def getlogcolumns():
147 """Return a dict of log column labels"""
162 """Return a dict of log column labels"""
148 _ = pycompat.identity # temporarily disable gettext
163 _ = pycompat.identity # temporarily disable gettext
149 # i18n: column positioning for "hg log"
164 # i18n: column positioning for "hg log"
150 columns = _('bookmark: %s\n'
165 columns = _('bookmark: %s\n'
151 'branch: %s\n'
166 'branch: %s\n'
152 'changeset: %s\n'
167 'changeset: %s\n'
153 'copies: %s\n'
168 'copies: %s\n'
154 'date: %s\n'
169 'date: %s\n'
155 'extra: %s=%s\n'
170 'extra: %s=%s\n'
156 'files+: %s\n'
171 'files+: %s\n'
157 'files-: %s\n'
172 'files-: %s\n'
158 'files: %s\n'
173 'files: %s\n'
159 'instability: %s\n'
174 'instability: %s\n'
160 'manifest: %s\n'
175 'manifest: %s\n'
161 'obsolete: %s\n'
176 'obsolete: %s\n'
162 'parent: %s\n'
177 'parent: %s\n'
163 'phase: %s\n'
178 'phase: %s\n'
164 'summary: %s\n'
179 'summary: %s\n'
165 'tag: %s\n'
180 'tag: %s\n'
166 'user: %s\n')
181 'user: %s\n')
167 return dict(zip([s.split(':', 1)[0] for s in columns.splitlines()],
182 return dict(zip([s.split(':', 1)[0] for s in columns.splitlines()],
168 i18n._(columns).splitlines(True)))
183 i18n._(columns).splitlines(True)))
169
184
170 # default templates internally used for rendering of lists
185 # default templates internally used for rendering of lists
171 defaulttempl = {
186 defaulttempl = {
172 'parent': '{rev}:{node|formatnode} ',
187 'parent': '{rev}:{node|formatnode} ',
173 'manifest': '{rev}:{node|formatnode}',
188 'manifest': '{rev}:{node|formatnode}',
174 'file_copy': '{name} ({source})',
189 'file_copy': '{name} ({source})',
175 'envvar': '{key}={value}',
190 'envvar': '{key}={value}',
176 'extra': '{key}={value|stringescape}'
191 'extra': '{key}={value|stringescape}'
177 }
192 }
178 # filecopy is preserved for compatibility reasons
193 # filecopy is preserved for compatibility reasons
179 defaulttempl['filecopy'] = defaulttempl['file_copy']
194 defaulttempl['filecopy'] = defaulttempl['file_copy']
180
195
181 # keywords are callables (see registrar.templatekeyword for details)
196 # keywords are callables (see registrar.templatekeyword for details)
182 keywords = {}
197 keywords = {}
183 templatekeyword = registrar.templatekeyword(keywords)
198 templatekeyword = registrar.templatekeyword(keywords)
184
199
185 @templatekeyword('author', requires={'ctx'})
200 @templatekeyword('author', requires={'ctx'})
186 def showauthor(context, mapping):
201 def showauthor(context, mapping):
187 """String. The unmodified author of the changeset."""
202 """String. The unmodified author of the changeset."""
188 ctx = context.resource(mapping, 'ctx')
203 ctx = context.resource(mapping, 'ctx')
189 return ctx.user()
204 return ctx.user()
190
205
191 @templatekeyword('bisect', requires={'repo', 'ctx'})
206 @templatekeyword('bisect', requires={'repo', 'ctx'})
192 def showbisect(context, mapping):
207 def showbisect(context, mapping):
193 """String. The changeset bisection status."""
208 """String. The changeset bisection status."""
194 repo = context.resource(mapping, 'repo')
209 repo = context.resource(mapping, 'repo')
195 ctx = context.resource(mapping, 'ctx')
210 ctx = context.resource(mapping, 'ctx')
196 return hbisect.label(repo, ctx.node())
211 return hbisect.label(repo, ctx.node())
197
212
198 @templatekeyword('branch', requires={'ctx'})
213 @templatekeyword('branch', requires={'ctx'})
199 def showbranch(context, mapping):
214 def showbranch(context, mapping):
200 """String. The name of the branch on which the changeset was
215 """String. The name of the branch on which the changeset was
201 committed.
216 committed.
202 """
217 """
203 ctx = context.resource(mapping, 'ctx')
218 ctx = context.resource(mapping, 'ctx')
204 return ctx.branch()
219 return ctx.branch()
205
220
206 @templatekeyword('branches', requires={'ctx', 'templ'})
221 @templatekeyword('branches', requires={'ctx'})
207 def showbranches(context, mapping):
222 def showbranches(context, mapping):
208 """List of strings. The name of the branch on which the
223 """List of strings. The name of the branch on which the
209 changeset was committed. Will be empty if the branch name was
224 changeset was committed. Will be empty if the branch name was
210 default. (DEPRECATED)
225 default. (DEPRECATED)
211 """
226 """
212 ctx = context.resource(mapping, 'ctx')
227 ctx = context.resource(mapping, 'ctx')
213 branch = ctx.branch()
228 branch = ctx.branch()
214 if branch != 'default':
229 if branch != 'default':
215 return compatlist(context, mapping, 'branch', [branch],
230 return compatlist(context, mapping, 'branch', [branch],
216 plural='branches')
231 plural='branches')
217 return compatlist(context, mapping, 'branch', [], plural='branches')
232 return compatlist(context, mapping, 'branch', [], plural='branches')
218
233
219 @templatekeyword('bookmarks', requires={'repo', 'ctx', 'templ'})
234 @templatekeyword('bookmarks', requires={'repo', 'ctx', 'templ'})
220 def showbookmarks(context, mapping):
235 def showbookmarks(context, mapping):
221 """List of strings. Any bookmarks associated with the
236 """List of strings. Any bookmarks associated with the
222 changeset. Also sets 'active', the name of the active bookmark.
237 changeset. Also sets 'active', the name of the active bookmark.
223 """
238 """
224 repo = context.resource(mapping, 'repo')
239 repo = context.resource(mapping, 'repo')
225 ctx = context.resource(mapping, 'ctx')
240 ctx = context.resource(mapping, 'ctx')
226 templ = context.resource(mapping, 'templ')
241 templ = context.resource(mapping, 'templ')
227 bookmarks = ctx.bookmarks()
242 bookmarks = ctx.bookmarks()
228 active = repo._activebookmark
243 active = repo._activebookmark
229 makemap = lambda v: {'bookmark': v, 'active': active, 'current': active}
244 makemap = lambda v: {'bookmark': v, 'active': active, 'current': active}
230 f = _showlist('bookmark', bookmarks, templ, mapping)
245 f = _showlist('bookmark', bookmarks, templ, mapping)
231 return _hybrid(f, bookmarks, makemap, pycompat.identity)
246 return _hybrid(f, bookmarks, makemap, pycompat.identity)
232
247
233 @templatekeyword('children', requires={'ctx', 'templ'})
248 @templatekeyword('children', requires={'ctx'})
234 def showchildren(context, mapping):
249 def showchildren(context, mapping):
235 """List of strings. The children of the changeset."""
250 """List of strings. The children of the changeset."""
236 ctx = context.resource(mapping, 'ctx')
251 ctx = context.resource(mapping, 'ctx')
237 childrevs = ['%d:%s' % (cctx.rev(), cctx) for cctx in ctx.children()]
252 childrevs = ['%d:%s' % (cctx.rev(), cctx) for cctx in ctx.children()]
238 return compatlist(context, mapping, 'children', childrevs, element='child')
253 return compatlist(context, mapping, 'children', childrevs, element='child')
239
254
240 # Deprecated, but kept alive for help generation a purpose.
255 # Deprecated, but kept alive for help generation a purpose.
241 @templatekeyword('currentbookmark', requires={'repo', 'ctx'})
256 @templatekeyword('currentbookmark', requires={'repo', 'ctx'})
242 def showcurrentbookmark(context, mapping):
257 def showcurrentbookmark(context, mapping):
243 """String. The active bookmark, if it is associated with the changeset.
258 """String. The active bookmark, if it is associated with the changeset.
244 (DEPRECATED)"""
259 (DEPRECATED)"""
245 return showactivebookmark(context, mapping)
260 return showactivebookmark(context, mapping)
246
261
247 @templatekeyword('activebookmark', requires={'repo', 'ctx'})
262 @templatekeyword('activebookmark', requires={'repo', 'ctx'})
248 def showactivebookmark(context, mapping):
263 def showactivebookmark(context, mapping):
249 """String. The active bookmark, if it is associated with the changeset."""
264 """String. The active bookmark, if it is associated with the changeset."""
250 repo = context.resource(mapping, 'repo')
265 repo = context.resource(mapping, 'repo')
251 ctx = context.resource(mapping, 'ctx')
266 ctx = context.resource(mapping, 'ctx')
252 active = repo._activebookmark
267 active = repo._activebookmark
253 if active and active in ctx.bookmarks():
268 if active and active in ctx.bookmarks():
254 return active
269 return active
255 return ''
270 return ''
256
271
257 @templatekeyword('date', requires={'ctx'})
272 @templatekeyword('date', requires={'ctx'})
258 def showdate(context, mapping):
273 def showdate(context, mapping):
259 """Date information. The date when the changeset was committed."""
274 """Date information. The date when the changeset was committed."""
260 ctx = context.resource(mapping, 'ctx')
275 ctx = context.resource(mapping, 'ctx')
261 return ctx.date()
276 return ctx.date()
262
277
263 @templatekeyword('desc', requires={'ctx'})
278 @templatekeyword('desc', requires={'ctx'})
264 def showdescription(context, mapping):
279 def showdescription(context, mapping):
265 """String. The text of the changeset description."""
280 """String. The text of the changeset description."""
266 ctx = context.resource(mapping, 'ctx')
281 ctx = context.resource(mapping, 'ctx')
267 s = ctx.description()
282 s = ctx.description()
268 if isinstance(s, encoding.localstr):
283 if isinstance(s, encoding.localstr):
269 # try hard to preserve utf-8 bytes
284 # try hard to preserve utf-8 bytes
270 return encoding.tolocal(encoding.fromlocal(s).strip())
285 return encoding.tolocal(encoding.fromlocal(s).strip())
271 else:
286 else:
272 return s.strip()
287 return s.strip()
273
288
274 @templatekeyword('diffstat', requires={'ctx'})
289 @templatekeyword('diffstat', requires={'ctx'})
275 def showdiffstat(context, mapping):
290 def showdiffstat(context, mapping):
276 """String. Statistics of changes with the following format:
291 """String. Statistics of changes with the following format:
277 "modified files: +added/-removed lines"
292 "modified files: +added/-removed lines"
278 """
293 """
279 ctx = context.resource(mapping, 'ctx')
294 ctx = context.resource(mapping, 'ctx')
280 stats = patch.diffstatdata(util.iterlines(ctx.diff(noprefix=False)))
295 stats = patch.diffstatdata(util.iterlines(ctx.diff(noprefix=False)))
281 maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
296 maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
282 return '%d: +%d/-%d' % (len(stats), adds, removes)
297 return '%d: +%d/-%d' % (len(stats), adds, removes)
283
298
284 @templatekeyword('envvars', requires={'ui', 'templ'})
299 @templatekeyword('envvars', requires={'ui'})
285 def showenvvars(context, mapping):
300 def showenvvars(context, mapping):
286 """A dictionary of environment variables. (EXPERIMENTAL)"""
301 """A dictionary of environment variables. (EXPERIMENTAL)"""
287 ui = context.resource(mapping, 'ui')
302 ui = context.resource(mapping, 'ui')
288 env = ui.exportableenviron()
303 env = ui.exportableenviron()
289 env = util.sortdict((k, env[k]) for k in sorted(env))
304 env = util.sortdict((k, env[k]) for k in sorted(env))
290 return compatdict(context, mapping, 'envvar', env, plural='envvars')
305 return compatdict(context, mapping, 'envvar', env, plural='envvars')
291
306
292 @templatekeyword('extras', requires={'ctx', 'templ'})
307 @templatekeyword('extras', requires={'ctx', 'templ'})
293 def showextras(context, mapping):
308 def showextras(context, mapping):
294 """List of dicts with key, value entries of the 'extras'
309 """List of dicts with key, value entries of the 'extras'
295 field of this changeset."""
310 field of this changeset."""
296 ctx = context.resource(mapping, 'ctx')
311 ctx = context.resource(mapping, 'ctx')
297 templ = context.resource(mapping, 'templ')
312 templ = context.resource(mapping, 'templ')
298 extras = ctx.extra()
313 extras = ctx.extra()
299 extras = util.sortdict((k, extras[k]) for k in sorted(extras))
314 extras = util.sortdict((k, extras[k]) for k in sorted(extras))
300 makemap = lambda k: {'key': k, 'value': extras[k]}
315 makemap = lambda k: {'key': k, 'value': extras[k]}
301 c = [makemap(k) for k in extras]
316 c = [makemap(k) for k in extras]
302 f = _showlist('extra', c, templ, mapping, plural='extras')
317 f = _showlist('extra', c, templ, mapping, plural='extras')
303 return _hybrid(f, extras, makemap,
318 return _hybrid(f, extras, makemap,
304 lambda k: '%s=%s' % (k, util.escapestr(extras[k])))
319 lambda k: '%s=%s' % (k, util.escapestr(extras[k])))
305
320
306 def _showfilesbystat(context, mapping, name, index):
321 def _showfilesbystat(context, mapping, name, index):
307 repo = context.resource(mapping, 'repo')
322 repo = context.resource(mapping, 'repo')
308 ctx = context.resource(mapping, 'ctx')
323 ctx = context.resource(mapping, 'ctx')
309 revcache = context.resource(mapping, 'revcache')
324 revcache = context.resource(mapping, 'revcache')
310 if 'files' not in revcache:
325 if 'files' not in revcache:
311 revcache['files'] = repo.status(ctx.p1(), ctx)[:3]
326 revcache['files'] = repo.status(ctx.p1(), ctx)[:3]
312 files = revcache['files'][index]
327 files = revcache['files'][index]
313 return compatlist(context, mapping, name, files, element='file')
328 return compatlist(context, mapping, name, files, element='file')
314
329
315 @templatekeyword('file_adds', requires={'repo', 'ctx', 'revcache', 'templ'})
330 @templatekeyword('file_adds', requires={'repo', 'ctx', 'revcache'})
316 def showfileadds(context, mapping):
331 def showfileadds(context, mapping):
317 """List of strings. Files added by this changeset."""
332 """List of strings. Files added by this changeset."""
318 return _showfilesbystat(context, mapping, 'file_add', 1)
333 return _showfilesbystat(context, mapping, 'file_add', 1)
319
334
320 @templatekeyword('file_copies',
335 @templatekeyword('file_copies',
321 requires={'repo', 'ctx', 'cache', 'revcache', 'templ'})
336 requires={'repo', 'ctx', 'cache', 'revcache'})
322 def showfilecopies(context, mapping):
337 def showfilecopies(context, mapping):
323 """List of strings. Files copied in this changeset with
338 """List of strings. Files copied in this changeset with
324 their sources.
339 their sources.
325 """
340 """
326 repo = context.resource(mapping, 'repo')
341 repo = context.resource(mapping, 'repo')
327 ctx = context.resource(mapping, 'ctx')
342 ctx = context.resource(mapping, 'ctx')
328 cache = context.resource(mapping, 'cache')
343 cache = context.resource(mapping, 'cache')
329 copies = context.resource(mapping, 'revcache').get('copies')
344 copies = context.resource(mapping, 'revcache').get('copies')
330 if copies is None:
345 if copies is None:
331 if 'getrenamed' not in cache:
346 if 'getrenamed' not in cache:
332 cache['getrenamed'] = getrenamedfn(repo)
347 cache['getrenamed'] = getrenamedfn(repo)
333 copies = []
348 copies = []
334 getrenamed = cache['getrenamed']
349 getrenamed = cache['getrenamed']
335 for fn in ctx.files():
350 for fn in ctx.files():
336 rename = getrenamed(fn, ctx.rev())
351 rename = getrenamed(fn, ctx.rev())
337 if rename:
352 if rename:
338 copies.append((fn, rename[0]))
353 copies.append((fn, rename[0]))
339
354
340 copies = util.sortdict(copies)
355 copies = util.sortdict(copies)
341 return compatdict(context, mapping, 'file_copy', copies,
356 return compatdict(context, mapping, 'file_copy', copies,
342 key='name', value='source', fmt='%s (%s)',
357 key='name', value='source', fmt='%s (%s)',
343 plural='file_copies')
358 plural='file_copies')
344
359
345 # showfilecopiesswitch() displays file copies only if copy records are
360 # showfilecopiesswitch() displays file copies only if copy records are
346 # provided before calling the templater, usually with a --copies
361 # provided before calling the templater, usually with a --copies
347 # command line switch.
362 # command line switch.
348 @templatekeyword('file_copies_switch', requires={'revcache', 'templ'})
363 @templatekeyword('file_copies_switch', requires={'revcache'})
349 def showfilecopiesswitch(context, mapping):
364 def showfilecopiesswitch(context, mapping):
350 """List of strings. Like "file_copies" but displayed
365 """List of strings. Like "file_copies" but displayed
351 only if the --copied switch is set.
366 only if the --copied switch is set.
352 """
367 """
353 copies = context.resource(mapping, 'revcache').get('copies') or []
368 copies = context.resource(mapping, 'revcache').get('copies') or []
354 copies = util.sortdict(copies)
369 copies = util.sortdict(copies)
355 return compatdict(context, mapping, 'file_copy', copies,
370 return compatdict(context, mapping, 'file_copy', copies,
356 key='name', value='source', fmt='%s (%s)',
371 key='name', value='source', fmt='%s (%s)',
357 plural='file_copies')
372 plural='file_copies')
358
373
359 @templatekeyword('file_dels', requires={'repo', 'ctx', 'revcache', 'templ'})
374 @templatekeyword('file_dels', requires={'repo', 'ctx', 'revcache'})
360 def showfiledels(context, mapping):
375 def showfiledels(context, mapping):
361 """List of strings. Files removed by this changeset."""
376 """List of strings. Files removed by this changeset."""
362 return _showfilesbystat(context, mapping, 'file_del', 2)
377 return _showfilesbystat(context, mapping, 'file_del', 2)
363
378
364 @templatekeyword('file_mods', requires={'repo', 'ctx', 'revcache', 'templ'})
379 @templatekeyword('file_mods', requires={'repo', 'ctx', 'revcache'})
365 def showfilemods(context, mapping):
380 def showfilemods(context, mapping):
366 """List of strings. Files modified by this changeset."""
381 """List of strings. Files modified by this changeset."""
367 return _showfilesbystat(context, mapping, 'file_mod', 0)
382 return _showfilesbystat(context, mapping, 'file_mod', 0)
368
383
369 @templatekeyword('files', requires={'ctx', 'templ'})
384 @templatekeyword('files', requires={'ctx'})
370 def showfiles(context, mapping):
385 def showfiles(context, mapping):
371 """List of strings. All files modified, added, or removed by this
386 """List of strings. All files modified, added, or removed by this
372 changeset.
387 changeset.
373 """
388 """
374 ctx = context.resource(mapping, 'ctx')
389 ctx = context.resource(mapping, 'ctx')
375 return compatlist(context, mapping, 'file', ctx.files())
390 return compatlist(context, mapping, 'file', ctx.files())
376
391
377 @templatekeyword('graphnode', requires={'repo', 'ctx'})
392 @templatekeyword('graphnode', requires={'repo', 'ctx'})
378 def showgraphnode(context, mapping):
393 def showgraphnode(context, mapping):
379 """String. The character representing the changeset node in an ASCII
394 """String. The character representing the changeset node in an ASCII
380 revision graph."""
395 revision graph."""
381 repo = context.resource(mapping, 'repo')
396 repo = context.resource(mapping, 'repo')
382 ctx = context.resource(mapping, 'ctx')
397 ctx = context.resource(mapping, 'ctx')
383 return getgraphnode(repo, ctx)
398 return getgraphnode(repo, ctx)
384
399
385 def getgraphnode(repo, ctx):
400 def getgraphnode(repo, ctx):
386 wpnodes = repo.dirstate.parents()
401 wpnodes = repo.dirstate.parents()
387 if wpnodes[1] == nullid:
402 if wpnodes[1] == nullid:
388 wpnodes = wpnodes[:1]
403 wpnodes = wpnodes[:1]
389 if ctx.node() in wpnodes:
404 if ctx.node() in wpnodes:
390 return '@'
405 return '@'
391 elif ctx.obsolete():
406 elif ctx.obsolete():
392 return 'x'
407 return 'x'
393 elif ctx.isunstable():
408 elif ctx.isunstable():
394 return '*'
409 return '*'
395 elif ctx.closesbranch():
410 elif ctx.closesbranch():
396 return '_'
411 return '_'
397 else:
412 else:
398 return 'o'
413 return 'o'
399
414
400 @templatekeyword('graphwidth', requires=())
415 @templatekeyword('graphwidth', requires=())
401 def showgraphwidth(context, mapping):
416 def showgraphwidth(context, mapping):
402 """Integer. The width of the graph drawn by 'log --graph' or zero."""
417 """Integer. The width of the graph drawn by 'log --graph' or zero."""
403 # just hosts documentation; should be overridden by template mapping
418 # just hosts documentation; should be overridden by template mapping
404 return 0
419 return 0
405
420
406 @templatekeyword('index', requires=())
421 @templatekeyword('index', requires=())
407 def showindex(context, mapping):
422 def showindex(context, mapping):
408 """Integer. The current iteration of the loop. (0 indexed)"""
423 """Integer. The current iteration of the loop. (0 indexed)"""
409 # just hosts documentation; should be overridden by template mapping
424 # just hosts documentation; should be overridden by template mapping
410 raise error.Abort(_("can't use index in this context"))
425 raise error.Abort(_("can't use index in this context"))
411
426
412 @templatekeyword('latesttag', requires={'repo', 'ctx', 'cache', 'templ'})
427 @templatekeyword('latesttag', requires={'repo', 'ctx', 'cache', 'templ'})
413 def showlatesttag(context, mapping):
428 def showlatesttag(context, mapping):
414 """List of strings. The global tags on the most recent globally
429 """List of strings. The global tags on the most recent globally
415 tagged ancestor of this changeset. If no such tags exist, the list
430 tagged ancestor of this changeset. If no such tags exist, the list
416 consists of the single string "null".
431 consists of the single string "null".
417 """
432 """
418 return showlatesttags(context, mapping, None)
433 return showlatesttags(context, mapping, None)
419
434
420 def showlatesttags(context, mapping, pattern):
435 def showlatesttags(context, mapping, pattern):
421 """helper method for the latesttag keyword and function"""
436 """helper method for the latesttag keyword and function"""
422 latesttags = getlatesttags(context, mapping, pattern)
437 latesttags = getlatesttags(context, mapping, pattern)
423
438
424 # latesttag[0] is an implementation detail for sorting csets on different
439 # latesttag[0] is an implementation detail for sorting csets on different
425 # branches in a stable manner- it is the date the tagged cset was created,
440 # branches in a stable manner- it is the date the tagged cset was created,
426 # not the date the tag was created. Therefore it isn't made visible here.
441 # not the date the tag was created. Therefore it isn't made visible here.
427 makemap = lambda v: {
442 makemap = lambda v: {
428 'changes': _showchangessincetag,
443 'changes': _showchangessincetag,
429 'distance': latesttags[1],
444 'distance': latesttags[1],
430 'latesttag': v, # BC with {latesttag % '{latesttag}'}
445 'latesttag': v, # BC with {latesttag % '{latesttag}'}
431 'tag': v
446 'tag': v
432 }
447 }
433
448
434 tags = latesttags[2]
449 tags = latesttags[2]
435 templ = context.resource(mapping, 'templ')
450 templ = context.resource(mapping, 'templ')
436 f = _showlist('latesttag', tags, templ, mapping, separator=':')
451 f = _showlist('latesttag', tags, templ, mapping, separator=':')
437 return _hybrid(f, tags, makemap, pycompat.identity)
452 return _hybrid(f, tags, makemap, pycompat.identity)
438
453
439 @templatekeyword('latesttagdistance', requires={'repo', 'ctx', 'cache'})
454 @templatekeyword('latesttagdistance', requires={'repo', 'ctx', 'cache'})
440 def showlatesttagdistance(context, mapping):
455 def showlatesttagdistance(context, mapping):
441 """Integer. Longest path to the latest tag."""
456 """Integer. Longest path to the latest tag."""
442 return getlatesttags(context, mapping)[1]
457 return getlatesttags(context, mapping)[1]
443
458
444 @templatekeyword('changessincelatesttag', requires={'repo', 'ctx', 'cache'})
459 @templatekeyword('changessincelatesttag', requires={'repo', 'ctx', 'cache'})
445 def showchangessincelatesttag(context, mapping):
460 def showchangessincelatesttag(context, mapping):
446 """Integer. All ancestors not in the latest tag."""
461 """Integer. All ancestors not in the latest tag."""
447 mapping = mapping.copy()
462 mapping = mapping.copy()
448 mapping['tag'] = getlatesttags(context, mapping)[2][0]
463 mapping['tag'] = getlatesttags(context, mapping)[2][0]
449 return _showchangessincetag(context, mapping)
464 return _showchangessincetag(context, mapping)
450
465
451 def _showchangessincetag(context, mapping):
466 def _showchangessincetag(context, mapping):
452 repo = context.resource(mapping, 'repo')
467 repo = context.resource(mapping, 'repo')
453 ctx = context.resource(mapping, 'ctx')
468 ctx = context.resource(mapping, 'ctx')
454 offset = 0
469 offset = 0
455 revs = [ctx.rev()]
470 revs = [ctx.rev()]
456 tag = context.symbol(mapping, 'tag')
471 tag = context.symbol(mapping, 'tag')
457
472
458 # The only() revset doesn't currently support wdir()
473 # The only() revset doesn't currently support wdir()
459 if ctx.rev() is None:
474 if ctx.rev() is None:
460 offset = 1
475 offset = 1
461 revs = [p.rev() for p in ctx.parents()]
476 revs = [p.rev() for p in ctx.parents()]
462
477
463 return len(repo.revs('only(%ld, %s)', revs, tag)) + offset
478 return len(repo.revs('only(%ld, %s)', revs, tag)) + offset
464
479
465 # teach templater latesttags.changes is switched to (context, mapping) API
480 # teach templater latesttags.changes is switched to (context, mapping) API
466 _showchangessincetag._requires = {'repo', 'ctx'}
481 _showchangessincetag._requires = {'repo', 'ctx'}
467
482
468 @templatekeyword('manifest', requires={'repo', 'ctx', 'templ'})
483 @templatekeyword('manifest', requires={'repo', 'ctx'})
469 def showmanifest(context, mapping):
484 def showmanifest(context, mapping):
470 repo = context.resource(mapping, 'repo')
485 repo = context.resource(mapping, 'repo')
471 ctx = context.resource(mapping, 'ctx')
486 ctx = context.resource(mapping, 'ctx')
472 templ = context.resource(mapping, 'templ')
473 mnode = ctx.manifestnode()
487 mnode = ctx.manifestnode()
474 if mnode is None:
488 if mnode is None:
475 # just avoid crash, we might want to use the 'ff...' hash in future
489 # just avoid crash, we might want to use the 'ff...' hash in future
476 return
490 return
477 mrev = repo.manifestlog._revlog.rev(mnode)
491 mrev = repo.manifestlog._revlog.rev(mnode)
478 mhex = hex(mnode)
492 mhex = hex(mnode)
479 mapping = mapping.copy()
493 mapping = mapping.copy()
480 mapping.update({'rev': mrev, 'node': mhex})
494 mapping.update({'rev': mrev, 'node': mhex})
481 f = templ.generate('manifest', mapping)
495 f = context.process('manifest', mapping)
482 # TODO: perhaps 'ctx' should be dropped from mapping because manifest
496 # TODO: perhaps 'ctx' should be dropped from mapping because manifest
483 # rev and node are completely different from changeset's.
497 # rev and node are completely different from changeset's.
484 return _mappable(f, None, f, lambda x: {'rev': mrev, 'node': mhex})
498 return _mappable(f, None, f, lambda x: {'rev': mrev, 'node': mhex})
485
499
486 @templatekeyword('obsfate', requires={'ui', 'repo', 'ctx', 'templ'})
500 @templatekeyword('obsfate', requires={'ui', 'repo', 'ctx', 'templ'})
487 def showobsfate(context, mapping):
501 def showobsfate(context, mapping):
488 # this function returns a list containing pre-formatted obsfate strings.
502 # this function returns a list containing pre-formatted obsfate strings.
489 #
503 #
490 # This function will be replaced by templates fragments when we will have
504 # This function will be replaced by templates fragments when we will have
491 # the verbosity templatekw available.
505 # the verbosity templatekw available.
492 succsandmarkers = showsuccsandmarkers(context, mapping)
506 succsandmarkers = showsuccsandmarkers(context, mapping)
493
507
494 ui = context.resource(mapping, 'ui')
508 ui = context.resource(mapping, 'ui')
495 values = []
509 values = []
496
510
497 for x in succsandmarkers:
511 for x in succsandmarkers:
498 values.append(obsutil.obsfateprinter(x['successors'], x['markers'], ui))
512 values.append(obsutil.obsfateprinter(x['successors'], x['markers'], ui))
499
513
500 return compatlist(context, mapping, "fate", values)
514 return compatlist(context, mapping, "fate", values)
501
515
502 def shownames(context, mapping, namespace):
516 def shownames(context, mapping, namespace):
503 """helper method to generate a template keyword for a namespace"""
517 """helper method to generate a template keyword for a namespace"""
504 repo = context.resource(mapping, 'repo')
518 repo = context.resource(mapping, 'repo')
505 ctx = context.resource(mapping, 'ctx')
519 ctx = context.resource(mapping, 'ctx')
506 ns = repo.names[namespace]
520 ns = repo.names[namespace]
507 names = ns.names(repo, ctx.node())
521 names = ns.names(repo, ctx.node())
508 return compatlist(context, mapping, ns.templatename, names,
522 return compatlist(context, mapping, ns.templatename, names,
509 plural=namespace)
523 plural=namespace)
510
524
511 @templatekeyword('namespaces', requires={'repo', 'ctx', 'templ'})
525 @templatekeyword('namespaces', requires={'repo', 'ctx', 'templ'})
512 def shownamespaces(context, mapping):
526 def shownamespaces(context, mapping):
513 """Dict of lists. Names attached to this changeset per
527 """Dict of lists. Names attached to this changeset per
514 namespace."""
528 namespace."""
515 repo = context.resource(mapping, 'repo')
529 repo = context.resource(mapping, 'repo')
516 ctx = context.resource(mapping, 'ctx')
530 ctx = context.resource(mapping, 'ctx')
517 templ = context.resource(mapping, 'templ')
531 templ = context.resource(mapping, 'templ')
518
532
519 namespaces = util.sortdict()
533 namespaces = util.sortdict()
520 def makensmapfn(ns):
534 def makensmapfn(ns):
521 # 'name' for iterating over namespaces, templatename for local reference
535 # 'name' for iterating over namespaces, templatename for local reference
522 return lambda v: {'name': v, ns.templatename: v}
536 return lambda v: {'name': v, ns.templatename: v}
523
537
524 for k, ns in repo.names.iteritems():
538 for k, ns in repo.names.iteritems():
525 names = ns.names(repo, ctx.node())
539 names = ns.names(repo, ctx.node())
526 f = _showlist('name', names, templ, mapping)
540 f = _showlist('name', names, templ, mapping)
527 namespaces[k] = _hybrid(f, names, makensmapfn(ns), pycompat.identity)
541 namespaces[k] = _hybrid(f, names, makensmapfn(ns), pycompat.identity)
528
542
529 f = _showlist('namespace', list(namespaces), templ, mapping)
543 f = _showlist('namespace', list(namespaces), templ, mapping)
530
544
531 def makemap(ns):
545 def makemap(ns):
532 return {
546 return {
533 'namespace': ns,
547 'namespace': ns,
534 'names': namespaces[ns],
548 'names': namespaces[ns],
535 'builtin': repo.names[ns].builtin,
549 'builtin': repo.names[ns].builtin,
536 'colorname': repo.names[ns].colorname,
550 'colorname': repo.names[ns].colorname,
537 }
551 }
538
552
539 return _hybrid(f, namespaces, makemap, pycompat.identity)
553 return _hybrid(f, namespaces, makemap, pycompat.identity)
540
554
541 @templatekeyword('node', requires={'ctx'})
555 @templatekeyword('node', requires={'ctx'})
542 def shownode(context, mapping):
556 def shownode(context, mapping):
543 """String. The changeset identification hash, as a 40 hexadecimal
557 """String. The changeset identification hash, as a 40 hexadecimal
544 digit string.
558 digit string.
545 """
559 """
546 ctx = context.resource(mapping, 'ctx')
560 ctx = context.resource(mapping, 'ctx')
547 return ctx.hex()
561 return ctx.hex()
548
562
549 @templatekeyword('obsolete', requires={'ctx'})
563 @templatekeyword('obsolete', requires={'ctx'})
550 def showobsolete(context, mapping):
564 def showobsolete(context, mapping):
551 """String. Whether the changeset is obsolete. (EXPERIMENTAL)"""
565 """String. Whether the changeset is obsolete. (EXPERIMENTAL)"""
552 ctx = context.resource(mapping, 'ctx')
566 ctx = context.resource(mapping, 'ctx')
553 if ctx.obsolete():
567 if ctx.obsolete():
554 return 'obsolete'
568 return 'obsolete'
555 return ''
569 return ''
556
570
557 @templatekeyword('peerurls', requires={'repo'})
571 @templatekeyword('peerurls', requires={'repo'})
558 def showpeerurls(context, mapping):
572 def showpeerurls(context, mapping):
559 """A dictionary of repository locations defined in the [paths] section
573 """A dictionary of repository locations defined in the [paths] section
560 of your configuration file."""
574 of your configuration file."""
561 repo = context.resource(mapping, 'repo')
575 repo = context.resource(mapping, 'repo')
562 # see commands.paths() for naming of dictionary keys
576 # see commands.paths() for naming of dictionary keys
563 paths = repo.ui.paths
577 paths = repo.ui.paths
564 urls = util.sortdict((k, p.rawloc) for k, p in sorted(paths.iteritems()))
578 urls = util.sortdict((k, p.rawloc) for k, p in sorted(paths.iteritems()))
565 def makemap(k):
579 def makemap(k):
566 p = paths[k]
580 p = paths[k]
567 d = {'name': k, 'url': p.rawloc}
581 d = {'name': k, 'url': p.rawloc}
568 d.update((o, v) for o, v in sorted(p.suboptions.iteritems()))
582 d.update((o, v) for o, v in sorted(p.suboptions.iteritems()))
569 return d
583 return d
570 return _hybrid(None, urls, makemap, lambda k: '%s=%s' % (k, urls[k]))
584 return _hybrid(None, urls, makemap, lambda k: '%s=%s' % (k, urls[k]))
571
585
572 @templatekeyword("predecessors", requires={'repo', 'ctx'})
586 @templatekeyword("predecessors", requires={'repo', 'ctx'})
573 def showpredecessors(context, mapping):
587 def showpredecessors(context, mapping):
574 """Returns the list if the closest visible successors. (EXPERIMENTAL)"""
588 """Returns the list if the closest visible successors. (EXPERIMENTAL)"""
575 repo = context.resource(mapping, 'repo')
589 repo = context.resource(mapping, 'repo')
576 ctx = context.resource(mapping, 'ctx')
590 ctx = context.resource(mapping, 'ctx')
577 predecessors = sorted(obsutil.closestpredecessors(repo, ctx.node()))
591 predecessors = sorted(obsutil.closestpredecessors(repo, ctx.node()))
578 predecessors = map(hex, predecessors)
592 predecessors = map(hex, predecessors)
579
593
580 return _hybrid(None, predecessors,
594 return _hybrid(None, predecessors,
581 lambda x: {'ctx': repo[x], 'revcache': {}},
595 lambda x: {'ctx': repo[x], 'revcache': {}},
582 lambda x: scmutil.formatchangeid(repo[x]))
596 lambda x: scmutil.formatchangeid(repo[x]))
583
597
584 @templatekeyword('reporoot', requires={'repo'})
598 @templatekeyword('reporoot', requires={'repo'})
585 def showreporoot(context, mapping):
599 def showreporoot(context, mapping):
586 """String. The root directory of the current repository."""
600 """String. The root directory of the current repository."""
587 repo = context.resource(mapping, 'repo')
601 repo = context.resource(mapping, 'repo')
588 return repo.root
602 return repo.root
589
603
590 @templatekeyword("successorssets", requires={'repo', 'ctx'})
604 @templatekeyword("successorssets", requires={'repo', 'ctx'})
591 def showsuccessorssets(context, mapping):
605 def showsuccessorssets(context, mapping):
592 """Returns a string of sets of successors for a changectx. Format used
606 """Returns a string of sets of successors for a changectx. Format used
593 is: [ctx1, ctx2], [ctx3] if ctx has been splitted into ctx1 and ctx2
607 is: [ctx1, ctx2], [ctx3] if ctx has been splitted into ctx1 and ctx2
594 while also diverged into ctx3. (EXPERIMENTAL)"""
608 while also diverged into ctx3. (EXPERIMENTAL)"""
595 repo = context.resource(mapping, 'repo')
609 repo = context.resource(mapping, 'repo')
596 ctx = context.resource(mapping, 'ctx')
610 ctx = context.resource(mapping, 'ctx')
597 if not ctx.obsolete():
611 if not ctx.obsolete():
598 return ''
612 return ''
599
613
600 ssets = obsutil.successorssets(repo, ctx.node(), closest=True)
614 ssets = obsutil.successorssets(repo, ctx.node(), closest=True)
601 ssets = [[hex(n) for n in ss] for ss in ssets]
615 ssets = [[hex(n) for n in ss] for ss in ssets]
602
616
603 data = []
617 data = []
604 for ss in ssets:
618 for ss in ssets:
605 h = _hybrid(None, ss, lambda x: {'ctx': repo[x], 'revcache': {}},
619 h = _hybrid(None, ss, lambda x: {'ctx': repo[x], 'revcache': {}},
606 lambda x: scmutil.formatchangeid(repo[x]))
620 lambda x: scmutil.formatchangeid(repo[x]))
607 data.append(h)
621 data.append(h)
608
622
609 # Format the successorssets
623 # Format the successorssets
610 def render(d):
624 def render(d):
611 t = []
625 t = []
612 for i in d.gen():
626 for i in d.gen():
613 t.append(i)
627 t.append(i)
614 return "".join(t)
628 return "".join(t)
615
629
616 def gen(data):
630 def gen(data):
617 yield "; ".join(render(d) for d in data)
631 yield "; ".join(render(d) for d in data)
618
632
619 return _hybrid(gen(data), data, lambda x: {'successorset': x},
633 return _hybrid(gen(data), data, lambda x: {'successorset': x},
620 pycompat.identity)
634 pycompat.identity)
621
635
622 @templatekeyword("succsandmarkers", requires={'repo', 'ctx', 'templ'})
636 @templatekeyword("succsandmarkers", requires={'repo', 'ctx', 'templ'})
623 def showsuccsandmarkers(context, mapping):
637 def showsuccsandmarkers(context, mapping):
624 """Returns a list of dict for each final successor of ctx. The dict
638 """Returns a list of dict for each final successor of ctx. The dict
625 contains successors node id in "successors" keys and the list of
639 contains successors node id in "successors" keys and the list of
626 obs-markers from ctx to the set of successors in "markers".
640 obs-markers from ctx to the set of successors in "markers".
627 (EXPERIMENTAL)
641 (EXPERIMENTAL)
628 """
642 """
629 repo = context.resource(mapping, 'repo')
643 repo = context.resource(mapping, 'repo')
630 ctx = context.resource(mapping, 'ctx')
644 ctx = context.resource(mapping, 'ctx')
631 templ = context.resource(mapping, 'templ')
645 templ = context.resource(mapping, 'templ')
632
646
633 values = obsutil.successorsandmarkers(repo, ctx)
647 values = obsutil.successorsandmarkers(repo, ctx)
634
648
635 if values is None:
649 if values is None:
636 values = []
650 values = []
637
651
638 # Format successors and markers to avoid exposing binary to templates
652 # Format successors and markers to avoid exposing binary to templates
639 data = []
653 data = []
640 for i in values:
654 for i in values:
641 # Format successors
655 # Format successors
642 successors = i['successors']
656 successors = i['successors']
643
657
644 successors = [hex(n) for n in successors]
658 successors = [hex(n) for n in successors]
645 successors = _hybrid(None, successors,
659 successors = _hybrid(None, successors,
646 lambda x: {'ctx': repo[x], 'revcache': {}},
660 lambda x: {'ctx': repo[x], 'revcache': {}},
647 lambda x: scmutil.formatchangeid(repo[x]))
661 lambda x: scmutil.formatchangeid(repo[x]))
648
662
649 # Format markers
663 # Format markers
650 finalmarkers = []
664 finalmarkers = []
651 for m in i['markers']:
665 for m in i['markers']:
652 hexprec = hex(m[0])
666 hexprec = hex(m[0])
653 hexsucs = tuple(hex(n) for n in m[1])
667 hexsucs = tuple(hex(n) for n in m[1])
654 hexparents = None
668 hexparents = None
655 if m[5] is not None:
669 if m[5] is not None:
656 hexparents = tuple(hex(n) for n in m[5])
670 hexparents = tuple(hex(n) for n in m[5])
657 newmarker = (hexprec, hexsucs) + m[2:5] + (hexparents,) + m[6:]
671 newmarker = (hexprec, hexsucs) + m[2:5] + (hexparents,) + m[6:]
658 finalmarkers.append(newmarker)
672 finalmarkers.append(newmarker)
659
673
660 data.append({'successors': successors, 'markers': finalmarkers})
674 data.append({'successors': successors, 'markers': finalmarkers})
661
675
662 f = _showlist('succsandmarkers', data, templ, mapping)
676 f = _showlist('succsandmarkers', data, templ, mapping)
663 return _hybrid(f, data, lambda x: x, pycompat.identity)
677 return _hybrid(f, data, lambda x: x, pycompat.identity)
664
678
665 @templatekeyword('p1rev', requires={'ctx'})
679 @templatekeyword('p1rev', requires={'ctx'})
666 def showp1rev(context, mapping):
680 def showp1rev(context, mapping):
667 """Integer. The repository-local revision number of the changeset's
681 """Integer. The repository-local revision number of the changeset's
668 first parent, or -1 if the changeset has no parents."""
682 first parent, or -1 if the changeset has no parents."""
669 ctx = context.resource(mapping, 'ctx')
683 ctx = context.resource(mapping, 'ctx')
670 return ctx.p1().rev()
684 return ctx.p1().rev()
671
685
672 @templatekeyword('p2rev', requires={'ctx'})
686 @templatekeyword('p2rev', requires={'ctx'})
673 def showp2rev(context, mapping):
687 def showp2rev(context, mapping):
674 """Integer. The repository-local revision number of the changeset's
688 """Integer. The repository-local revision number of the changeset's
675 second parent, or -1 if the changeset has no second parent."""
689 second parent, or -1 if the changeset has no second parent."""
676 ctx = context.resource(mapping, 'ctx')
690 ctx = context.resource(mapping, 'ctx')
677 return ctx.p2().rev()
691 return ctx.p2().rev()
678
692
679 @templatekeyword('p1node', requires={'ctx'})
693 @templatekeyword('p1node', requires={'ctx'})
680 def showp1node(context, mapping):
694 def showp1node(context, mapping):
681 """String. The identification hash of the changeset's first parent,
695 """String. The identification hash of the changeset's first parent,
682 as a 40 digit hexadecimal string. If the changeset has no parents, all
696 as a 40 digit hexadecimal string. If the changeset has no parents, all
683 digits are 0."""
697 digits are 0."""
684 ctx = context.resource(mapping, 'ctx')
698 ctx = context.resource(mapping, 'ctx')
685 return ctx.p1().hex()
699 return ctx.p1().hex()
686
700
687 @templatekeyword('p2node', requires={'ctx'})
701 @templatekeyword('p2node', requires={'ctx'})
688 def showp2node(context, mapping):
702 def showp2node(context, mapping):
689 """String. The identification hash of the changeset's second
703 """String. The identification hash of the changeset's second
690 parent, as a 40 digit hexadecimal string. If the changeset has no second
704 parent, as a 40 digit hexadecimal string. If the changeset has no second
691 parent, all digits are 0."""
705 parent, all digits are 0."""
692 ctx = context.resource(mapping, 'ctx')
706 ctx = context.resource(mapping, 'ctx')
693 return ctx.p2().hex()
707 return ctx.p2().hex()
694
708
695 @templatekeyword('parents', requires={'repo', 'ctx', 'templ'})
709 @templatekeyword('parents', requires={'repo', 'ctx', 'templ'})
696 def showparents(context, mapping):
710 def showparents(context, mapping):
697 """List of strings. The parents of the changeset in "rev:node"
711 """List of strings. The parents of the changeset in "rev:node"
698 format. If the changeset has only one "natural" parent (the predecessor
712 format. If the changeset has only one "natural" parent (the predecessor
699 revision) nothing is shown."""
713 revision) nothing is shown."""
700 repo = context.resource(mapping, 'repo')
714 repo = context.resource(mapping, 'repo')
701 ctx = context.resource(mapping, 'ctx')
715 ctx = context.resource(mapping, 'ctx')
702 templ = context.resource(mapping, 'templ')
716 templ = context.resource(mapping, 'templ')
703 pctxs = scmutil.meaningfulparents(repo, ctx)
717 pctxs = scmutil.meaningfulparents(repo, ctx)
704 prevs = [p.rev() for p in pctxs]
718 prevs = [p.rev() for p in pctxs]
705 parents = [[('rev', p.rev()),
719 parents = [[('rev', p.rev()),
706 ('node', p.hex()),
720 ('node', p.hex()),
707 ('phase', p.phasestr())]
721 ('phase', p.phasestr())]
708 for p in pctxs]
722 for p in pctxs]
709 f = _showlist('parent', parents, templ, mapping)
723 f = _showlist('parent', parents, templ, mapping)
710 return _hybrid(f, prevs, lambda x: {'ctx': repo[x], 'revcache': {}},
724 return _hybrid(f, prevs, lambda x: {'ctx': repo[x], 'revcache': {}},
711 lambda x: scmutil.formatchangeid(repo[x]), keytype=int)
725 lambda x: scmutil.formatchangeid(repo[x]), keytype=int)
712
726
713 @templatekeyword('phase', requires={'ctx'})
727 @templatekeyword('phase', requires={'ctx'})
714 def showphase(context, mapping):
728 def showphase(context, mapping):
715 """String. The changeset phase name."""
729 """String. The changeset phase name."""
716 ctx = context.resource(mapping, 'ctx')
730 ctx = context.resource(mapping, 'ctx')
717 return ctx.phasestr()
731 return ctx.phasestr()
718
732
719 @templatekeyword('phaseidx', requires={'ctx'})
733 @templatekeyword('phaseidx', requires={'ctx'})
720 def showphaseidx(context, mapping):
734 def showphaseidx(context, mapping):
721 """Integer. The changeset phase index. (ADVANCED)"""
735 """Integer. The changeset phase index. (ADVANCED)"""
722 ctx = context.resource(mapping, 'ctx')
736 ctx = context.resource(mapping, 'ctx')
723 return ctx.phase()
737 return ctx.phase()
724
738
725 @templatekeyword('rev', requires={'ctx'})
739 @templatekeyword('rev', requires={'ctx'})
726 def showrev(context, mapping):
740 def showrev(context, mapping):
727 """Integer. The repository-local changeset revision number."""
741 """Integer. The repository-local changeset revision number."""
728 ctx = context.resource(mapping, 'ctx')
742 ctx = context.resource(mapping, 'ctx')
729 return scmutil.intrev(ctx)
743 return scmutil.intrev(ctx)
730
744
731 def showrevslist(context, mapping, name, revs):
745 def showrevslist(context, mapping, name, revs):
732 """helper to generate a list of revisions in which a mapped template will
746 """helper to generate a list of revisions in which a mapped template will
733 be evaluated"""
747 be evaluated"""
734 repo = context.resource(mapping, 'repo')
748 repo = context.resource(mapping, 'repo')
735 templ = context.resource(mapping, 'templ')
749 templ = context.resource(mapping, 'templ')
736 f = _showlist(name, ['%d' % r for r in revs], templ, mapping)
750 f = _showlist(name, ['%d' % r for r in revs], templ, mapping)
737 return _hybrid(f, revs,
751 return _hybrid(f, revs,
738 lambda x: {name: x, 'ctx': repo[x], 'revcache': {}},
752 lambda x: {name: x, 'ctx': repo[x], 'revcache': {}},
739 pycompat.identity, keytype=int)
753 pycompat.identity, keytype=int)
740
754
741 @templatekeyword('subrepos', requires={'ctx', 'templ'})
755 @templatekeyword('subrepos', requires={'ctx'})
742 def showsubrepos(context, mapping):
756 def showsubrepos(context, mapping):
743 """List of strings. Updated subrepositories in the changeset."""
757 """List of strings. Updated subrepositories in the changeset."""
744 ctx = context.resource(mapping, 'ctx')
758 ctx = context.resource(mapping, 'ctx')
745 substate = ctx.substate
759 substate = ctx.substate
746 if not substate:
760 if not substate:
747 return compatlist(context, mapping, 'subrepo', [])
761 return compatlist(context, mapping, 'subrepo', [])
748 psubstate = ctx.parents()[0].substate or {}
762 psubstate = ctx.parents()[0].substate or {}
749 subrepos = []
763 subrepos = []
750 for sub in substate:
764 for sub in substate:
751 if sub not in psubstate or substate[sub] != psubstate[sub]:
765 if sub not in psubstate or substate[sub] != psubstate[sub]:
752 subrepos.append(sub) # modified or newly added in ctx
766 subrepos.append(sub) # modified or newly added in ctx
753 for sub in psubstate:
767 for sub in psubstate:
754 if sub not in substate:
768 if sub not in substate:
755 subrepos.append(sub) # removed in ctx
769 subrepos.append(sub) # removed in ctx
756 return compatlist(context, mapping, 'subrepo', sorted(subrepos))
770 return compatlist(context, mapping, 'subrepo', sorted(subrepos))
757
771
758 # don't remove "showtags" definition, even though namespaces will put
772 # don't remove "showtags" definition, even though namespaces will put
759 # a helper function for "tags" keyword into "keywords" map automatically,
773 # a helper function for "tags" keyword into "keywords" map automatically,
760 # because online help text is built without namespaces initialization
774 # because online help text is built without namespaces initialization
761 @templatekeyword('tags', requires={'repo', 'ctx', 'templ'})
775 @templatekeyword('tags', requires={'repo', 'ctx'})
762 def showtags(context, mapping):
776 def showtags(context, mapping):
763 """List of strings. Any tags associated with the changeset."""
777 """List of strings. Any tags associated with the changeset."""
764 return shownames(context, mapping, 'tags')
778 return shownames(context, mapping, 'tags')
765
779
766 @templatekeyword('termwidth', requires={'ui'})
780 @templatekeyword('termwidth', requires={'ui'})
767 def showtermwidth(context, mapping):
781 def showtermwidth(context, mapping):
768 """Integer. The width of the current terminal."""
782 """Integer. The width of the current terminal."""
769 ui = context.resource(mapping, 'ui')
783 ui = context.resource(mapping, 'ui')
770 return ui.termwidth()
784 return ui.termwidth()
771
785
772 @templatekeyword('instabilities', requires={'ctx', 'templ'})
786 @templatekeyword('instabilities', requires={'ctx'})
773 def showinstabilities(context, mapping):
787 def showinstabilities(context, mapping):
774 """List of strings. Evolution instabilities affecting the changeset.
788 """List of strings. Evolution instabilities affecting the changeset.
775 (EXPERIMENTAL)
789 (EXPERIMENTAL)
776 """
790 """
777 ctx = context.resource(mapping, 'ctx')
791 ctx = context.resource(mapping, 'ctx')
778 return compatlist(context, mapping, 'instability', ctx.instabilities(),
792 return compatlist(context, mapping, 'instability', ctx.instabilities(),
779 plural='instabilities')
793 plural='instabilities')
780
794
781 @templatekeyword('verbosity', requires={'ui'})
795 @templatekeyword('verbosity', requires={'ui'})
782 def showverbosity(context, mapping):
796 def showverbosity(context, mapping):
783 """String. The current output verbosity in 'debug', 'quiet', 'verbose',
797 """String. The current output verbosity in 'debug', 'quiet', 'verbose',
784 or ''."""
798 or ''."""
785 ui = context.resource(mapping, 'ui')
799 ui = context.resource(mapping, 'ui')
786 # see logcmdutil.changesettemplater for priority of these flags
800 # see logcmdutil.changesettemplater for priority of these flags
787 if ui.debugflag:
801 if ui.debugflag:
788 return 'debug'
802 return 'debug'
789 elif ui.quiet:
803 elif ui.quiet:
790 return 'quiet'
804 return 'quiet'
791 elif ui.verbose:
805 elif ui.verbose:
792 return 'verbose'
806 return 'verbose'
793 return ''
807 return ''
794
808
795 def loadkeyword(ui, extname, registrarobj):
809 def loadkeyword(ui, extname, registrarobj):
796 """Load template keyword from specified registrarobj
810 """Load template keyword from specified registrarobj
797 """
811 """
798 for name, func in registrarobj._table.iteritems():
812 for name, func in registrarobj._table.iteritems():
799 keywords[name] = func
813 keywords[name] = func
800
814
801 # tell hggettext to extract docstrings from these functions:
815 # tell hggettext to extract docstrings from these functions:
802 i18nfunctions = keywords.values()
816 i18nfunctions = keywords.values()
@@ -1,448 +1,447
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 util,
16 util,
17 )
17 )
18
18
19 class ResourceUnavailable(error.Abort):
19 class ResourceUnavailable(error.Abort):
20 pass
20 pass
21
21
22 class TemplateNotFound(error.Abort):
22 class TemplateNotFound(error.Abort):
23 pass
23 pass
24
24
25 class hybrid(object):
25 class hybrid(object):
26 """Wrapper for list or dict to support legacy template
26 """Wrapper for list or dict to support legacy template
27
27
28 This class allows us to handle both:
28 This class allows us to handle both:
29 - "{files}" (legacy command-line-specific list hack) and
29 - "{files}" (legacy command-line-specific list hack) and
30 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
30 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
31 and to access raw values:
31 and to access raw values:
32 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
32 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
33 - "{get(extras, key)}"
33 - "{get(extras, key)}"
34 - "{files|json}"
34 - "{files|json}"
35 """
35 """
36
36
37 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
37 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
38 if gen is not None:
38 if gen is not None:
39 self.gen = gen # generator or function returning generator
39 self.gen = gen # generator or function returning generator
40 self._values = values
40 self._values = values
41 self._makemap = makemap
41 self._makemap = makemap
42 self.joinfmt = joinfmt
42 self.joinfmt = joinfmt
43 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
43 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
44 def gen(self):
44 def gen(self):
45 """Default generator to stringify this as {join(self, ' ')}"""
45 """Default generator to stringify this as {join(self, ' ')}"""
46 for i, x in enumerate(self._values):
46 for i, x in enumerate(self._values):
47 if i > 0:
47 if i > 0:
48 yield ' '
48 yield ' '
49 yield self.joinfmt(x)
49 yield self.joinfmt(x)
50 def itermaps(self):
50 def itermaps(self):
51 makemap = self._makemap
51 makemap = self._makemap
52 for x in self._values:
52 for x in self._values:
53 yield makemap(x)
53 yield makemap(x)
54 def __contains__(self, x):
54 def __contains__(self, x):
55 return x in self._values
55 return x in self._values
56 def __getitem__(self, key):
56 def __getitem__(self, key):
57 return self._values[key]
57 return self._values[key]
58 def __len__(self):
58 def __len__(self):
59 return len(self._values)
59 return len(self._values)
60 def __iter__(self):
60 def __iter__(self):
61 return iter(self._values)
61 return iter(self._values)
62 def __getattr__(self, name):
62 def __getattr__(self, name):
63 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
63 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
64 r'itervalues', r'keys', r'values'):
64 r'itervalues', r'keys', r'values'):
65 raise AttributeError(name)
65 raise AttributeError(name)
66 return getattr(self._values, name)
66 return getattr(self._values, name)
67
67
68 class mappable(object):
68 class mappable(object):
69 """Wrapper for non-list/dict object to support map operation
69 """Wrapper for non-list/dict object to support map operation
70
70
71 This class allows us to handle both:
71 This class allows us to handle both:
72 - "{manifest}"
72 - "{manifest}"
73 - "{manifest % '{rev}:{node}'}"
73 - "{manifest % '{rev}:{node}'}"
74 - "{manifest.rev}"
74 - "{manifest.rev}"
75
75
76 Unlike a hybrid, this does not simulate the behavior of the underling
76 Unlike a hybrid, this does not simulate the behavior of the underling
77 value. Use unwrapvalue() or unwraphybrid() to obtain the inner object.
77 value. Use unwrapvalue() or unwraphybrid() to obtain the inner object.
78 """
78 """
79
79
80 def __init__(self, gen, key, value, makemap):
80 def __init__(self, gen, key, value, makemap):
81 if gen is not None:
81 if gen is not None:
82 self.gen = gen # generator or function returning generator
82 self.gen = gen # generator or function returning generator
83 self._key = key
83 self._key = key
84 self._value = value # may be generator of strings
84 self._value = value # may be generator of strings
85 self._makemap = makemap
85 self._makemap = makemap
86
86
87 def gen(self):
87 def gen(self):
88 yield pycompat.bytestr(self._value)
88 yield pycompat.bytestr(self._value)
89
89
90 def tomap(self):
90 def tomap(self):
91 return self._makemap(self._key)
91 return self._makemap(self._key)
92
92
93 def itermaps(self):
93 def itermaps(self):
94 yield self.tomap()
94 yield self.tomap()
95
95
96 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
96 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
97 """Wrap data to support both dict-like and string-like operations"""
97 """Wrap data to support both dict-like and string-like operations"""
98 prefmt = pycompat.identity
98 prefmt = pycompat.identity
99 if fmt is None:
99 if fmt is None:
100 fmt = '%s=%s'
100 fmt = '%s=%s'
101 prefmt = pycompat.bytestr
101 prefmt = pycompat.bytestr
102 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
102 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
103 lambda k: fmt % (prefmt(k), prefmt(data[k])))
103 lambda k: fmt % (prefmt(k), prefmt(data[k])))
104
104
105 def hybridlist(data, name, fmt=None, gen=None):
105 def hybridlist(data, name, fmt=None, gen=None):
106 """Wrap data to support both list-like and string-like operations"""
106 """Wrap data to support both list-like and string-like operations"""
107 prefmt = pycompat.identity
107 prefmt = pycompat.identity
108 if fmt is None:
108 if fmt is None:
109 fmt = '%s'
109 fmt = '%s'
110 prefmt = pycompat.bytestr
110 prefmt = pycompat.bytestr
111 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
111 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
112
112
113 def unwraphybrid(thing):
113 def unwraphybrid(thing):
114 """Return an object which can be stringified possibly by using a legacy
114 """Return an object which can be stringified possibly by using a legacy
115 template"""
115 template"""
116 gen = getattr(thing, 'gen', None)
116 gen = getattr(thing, 'gen', None)
117 if gen is None:
117 if gen is None:
118 return thing
118 return thing
119 if callable(gen):
119 if callable(gen):
120 return gen()
120 return gen()
121 return gen
121 return gen
122
122
123 def unwrapvalue(thing):
123 def unwrapvalue(thing):
124 """Move the inner value object out of the wrapper"""
124 """Move the inner value object out of the wrapper"""
125 if not util.safehasattr(thing, '_value'):
125 if not util.safehasattr(thing, '_value'):
126 return thing
126 return thing
127 return thing._value
127 return thing._value
128
128
129 def wraphybridvalue(container, key, value):
129 def wraphybridvalue(container, key, value):
130 """Wrap an element of hybrid container to be mappable
130 """Wrap an element of hybrid container to be mappable
131
131
132 The key is passed to the makemap function of the given container, which
132 The key is passed to the makemap function of the given container, which
133 should be an item generated by iter(container).
133 should be an item generated by iter(container).
134 """
134 """
135 makemap = getattr(container, '_makemap', None)
135 makemap = getattr(container, '_makemap', None)
136 if makemap is None:
136 if makemap is None:
137 return value
137 return value
138 if util.safehasattr(value, '_makemap'):
138 if util.safehasattr(value, '_makemap'):
139 # a nested hybrid list/dict, which has its own way of map operation
139 # a nested hybrid list/dict, which has its own way of map operation
140 return value
140 return value
141 return mappable(None, key, value, makemap)
141 return mappable(None, key, value, makemap)
142
142
143 def compatdict(context, mapping, name, data, key='key', value='value',
143 def compatdict(context, mapping, name, data, key='key', value='value',
144 fmt=None, plural=None, separator=' '):
144 fmt=None, plural=None, separator=' '):
145 """Wrap data like hybriddict(), but also supports old-style list template
145 """Wrap data like hybriddict(), but also supports old-style list template
146
146
147 This exists for backward compatibility with the old-style template. Use
147 This exists for backward compatibility with the old-style template. Use
148 hybriddict() for new template keywords.
148 hybriddict() for new template keywords.
149 """
149 """
150 c = [{key: k, value: v} for k, v in data.iteritems()]
150 c = [{key: k, value: v} for k, v in data.iteritems()]
151 t = context.resource(mapping, 'templ')
151 f = _showcompatlist(context, mapping, name, c, plural, separator)
152 f = _showlist(name, c, t, mapping, plural, separator)
153 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
152 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
154
153
155 def compatlist(context, mapping, name, data, element=None, fmt=None,
154 def compatlist(context, mapping, name, data, element=None, fmt=None,
156 plural=None, separator=' '):
155 plural=None, separator=' '):
157 """Wrap data like hybridlist(), but also supports old-style list template
156 """Wrap data like hybridlist(), but also supports old-style list template
158
157
159 This exists for backward compatibility with the old-style template. Use
158 This exists for backward compatibility with the old-style template. Use
160 hybridlist() for new template keywords.
159 hybridlist() for new template keywords.
161 """
160 """
162 t = context.resource(mapping, 'templ')
161 f = _showcompatlist(context, mapping, name, data, plural, separator)
163 f = _showlist(name, data, t, mapping, plural, separator)
164 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
162 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
165
163
166 def _showlist(name, values, templ, mapping, plural=None, separator=' '):
164 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
167 '''expand set of values.
165 """Return a generator that renders old-style list template
166
168 name is name of key in template map.
167 name is name of key in template map.
169 values is list of strings or dicts.
168 values is list of strings or dicts.
170 plural is plural of name, if not simply name + 's'.
169 plural is plural of name, if not simply name + 's'.
171 separator is used to join values as a string
170 separator is used to join values as a string
172
171
173 expansion works like this, given name 'foo'.
172 expansion works like this, given name 'foo'.
174
173
175 if values is empty, expand 'no_foos'.
174 if values is empty, expand 'no_foos'.
176
175
177 if 'foo' not in template map, return values as a string,
176 if 'foo' not in template map, return values as a string,
178 joined by 'separator'.
177 joined by 'separator'.
179
178
180 expand 'start_foos'.
179 expand 'start_foos'.
181
180
182 for each value, expand 'foo'. if 'last_foo' in template
181 for each value, expand 'foo'. if 'last_foo' in template
183 map, expand it instead of 'foo' for last key.
182 map, expand it instead of 'foo' for last key.
184
183
185 expand 'end_foos'.
184 expand 'end_foos'.
186 '''
185 """
187 if not plural:
186 if not plural:
188 plural = name + 's'
187 plural = name + 's'
189 if not values:
188 if not values:
190 noname = 'no_' + plural
189 noname = 'no_' + plural
191 if noname in templ:
190 if context.preload(noname):
192 yield templ.generate(noname, mapping)
191 yield context.process(noname, mapping)
193 return
192 return
194 if name not in templ:
193 if not context.preload(name):
195 if isinstance(values[0], bytes):
194 if isinstance(values[0], bytes):
196 yield separator.join(values)
195 yield separator.join(values)
197 else:
196 else:
198 for v in values:
197 for v in values:
199 r = dict(v)
198 r = dict(v)
200 r.update(mapping)
199 r.update(mapping)
201 yield r
200 yield r
202 return
201 return
203 startname = 'start_' + plural
202 startname = 'start_' + plural
204 if startname in templ:
203 if context.preload(startname):
205 yield templ.generate(startname, mapping)
204 yield context.process(startname, mapping)
206 vmapping = mapping.copy()
205 vmapping = mapping.copy()
207 def one(v, tag=name):
206 def one(v, tag=name):
208 try:
207 try:
209 vmapping.update(v)
208 vmapping.update(v)
210 # Python 2 raises ValueError if the type of v is wrong. Python
209 # Python 2 raises ValueError if the type of v is wrong. Python
211 # 3 raises TypeError.
210 # 3 raises TypeError.
212 except (AttributeError, TypeError, ValueError):
211 except (AttributeError, TypeError, ValueError):
213 try:
212 try:
214 # Python 2 raises ValueError trying to destructure an e.g.
213 # Python 2 raises ValueError trying to destructure an e.g.
215 # bytes. Python 3 raises TypeError.
214 # bytes. Python 3 raises TypeError.
216 for a, b in v:
215 for a, b in v:
217 vmapping[a] = b
216 vmapping[a] = b
218 except (TypeError, ValueError):
217 except (TypeError, ValueError):
219 vmapping[name] = v
218 vmapping[name] = v
220 return templ.generate(tag, vmapping)
219 return context.process(tag, vmapping)
221 lastname = 'last_' + name
220 lastname = 'last_' + name
222 if lastname in templ:
221 if context.preload(lastname):
223 last = values.pop()
222 last = values.pop()
224 else:
223 else:
225 last = None
224 last = None
226 for v in values:
225 for v in values:
227 yield one(v)
226 yield one(v)
228 if last is not None:
227 if last is not None:
229 yield one(last, tag=lastname)
228 yield one(last, tag=lastname)
230 endname = 'end_' + plural
229 endname = 'end_' + plural
231 if endname in templ:
230 if context.preload(endname):
232 yield templ.generate(endname, mapping)
231 yield context.process(endname, mapping)
233
232
234 def stringify(thing):
233 def stringify(thing):
235 """Turn values into bytes by converting into text and concatenating them"""
234 """Turn values into bytes by converting into text and concatenating them"""
236 thing = unwraphybrid(thing)
235 thing = unwraphybrid(thing)
237 if util.safehasattr(thing, '__iter__') and not isinstance(thing, bytes):
236 if util.safehasattr(thing, '__iter__') and not isinstance(thing, bytes):
238 if isinstance(thing, str):
237 if isinstance(thing, str):
239 # This is only reachable on Python 3 (otherwise
238 # This is only reachable on Python 3 (otherwise
240 # isinstance(thing, bytes) would have been true), and is
239 # isinstance(thing, bytes) would have been true), and is
241 # here to prevent infinite recursion bugs on Python 3.
240 # here to prevent infinite recursion bugs on Python 3.
242 raise error.ProgrammingError(
241 raise error.ProgrammingError(
243 'stringify got unexpected unicode string: %r' % thing)
242 'stringify got unexpected unicode string: %r' % thing)
244 return "".join([stringify(t) for t in thing if t is not None])
243 return "".join([stringify(t) for t in thing if t is not None])
245 if thing is None:
244 if thing is None:
246 return ""
245 return ""
247 return pycompat.bytestr(thing)
246 return pycompat.bytestr(thing)
248
247
249 def findsymbolicname(arg):
248 def findsymbolicname(arg):
250 """Find symbolic name for the given compiled expression; returns None
249 """Find symbolic name for the given compiled expression; returns None
251 if nothing found reliably"""
250 if nothing found reliably"""
252 while True:
251 while True:
253 func, data = arg
252 func, data = arg
254 if func is runsymbol:
253 if func is runsymbol:
255 return data
254 return data
256 elif func is runfilter:
255 elif func is runfilter:
257 arg = data[0]
256 arg = data[0]
258 else:
257 else:
259 return None
258 return None
260
259
261 def evalrawexp(context, mapping, arg):
260 def evalrawexp(context, mapping, arg):
262 """Evaluate given argument as a bare template object which may require
261 """Evaluate given argument as a bare template object which may require
263 further processing (such as folding generator of strings)"""
262 further processing (such as folding generator of strings)"""
264 func, data = arg
263 func, data = arg
265 return func(context, mapping, data)
264 return func(context, mapping, data)
266
265
267 def evalfuncarg(context, mapping, arg):
266 def evalfuncarg(context, mapping, arg):
268 """Evaluate given argument as value type"""
267 """Evaluate given argument as value type"""
269 thing = evalrawexp(context, mapping, arg)
268 thing = evalrawexp(context, mapping, arg)
270 thing = unwrapvalue(thing)
269 thing = unwrapvalue(thing)
271 # evalrawexp() may return string, generator of strings or arbitrary object
270 # evalrawexp() may return string, generator of strings or arbitrary object
272 # such as date tuple, but filter does not want generator.
271 # such as date tuple, but filter does not want generator.
273 if isinstance(thing, types.GeneratorType):
272 if isinstance(thing, types.GeneratorType):
274 thing = stringify(thing)
273 thing = stringify(thing)
275 return thing
274 return thing
276
275
277 def evalboolean(context, mapping, arg):
276 def evalboolean(context, mapping, arg):
278 """Evaluate given argument as boolean, but also takes boolean literals"""
277 """Evaluate given argument as boolean, but also takes boolean literals"""
279 func, data = arg
278 func, data = arg
280 if func is runsymbol:
279 if func is runsymbol:
281 thing = func(context, mapping, data, default=None)
280 thing = func(context, mapping, data, default=None)
282 if thing is None:
281 if thing is None:
283 # not a template keyword, takes as a boolean literal
282 # not a template keyword, takes as a boolean literal
284 thing = util.parsebool(data)
283 thing = util.parsebool(data)
285 else:
284 else:
286 thing = func(context, mapping, data)
285 thing = func(context, mapping, data)
287 thing = unwrapvalue(thing)
286 thing = unwrapvalue(thing)
288 if isinstance(thing, bool):
287 if isinstance(thing, bool):
289 return thing
288 return thing
290 # other objects are evaluated as strings, which means 0 is True, but
289 # other objects are evaluated as strings, which means 0 is True, but
291 # empty dict/list should be False as they are expected to be ''
290 # empty dict/list should be False as they are expected to be ''
292 return bool(stringify(thing))
291 return bool(stringify(thing))
293
292
294 def evalinteger(context, mapping, arg, err=None):
293 def evalinteger(context, mapping, arg, err=None):
295 v = evalfuncarg(context, mapping, arg)
294 v = evalfuncarg(context, mapping, arg)
296 try:
295 try:
297 return int(v)
296 return int(v)
298 except (TypeError, ValueError):
297 except (TypeError, ValueError):
299 raise error.ParseError(err or _('not an integer'))
298 raise error.ParseError(err or _('not an integer'))
300
299
301 def evalstring(context, mapping, arg):
300 def evalstring(context, mapping, arg):
302 return stringify(evalrawexp(context, mapping, arg))
301 return stringify(evalrawexp(context, mapping, arg))
303
302
304 def evalstringliteral(context, mapping, arg):
303 def evalstringliteral(context, mapping, arg):
305 """Evaluate given argument as string template, but returns symbol name
304 """Evaluate given argument as string template, but returns symbol name
306 if it is unknown"""
305 if it is unknown"""
307 func, data = arg
306 func, data = arg
308 if func is runsymbol:
307 if func is runsymbol:
309 thing = func(context, mapping, data, default=data)
308 thing = func(context, mapping, data, default=data)
310 else:
309 else:
311 thing = func(context, mapping, data)
310 thing = func(context, mapping, data)
312 return stringify(thing)
311 return stringify(thing)
313
312
314 _evalfuncbytype = {
313 _evalfuncbytype = {
315 bool: evalboolean,
314 bool: evalboolean,
316 bytes: evalstring,
315 bytes: evalstring,
317 int: evalinteger,
316 int: evalinteger,
318 }
317 }
319
318
320 def evalastype(context, mapping, arg, typ):
319 def evalastype(context, mapping, arg, typ):
321 """Evaluate given argument and coerce its type"""
320 """Evaluate given argument and coerce its type"""
322 try:
321 try:
323 f = _evalfuncbytype[typ]
322 f = _evalfuncbytype[typ]
324 except KeyError:
323 except KeyError:
325 raise error.ProgrammingError('invalid type specified: %r' % typ)
324 raise error.ProgrammingError('invalid type specified: %r' % typ)
326 return f(context, mapping, arg)
325 return f(context, mapping, arg)
327
326
328 def runinteger(context, mapping, data):
327 def runinteger(context, mapping, data):
329 return int(data)
328 return int(data)
330
329
331 def runstring(context, mapping, data):
330 def runstring(context, mapping, data):
332 return data
331 return data
333
332
334 def _recursivesymbolblocker(key):
333 def _recursivesymbolblocker(key):
335 def showrecursion(**args):
334 def showrecursion(**args):
336 raise error.Abort(_("recursive reference '%s' in template") % key)
335 raise error.Abort(_("recursive reference '%s' in template") % key)
337 return showrecursion
336 return showrecursion
338
337
339 def runsymbol(context, mapping, key, default=''):
338 def runsymbol(context, mapping, key, default=''):
340 v = context.symbol(mapping, key)
339 v = context.symbol(mapping, key)
341 if v is None:
340 if v is None:
342 # put poison to cut recursion. we can't move this to parsing phase
341 # put poison to cut recursion. we can't move this to parsing phase
343 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
342 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
344 safemapping = mapping.copy()
343 safemapping = mapping.copy()
345 safemapping[key] = _recursivesymbolblocker(key)
344 safemapping[key] = _recursivesymbolblocker(key)
346 try:
345 try:
347 v = context.process(key, safemapping)
346 v = context.process(key, safemapping)
348 except TemplateNotFound:
347 except TemplateNotFound:
349 v = default
348 v = default
350 if callable(v) and getattr(v, '_requires', None) is None:
349 if callable(v) and getattr(v, '_requires', None) is None:
351 # old templatekw: expand all keywords and resources
350 # old templatekw: expand all keywords and resources
352 props = {k: f(context, mapping, k)
351 props = {k: f(context, mapping, k)
353 for k, f in context._resources.items()}
352 for k, f in context._resources.items()}
354 props.update(mapping)
353 props.update(mapping)
355 return v(**pycompat.strkwargs(props))
354 return v(**pycompat.strkwargs(props))
356 if callable(v):
355 if callable(v):
357 # new templatekw
356 # new templatekw
358 try:
357 try:
359 return v(context, mapping)
358 return v(context, mapping)
360 except ResourceUnavailable:
359 except ResourceUnavailable:
361 # unsupported keyword is mapped to empty just like unknown keyword
360 # unsupported keyword is mapped to empty just like unknown keyword
362 return None
361 return None
363 return v
362 return v
364
363
365 def runtemplate(context, mapping, template):
364 def runtemplate(context, mapping, template):
366 for arg in template:
365 for arg in template:
367 yield evalrawexp(context, mapping, arg)
366 yield evalrawexp(context, mapping, arg)
368
367
369 def runfilter(context, mapping, data):
368 def runfilter(context, mapping, data):
370 arg, filt = data
369 arg, filt = data
371 thing = evalfuncarg(context, mapping, arg)
370 thing = evalfuncarg(context, mapping, arg)
372 try:
371 try:
373 return filt(thing)
372 return filt(thing)
374 except (ValueError, AttributeError, TypeError):
373 except (ValueError, AttributeError, TypeError):
375 sym = findsymbolicname(arg)
374 sym = findsymbolicname(arg)
376 if sym:
375 if sym:
377 msg = (_("template filter '%s' is not compatible with keyword '%s'")
376 msg = (_("template filter '%s' is not compatible with keyword '%s'")
378 % (pycompat.sysbytes(filt.__name__), sym))
377 % (pycompat.sysbytes(filt.__name__), sym))
379 else:
378 else:
380 msg = (_("incompatible use of template filter '%s'")
379 msg = (_("incompatible use of template filter '%s'")
381 % pycompat.sysbytes(filt.__name__))
380 % pycompat.sysbytes(filt.__name__))
382 raise error.Abort(msg)
381 raise error.Abort(msg)
383
382
384 def runmap(context, mapping, data):
383 def runmap(context, mapping, data):
385 darg, targ = data
384 darg, targ = data
386 d = evalrawexp(context, mapping, darg)
385 d = evalrawexp(context, mapping, darg)
387 if util.safehasattr(d, 'itermaps'):
386 if util.safehasattr(d, 'itermaps'):
388 diter = d.itermaps()
387 diter = d.itermaps()
389 else:
388 else:
390 try:
389 try:
391 diter = iter(d)
390 diter = iter(d)
392 except TypeError:
391 except TypeError:
393 sym = findsymbolicname(darg)
392 sym = findsymbolicname(darg)
394 if sym:
393 if sym:
395 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
394 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
396 else:
395 else:
397 raise error.ParseError(_("%r is not iterable") % d)
396 raise error.ParseError(_("%r is not iterable") % d)
398
397
399 for i, v in enumerate(diter):
398 for i, v in enumerate(diter):
400 lm = mapping.copy()
399 lm = mapping.copy()
401 lm['index'] = i
400 lm['index'] = i
402 if isinstance(v, dict):
401 if isinstance(v, dict):
403 lm.update(v)
402 lm.update(v)
404 lm['originalnode'] = mapping.get('node')
403 lm['originalnode'] = mapping.get('node')
405 yield evalrawexp(context, lm, targ)
404 yield evalrawexp(context, lm, targ)
406 else:
405 else:
407 # v is not an iterable of dicts, this happen when 'key'
406 # v is not an iterable of dicts, this happen when 'key'
408 # has been fully expanded already and format is useless.
407 # has been fully expanded already and format is useless.
409 # If so, return the expanded value.
408 # If so, return the expanded value.
410 yield v
409 yield v
411
410
412 def runmember(context, mapping, data):
411 def runmember(context, mapping, data):
413 darg, memb = data
412 darg, memb = data
414 d = evalrawexp(context, mapping, darg)
413 d = evalrawexp(context, mapping, darg)
415 if util.safehasattr(d, 'tomap'):
414 if util.safehasattr(d, 'tomap'):
416 lm = mapping.copy()
415 lm = mapping.copy()
417 lm.update(d.tomap())
416 lm.update(d.tomap())
418 return runsymbol(context, lm, memb)
417 return runsymbol(context, lm, memb)
419 if util.safehasattr(d, 'get'):
418 if util.safehasattr(d, 'get'):
420 return getdictitem(d, memb)
419 return getdictitem(d, memb)
421
420
422 sym = findsymbolicname(darg)
421 sym = findsymbolicname(darg)
423 if sym:
422 if sym:
424 raise error.ParseError(_("keyword '%s' has no member") % sym)
423 raise error.ParseError(_("keyword '%s' has no member") % sym)
425 else:
424 else:
426 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
425 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
427
426
428 def runnegate(context, mapping, data):
427 def runnegate(context, mapping, data):
429 data = evalinteger(context, mapping, data,
428 data = evalinteger(context, mapping, data,
430 _('negation needs an integer argument'))
429 _('negation needs an integer argument'))
431 return -data
430 return -data
432
431
433 def runarithmetic(context, mapping, data):
432 def runarithmetic(context, mapping, data):
434 func, left, right = data
433 func, left, right = data
435 left = evalinteger(context, mapping, left,
434 left = evalinteger(context, mapping, left,
436 _('arithmetic only defined on integers'))
435 _('arithmetic only defined on integers'))
437 right = evalinteger(context, mapping, right,
436 right = evalinteger(context, mapping, right,
438 _('arithmetic only defined on integers'))
437 _('arithmetic only defined on integers'))
439 try:
438 try:
440 return func(left, right)
439 return func(left, right)
441 except ZeroDivisionError:
440 except ZeroDivisionError:
442 raise error.Abort(_('division by zero is not defined'))
441 raise error.Abort(_('division by zero is not defined'))
443
442
444 def getdictitem(dictarg, key):
443 def getdictitem(dictarg, key):
445 val = dictarg.get(key)
444 val = dictarg.get(key)
446 if val is None:
445 if val is None:
447 return
446 return
448 return wraphybridvalue(dictarg, key, val)
447 return wraphybridvalue(dictarg, key, val)
General Comments 0
You need to be logged in to leave comments. Login now